- added settings page and function
- add Role Ring for avatar and display role for user nav
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
import { Session } from '@/lib/auth/session';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
|
||||
interface AvatarUserProps {
|
||||
session: Session | null | undefined;
|
||||
className?: string;
|
||||
textSize?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
|
||||
}
|
||||
|
||||
const AvatarUser = ({
|
||||
session,
|
||||
className,
|
||||
textSize = 'md',
|
||||
}: AvatarUserProps) => {
|
||||
const imagePath = session?.user?.image
|
||||
? `./files/${session.user.image}`
|
||||
: undefined;
|
||||
const shortName = session?.user?.name
|
||||
?.split(' ')
|
||||
.slice(0, 2)
|
||||
.map((name) => name[0])
|
||||
.join('');
|
||||
|
||||
return (
|
||||
<Avatar className={className}>
|
||||
<AvatarImage src={imagePath} />
|
||||
<AvatarFallback
|
||||
className={cn(
|
||||
'bg-orange-400 text-white',
|
||||
textSize === 'sm' && 'text-xs',
|
||||
textSize === 'md' && 'text-sm',
|
||||
textSize === 'lg' && 'text-xl',
|
||||
textSize === 'xl' && 'text-2xl',
|
||||
textSize === '2xl' && 'text-3xl',
|
||||
textSize === '3xl' && 'text-4xl',
|
||||
)}
|
||||
>
|
||||
{shortName}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarUser;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Separator } from '@base-ui/react/separator'
|
||||
import { BellIcon } from '@phosphor-icons/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from './auth/auth-provider'
|
||||
import RouterBreadcrumb from './sidebar/RouterBreadcrumb'
|
||||
import { Badge } from './ui/badge'
|
||||
import { Button } from './ui/button'
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
import { Separator } from '@base-ui/react/separator';
|
||||
import { BellIcon } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RouterBreadcrumb from './sidebar/RouterBreadcrumb';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from './ui/dropdown-menu'
|
||||
import { SidebarTrigger } from './ui/sidebar'
|
||||
} from './ui/dropdown-menu';
|
||||
import { SidebarTrigger } from './ui/sidebar';
|
||||
|
||||
export default function Header() {
|
||||
const { t } = useTranslation()
|
||||
const { data: session } = useAuth()
|
||||
const { t } = useTranslation();
|
||||
const { data: session } = useSession();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -86,5 +86,5 @@ export default function Header() {
|
||||
<span className="font-medium">Start - SSR Demos</span>
|
||||
</Link> */}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
10
src/components/auth/AdminShow.tsx
Normal file
10
src/components/auth/AdminShow.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
|
||||
const AdminShow = ({ children }: { children: React.ReactNode }) => {
|
||||
const { data } = useSession();
|
||||
const isAdmin = data?.user?.role ? data?.user?.role === 'admin' : false;
|
||||
|
||||
return isAdmin && children;
|
||||
};
|
||||
|
||||
export default AdminShow;
|
||||
10
src/components/auth/AuthShow.tsx
Normal file
10
src/components/auth/AuthShow.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
|
||||
const AuthShow = ({ children }: { children: React.ReactNode }) => {
|
||||
const { data } = useSession();
|
||||
const isAuth = !!data;
|
||||
|
||||
return isAuth && children;
|
||||
};
|
||||
|
||||
export default AuthShow;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useSessionQuery } from '@/hooks/use-session'
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
const AuthContext = createContext<ReturnType<typeof useSessionQuery> | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const sessionQuery = useSessionQuery()
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={sessionQuery}>{children}</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(AuthContext)
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
44
src/components/avatar/AvatarUser.tsx
Normal file
44
src/components/avatar/AvatarUser.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
|
||||
import RoleRing from './RoleRing';
|
||||
|
||||
interface AvatarUserProps {
|
||||
className?: string;
|
||||
textSize?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
|
||||
}
|
||||
|
||||
const AvatarUser = ({ className, textSize = 'md' }: AvatarUserProps) => {
|
||||
const { data: session } = useSession();
|
||||
const imagePath = session?.user?.image
|
||||
? `./data/avatar/${session?.user?.image}`
|
||||
: undefined;
|
||||
const shortName = session?.user?.name
|
||||
?.split(' ')
|
||||
.slice(0, 2)
|
||||
.map((name) => name[0])
|
||||
.join('');
|
||||
|
||||
return (
|
||||
<RoleRing type={session?.user?.role}>
|
||||
<Avatar className={className}>
|
||||
<AvatarImage src={imagePath} />
|
||||
<AvatarFallback
|
||||
className={cn(
|
||||
'bg-orange-400 text-white',
|
||||
textSize === 'sm' && 'text-xs',
|
||||
textSize === 'md' && 'text-sm',
|
||||
textSize === 'lg' && 'text-xl',
|
||||
textSize === 'xl' && 'text-2xl',
|
||||
textSize === '2xl' && 'text-3xl',
|
||||
textSize === '3xl' && 'text-4xl',
|
||||
)}
|
||||
>
|
||||
{shortName}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</RoleRing>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarUser;
|
||||
50
src/components/avatar/RoleBadge.tsx
Normal file
50
src/components/avatar/RoleBadge.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { VariantProps } from 'class-variance-authority';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge, badgeVariants } from '../ui/badge';
|
||||
|
||||
type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
|
||||
|
||||
type RoleProps = {
|
||||
type?: string | null; // type can be any string, undefined, or null
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const RoleBadge = ({ type, className }: RoleProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// List all valid badge variant keys
|
||||
const validBadgeVariants: BadgeVariant[] = [
|
||||
'default',
|
||||
'secondary',
|
||||
'destructive',
|
||||
'outline',
|
||||
'ghost',
|
||||
'link',
|
||||
'admin',
|
||||
'user',
|
||||
'member',
|
||||
'owner',
|
||||
];
|
||||
|
||||
const LABEL_VALUE = {
|
||||
admin: t('roleTags.admin'),
|
||||
user: t('roleTags.user'),
|
||||
member: t('roleTags.member'),
|
||||
owner: t('roleTags.owner'),
|
||||
};
|
||||
|
||||
// Determine the actual variant to apply.
|
||||
// If 'type' is a valid variant key, use it. Otherwise, fallback to 'default'.
|
||||
const displayVariant: BadgeVariant =
|
||||
type && validBadgeVariants.includes(type as BadgeVariant)
|
||||
? (type as BadgeVariant)
|
||||
: 'default';
|
||||
|
||||
return (
|
||||
<Badge variant={displayVariant} className={className}>
|
||||
{LABEL_VALUE[(type as keyof typeof LABEL_VALUE) || 'default']}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleBadge;
|
||||
28
src/components/avatar/RoleRing.tsx
Normal file
28
src/components/avatar/RoleRing.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const RING_TYPE = {
|
||||
admin: 'after:inset-ring-cyan-500',
|
||||
user: 'after:inset-ring-green-500',
|
||||
member: 'after:inset-ring-blue-500',
|
||||
owner: 'after:inset-ring-red-500',
|
||||
};
|
||||
|
||||
type RoleRingProps = {
|
||||
children: React.ReactNode;
|
||||
type?: string | null;
|
||||
};
|
||||
|
||||
const RoleRing: React.FC<RoleRingProps> = ({ children, type }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-fit relative after:rounded-full after:block after:w-full after:h-full after:absolute after:top-0 after:left-0 after:inset-ring-3',
|
||||
RING_TYPE[type as keyof typeof RING_TYPE],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleRing;
|
||||
@@ -1,32 +1,20 @@
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { uploadProfileImage } from '@/server/profile-service';
|
||||
import { authClient, useSession } from '@/lib/auth-client';
|
||||
import { uploadProfileImage } from '@/service/profile.api';
|
||||
import { ProfileInput, profileUpdateSchema } from '@/service/profile.schema';
|
||||
import { UserCircleIcon } from '@phosphor-icons/react';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import z from 'zod';
|
||||
import { useAuth } from '../auth/auth-provider';
|
||||
import AvatarUser from '../AvatarUser';
|
||||
import AvatarUser from '../avatar/AvatarUser';
|
||||
import RoleBadge from '../avatar/RoleBadge';
|
||||
import { Button } from '../ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
||||
import { Input } from '../ui/input';
|
||||
|
||||
const profileSchema = z.object({
|
||||
name: z.string().nonempty(
|
||||
i18n.t('profile.messages.is_required', {
|
||||
field: i18n.t('profile.form.name'),
|
||||
}),
|
||||
),
|
||||
image: z.instanceof(File).optional(),
|
||||
});
|
||||
|
||||
type Profile = z.infer<typeof profileSchema>;
|
||||
|
||||
const defaultValues: Profile = {
|
||||
const defaultValues: ProfileInput = {
|
||||
name: '',
|
||||
image: undefined,
|
||||
};
|
||||
@@ -34,7 +22,7 @@ const defaultValues: Profile = {
|
||||
const ProfileForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { data: session, isLoading } = useAuth();
|
||||
const { data: session, isPending } = useSession();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useForm({
|
||||
@@ -43,8 +31,8 @@ const ProfileForm = () => {
|
||||
name: session?.user?.name || '',
|
||||
},
|
||||
validators: {
|
||||
onSubmit: profileSchema,
|
||||
onChange: profileSchema,
|
||||
onSubmit: profileUpdateSchema,
|
||||
onChange: profileUpdateSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
@@ -70,7 +58,9 @@ const ProfileForm = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['session'] });
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ['auth', 'session'],
|
||||
});
|
||||
toast.success(t('profile.messages.update_success'));
|
||||
},
|
||||
onError: (ctx) => {
|
||||
@@ -82,11 +72,11 @@ const ProfileForm = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) return null;
|
||||
if (isPending) return null;
|
||||
if (!session?.user?.name) return null;
|
||||
|
||||
return (
|
||||
<Card className="@container/card col-span-1 @xl/main:col-span-2">
|
||||
<Card className="@container/card col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<UserCircleIcon size={20} />
|
||||
@@ -104,11 +94,7 @@ const ProfileForm = () => {
|
||||
>
|
||||
<FieldGroup>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<AvatarUser
|
||||
session={session}
|
||||
className="h-20 w-20"
|
||||
textSize="2xl"
|
||||
/>
|
||||
<AvatarUser className="h-20 w-20" textSize="2xl" />
|
||||
<form.Field
|
||||
name="image"
|
||||
children={(field) => {
|
||||
@@ -173,13 +159,9 @@ const ProfileForm = () => {
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">{t('profile.form.role')}</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
value={session?.user?.role || ''}
|
||||
disabled
|
||||
className="disabled:opacity-80"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<RoleBadge type={session?.user?.role} />
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">{t('ui.update_btn')}</Button>
|
||||
|
||||
196
src/components/form/settings-form.tsx
Normal file
196
src/components/form/settings-form.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { settingQueries } from '@/service/queries';
|
||||
import { updateSettings } from '@/service/setting.api';
|
||||
import { settingSchema, SettingsInput } from '@/service/setting.schema';
|
||||
import { GearIcon } from '@phosphor-icons/react';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '../ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
||||
import { Input } from '../ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../ui/select';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
|
||||
const defaultValues: SettingsInput = {
|
||||
site_language: '',
|
||||
site_name: '',
|
||||
site_description: '',
|
||||
site_keywords: '',
|
||||
};
|
||||
|
||||
const SettingsForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: settings } = useQuery(settingQueries.list());
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: updateSettings,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: settingQueries.all });
|
||||
toast.success(t('settings.messages.update_success'));
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
...defaultValues,
|
||||
site_name: settings?.site_name?.value || '',
|
||||
site_description: settings?.site_description?.value || '',
|
||||
site_keywords: settings?.site_keywords?.value || '',
|
||||
site_language: settings?.site_language?.value || '',
|
||||
},
|
||||
validators: {
|
||||
onSubmit: settingSchema,
|
||||
onChange: settingSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
updateMutation.mutate({ data: value as SettingsInput });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="@container/card col-span-1 @xl/main:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<GearIcon size={20} />
|
||||
{t('settings.ui.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
id="settings-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="site_name"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid} className="col-span-2">
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{t('settings.form.name')}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="site_description"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid} className="col-span-2">
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{t('settings.form.description')}
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
rows={4}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="site_keywords"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid} className="col-span-2">
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{t('settings.form.keywords')}
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
rows={4}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="site_language"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid} className="col-span-2">
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{t('settings.form.language')}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={(value) => field.handleChange(value)}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="vi">Vietnamese</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Field>
|
||||
<Button type="submit">{t('ui.update_btn')}</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsForm;
|
||||
@@ -51,7 +51,7 @@ const SignInForm = () => {
|
||||
{
|
||||
onSuccess: () => {
|
||||
navigate({ to: '/' })
|
||||
queryClient.invalidateQueries({ queryKey: ['session'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['auth', 'session'] });
|
||||
toast.success(t('loginPage.messages.login_success'))
|
||||
},
|
||||
onError: (ctx) => {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { GaugeIcon, HouseIcon } from '@phosphor-icons/react'
|
||||
import { createLink } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GaugeIcon, GearIcon, HouseIcon } from '@phosphor-icons/react';
|
||||
import { createLink } from '@tanstack/react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AdminShow from '../auth/AdminShow';
|
||||
import AuthShow from '../auth/AuthShow';
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '../ui/sidebar'
|
||||
} from '../ui/sidebar';
|
||||
|
||||
const SidebarMenuButtonLink = createLink(SidebarMenuButton)
|
||||
const SidebarMenuButtonLink = createLink(SidebarMenuButton);
|
||||
|
||||
const NavMain = () => {
|
||||
const { t } = useTranslation()
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
@@ -24,18 +27,30 @@ const NavMain = () => {
|
||||
<HouseIcon size={24} />
|
||||
{t('nav.home')}
|
||||
</SidebarMenuButtonLink>
|
||||
<SidebarMenuButtonLink
|
||||
to="/dashboard"
|
||||
className="cursor-pointer"
|
||||
tooltip={t('nav.dashboard')}
|
||||
>
|
||||
<GaugeIcon size={24} />
|
||||
{t('nav.dashboard')}
|
||||
</SidebarMenuButtonLink>
|
||||
<AuthShow>
|
||||
<SidebarMenuButtonLink
|
||||
to="/dashboard"
|
||||
className="cursor-pointer"
|
||||
tooltip={t('nav.dashboard')}
|
||||
>
|
||||
<GaugeIcon size={24} />
|
||||
{t('nav.dashboard')}
|
||||
</SidebarMenuButtonLink>
|
||||
<AdminShow>
|
||||
<SidebarMenuButtonLink
|
||||
to="/settings"
|
||||
className="cursor-pointer"
|
||||
tooltip={t('nav.settings')}
|
||||
>
|
||||
<GearIcon size={24} />
|
||||
{t('nav.settings')}
|
||||
</SidebarMenuButtonLink>
|
||||
</AdminShow>
|
||||
</AuthShow>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default NavMain
|
||||
export default NavMain;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { authClient, useSession } from '@/lib/auth-client';
|
||||
import {
|
||||
DotsThreeVerticalIcon,
|
||||
KeyIcon,
|
||||
@@ -7,12 +7,11 @@ import {
|
||||
UserCircleIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createLink, useNavigate } from '@tanstack/react-router';
|
||||
import { createLink, Link, useNavigate } from '@tanstack/react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { useAuth } from '../auth/auth-provider';
|
||||
import AvatarUser from '../AvatarUser';
|
||||
|
||||
import AvatarUser from '../avatar/AvatarUser';
|
||||
import RoleBadge from '../avatar/RoleBadge';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -30,21 +29,20 @@ import {
|
||||
} from '../ui/sidebar';
|
||||
|
||||
const SidebarMenuButtonLink = createLink(SidebarMenuButton);
|
||||
const DropdownMenuItemLink = createLink(DropdownMenuItem);
|
||||
|
||||
const NavUser = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { isMobile } = useSidebar();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: session, isLoading } = useAuth();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const signout = async () => {
|
||||
await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
navigate({ to: '/' });
|
||||
queryClient.invalidateQueries({ queryKey: ['session'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['auth', 'session'] });
|
||||
toast.success(t('loginPage.messages.logout_success'));
|
||||
},
|
||||
onError: (ctx) => {
|
||||
@@ -54,7 +52,6 @@ const NavUser = () => {
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return null;
|
||||
if (!session?.user)
|
||||
return (
|
||||
<SidebarMenu>
|
||||
@@ -81,7 +78,7 @@ const NavUser = () => {
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
|
||||
tooltip={session?.user?.name}
|
||||
>
|
||||
<AvatarUser session={session} className="h-8 w-8" />
|
||||
<AvatarUser className="h-8 w-8" />
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">
|
||||
{session?.user?.name}
|
||||
@@ -95,16 +92,22 @@ const NavUser = () => {
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
sideOffset={15}
|
||||
>
|
||||
{/* Dropdown menu content */}
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<AvatarUser session={session} className="h-8 w-8" />
|
||||
<AvatarUser className="h-8 w-8" />
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">
|
||||
{session?.user?.name}
|
||||
</span>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="truncate font-medium">
|
||||
{session?.user?.name}
|
||||
</span>
|
||||
<RoleBadge
|
||||
type={session?.user?.role}
|
||||
className="text-[10px] px-2 py-0 leading-0.5 h-4"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate text-xs">
|
||||
{session?.user?.email}
|
||||
</span>
|
||||
@@ -113,21 +116,22 @@ const NavUser = () => {
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItemLink to="/profile" className="cursor-pointer">
|
||||
<UserCircleIcon size={28} />
|
||||
{t('nav.account')}
|
||||
</DropdownMenuItemLink>
|
||||
<DropdownMenuItemLink
|
||||
to="/change-password"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<KeyIcon size={28} />
|
||||
{t('nav.change_password')}
|
||||
</DropdownMenuItemLink>
|
||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||
<Link to="/profile">
|
||||
<UserCircleIcon size={28} />
|
||||
{t('nav.account')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||
<Link to="/change-password">
|
||||
<KeyIcon size={28} />
|
||||
{t('nav.change_password')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={signout} className="cursor-pointer">
|
||||
<DropdownMenuItem onSelect={signout} className="cursor-pointer">
|
||||
<SignOutIcon size={28} />
|
||||
{t('ui.logout_btn')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Slot } from 'radix-ui';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
"h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-2.5! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-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 transition-colors overflow-hidden group/badge",
|
||||
'h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-2.5! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-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 transition-colors overflow-hidden group/badge',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 dark:bg-input/30",
|
||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
||||
destructive:
|
||||
'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
|
||||
outline:
|
||||
'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 dark:bg-input/30',
|
||||
ghost:
|
||||
'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
admin: 'bg-cyan-100 text-cyan-600 [a]:hover:bg-cyan-200 ',
|
||||
user: 'bg-green-100 text-green-600 [a]:hover:bg-green-200',
|
||||
member: 'bg-blue-100 text-blue-600 [a]:hover:bg-blue-200',
|
||||
owner: 'bg-red-100 text-red-600 [a]:hover:bg-red-200',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
variant = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
}: React.ComponentProps<'span'> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
const Comp = asChild ? Slot.Root : 'span';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -39,7 +47,7 @@ function Badge({
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as React from "react"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon, CaretRightIcon } from "@phosphor-icons/react"
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CaretRightIcon, CheckIcon } from '@phosphor-icons/react';
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
@@ -15,7 +15,7 @@ function DropdownMenuPortal({
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
@@ -26,12 +26,12 @@ function DropdownMenuTrigger({
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
align = "start",
|
||||
align = 'start',
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
@@ -41,11 +41,14 @@ function DropdownMenuContent({
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto data-[state=closed]:overflow-hidden", className )}
|
||||
className={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto data-[state=closed]:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
@@ -53,17 +56,17 @@ function DropdownMenuGroup({
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: 'default' | 'destructive';
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@@ -72,11 +75,11 @@ function DropdownMenuItem({
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3.5 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
@@ -90,23 +93,22 @@ function DropdownMenuCheckboxItem({
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground min-h-7 gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-3.5 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
<CheckIcon />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
@@ -117,7 +119,7 @@ function DropdownMenuRadioGroup({
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
@@ -130,22 +132,21 @@ function DropdownMenuRadioItem({
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground min-h-7 gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-3.5 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
<CheckIcon />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
@@ -153,16 +154,19 @@ function DropdownMenuLabel({
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs data-[inset]:pl-8", className)}
|
||||
className={cn(
|
||||
'text-muted-foreground px-2 py-1.5 text-xs data-[inset]:pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
@@ -172,29 +176,32 @@ function DropdownMenuSeparator({
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border/50 -mx-1 my-1 h-px", className)}
|
||||
className={cn('bg-border/50 -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest", className)}
|
||||
className={cn(
|
||||
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
@@ -203,7 +210,7 @@ function DropdownMenuSubTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
@@ -211,14 +218,14 @@ function DropdownMenuSubTrigger({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs [&_svg:not([class*='size-'])]:size-3.5 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<CaretRightIcon className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
@@ -228,26 +235,29 @@ function DropdownMenuSubContent({
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden", className )}
|
||||
className={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
|
||||
183
src/components/ui/select.tsx
Normal file
183
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import * as React from "react"
|
||||
import { Select as SelectPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CaretDownIcon, CheckIcon, CaretUpIcon } from "@phosphor-icons/react"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return (
|
||||
<SelectPrimitive.Group
|
||||
data-slot="select-group"
|
||||
className={cn("scroll-my-1 p-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground bg-input/20 dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<CaretDownIcon className="text-muted-foreground size-3.5 pointer-events-none" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 min-w-32 rounded-lg shadow-md ring-1 duration-100 relative z-50 max-h-(--radix-select-content-available-height) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto", 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}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
data-position={position}
|
||||
className={cn(
|
||||
"data-[position=popper]:h-[var(--radix-select-trigger-height)] data-[position=popper]:w-full data-[position=popper]:min-w-[var(--radix-select-trigger-width)]",
|
||||
position === "popper" && ""
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border/50 -mx-1 my-1 h-px pointer-events-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<CaretUpIcon
|
||||
/>
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<CaretDownIcon
|
||||
/>
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -1,55 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Slot } from 'radix-ui';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
} from '@/components/ui/sheet';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { SidebarIcon } from "@phosphor-icons/react"
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SidebarIcon } from '@phosphor-icons/react';
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = '16rem';
|
||||
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
state: 'expanded' | 'collapsed';
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
throw new Error('useSidebar must be used within a SidebarProvider.');
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
@@ -60,37 +60,37 @@ function SidebarProvider({
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}: React.ComponentProps<'div'> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
const openState = typeof value === 'function' ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open]
|
||||
)
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
@@ -99,18 +99,18 @@ function SidebarProvider({
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
const state = open ? 'expanded' : 'collapsed';
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
@@ -122,8 +122,8 @@ function SidebarProvider({
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
@@ -131,50 +131,50 @@ function SidebarProvider({
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
'--sidebar-width': SIDEBAR_WIDTH,
|
||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offExamples",
|
||||
side = 'left',
|
||||
variant = 'sidebar',
|
||||
collapsible = 'offExamples',
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offExamples" | "icon" | "none"
|
||||
}: React.ComponentProps<'div'> & {
|
||||
side?: 'left' | 'right';
|
||||
variant?: 'sidebar' | 'floating' | 'inset';
|
||||
collapsible?: 'offExamples' | 'icon' | 'none';
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
if (collapsible === 'none') {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className
|
||||
'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
@@ -187,7 +187,7 @@ function Sidebar({
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
@@ -199,14 +199,14 @@ function Sidebar({
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-collapsible={state === 'collapsed' ? collapsible : ''}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
@@ -215,26 +215,26 @@ function Sidebar({
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent",
|
||||
"group-data-[collapsible=offExamples]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
'transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent',
|
||||
'group-data-[collapsible=offExamples]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offExamples]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offExamples]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offExamples]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offExamples]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -247,7 +247,7 @@ function Sidebar({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
@@ -255,7 +255,7 @@ function SidebarTrigger({
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -265,20 +265,19 @@ function SidebarTrigger({
|
||||
size="icon-sm"
|
||||
className={cn(className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<SidebarIcon
|
||||
/>
|
||||
<SidebarIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -289,30 +288,30 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offExamples]:bg-sidebar group-data-[collapsible=offExamples]:translate-x-0 group-data-[collapsible=offExamples]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offExamples]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offExamples]_&]:-left-2",
|
||||
className
|
||||
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'hover:group-data-[collapsible=offExamples]:bg-sidebar group-data-[collapsible=offExamples]:translate-x-0 group-data-[collapsible=offExamples]:after:left-full',
|
||||
'[[data-side=left][data-collapsible=offExamples]_&]:-right-2',
|
||||
'[[data-side=right][data-collapsible=offExamples]_&]:-left-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 relative flex w-full flex-1 flex-col",
|
||||
className
|
||||
'bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 relative flex w-full flex-1 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
@@ -323,32 +322,35 @@ function SidebarInput({
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-muted/20 dark:bg-muted/30 border-input h-8 w-full", className)}
|
||||
className={cn(
|
||||
'bg-muted/20 dark:bg-muted/30 border-input h-8 w-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("gap-2 p-2 flex flex-col", className)}
|
||||
className={cn('gap-2 p-2 flex flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn("gap-2 p-2 flex flex-col", className)}
|
||||
className={cn('gap-2 p-2 flex flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
@@ -359,152 +361,153 @@ function SidebarSeparator({
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
className={cn('bg-sidebar-border mx-2 w-auto', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"no-scrollbar gap-0 flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
'no-scrollbar gap-0 flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn(
|
||||
"px-2 py-1 relative flex w-full min-w-0 flex-col",
|
||||
className
|
||||
'px-2 py-1 relative flex w-full min-w-0 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "div"
|
||||
}: React.ComponentProps<'div'> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : 'div';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0",
|
||||
className
|
||||
'text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
}: React.ComponentProps<'button'> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
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 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0",
|
||||
className
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("text-xs w-full", className)}
|
||||
className={cn('text-xs w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("gap-px flex w-full min-w-0 flex-col", className)}
|
||||
className={cn('gap-px flex w-full min-w-0 flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
className={cn('group/menu-item relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-[calc(var(--radius-sm)+2px)] p-2 text-left text-xs transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
'ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-[calc(var(--radius-sm)+2px)] p-2 text-left text-xs transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline: "bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
outline:
|
||||
'bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-xs",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-xs group-data-[collapsible=icon]:p-0!",
|
||||
default: 'h-8 text-xs',
|
||||
sm: 'h-7 text-xs',
|
||||
lg: 'h-12 text-xs group-data-[collapsible=icon]:p-0!',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
}: React.ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
const Comp = asChild ? Slot.Root : 'button';
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
@@ -515,16 +518,16 @@ function SidebarMenuButton({
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
if (typeof tooltip === 'string') {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -533,11 +536,11 @@ function SidebarMenuButton({
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
hidden={state !== 'collapsed' || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
@@ -545,61 +548,61 @@ function SidebarMenuAction({
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}: React.ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
const Comp = asChild ? Slot.Root : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
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 aspect-square w-5 rounded-[calc(var(--radius-sm)-2px)] p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>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 aspect-square w-5 rounded-[calc(var(--radius-sm)-2px)] p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0',
|
||||
showOnHover &&
|
||||
"peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-open:opacity-100 md:opacity-0",
|
||||
className
|
||||
'peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-open:opacity-100 md:opacity-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 rounded-[calc(var(--radius-sm)-2px)] px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
'text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 rounded-[calc(var(--radius-sm)-2px)] px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}: React.ComponentProps<'div'> & {
|
||||
showIcon?: boolean;
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const [width] = React.useState(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
})
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("h-8 gap-2 rounded-md px-2 flex items-center", className)}
|
||||
className={cn('h-8 gap-2 rounded-md px-2 flex items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
@@ -613,51 +616,54 @@ function SidebarMenuSkeleton({
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
'--skeleton-width': width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn("border-sidebar-border mx-3.5 translate-x-px gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden flex min-w-0 flex-col", className)}
|
||||
className={cn(
|
||||
'border-sidebar-border mx-3.5 translate-x-px gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden flex min-w-0 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
}: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
className={cn('group/menu-sub-item relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
asChild = false,
|
||||
size = "md",
|
||||
size = 'md',
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
}: React.ComponentProps<'a'> & {
|
||||
asChild?: boolean;
|
||||
size?: 'sm' | 'md';
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "a"
|
||||
const Comp = asChild ? Slot.Root : 'a';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -666,12 +672,12 @@ function SidebarMenuSubButton({
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-xs data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0",
|
||||
className
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-xs data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -699,4 +705,4 @@ export {
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
};
|
||||
|
||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input bg-input/20 dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-md border px-2 py-2 text-sm transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
Reference in New Issue
Block a user