Update theme toggle functionality and UI components
Implemented a new ModeToggle feature for theme switching in personal account dropdown. The changes also made adjustments to several UI components, such as transforming Dialog to AlertDialog in transfer-ownership-dialog, and introducing invitation-submit-button in team-accounts. Some minor amendments include text changes and styling modifications.
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@tanstack/react-query": "5.28.6",
|
||||
"lucide-react": "^0.363.0",
|
||||
"next-themes": "0.3.0",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"react-i18next": "^14.1.0",
|
||||
"sonner": "^1.4.41",
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { SubMenuModeToggle } from '@kit/ui/mode-toggle';
|
||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
@@ -34,6 +35,7 @@ export function PersonalAccountDropdown({
|
||||
signOutRequested,
|
||||
showProfileName,
|
||||
paths,
|
||||
features,
|
||||
}: {
|
||||
className?: string;
|
||||
session: Session | null;
|
||||
@@ -42,6 +44,9 @@ export function PersonalAccountDropdown({
|
||||
paths: {
|
||||
home: string;
|
||||
};
|
||||
features: {
|
||||
enableThemeToggle: boolean;
|
||||
};
|
||||
}) {
|
||||
const { data: personalAccountData } = usePersonalAccountData();
|
||||
const authUser = session?.user;
|
||||
@@ -156,6 +161,12 @@ export function PersonalAccountDropdown({
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<If condition={features.enableThemeToggle}>
|
||||
<SubMenuModeToggle />
|
||||
</If>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
role={'button'}
|
||||
className={'cursor-pointer'}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
|
||||
import { isBrowser } from '@kit/shared/utils';
|
||||
import { Divider } from '@kit/ui/divider';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
|
||||
import { MagicLinkAuthContainer } from './magic-link-auth-container';
|
||||
import { OauthProviders } from './oauth-providers';
|
||||
@@ -46,7 +46,7 @@ export function SignInMethodsContainer(props: {
|
||||
</If>
|
||||
|
||||
<If condition={props.providers.oAuth.length}>
|
||||
<Divider />
|
||||
<Separator />
|
||||
|
||||
<OauthProviders
|
||||
enabledProviders={props.providers.oAuth}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
|
||||
import { isBrowser } from '@kit/shared/utils';
|
||||
import { Divider } from '@kit/ui/divider';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
|
||||
import { MagicLinkAuthContainer } from './magic-link-auth-container';
|
||||
import { OauthProviders } from './oauth-providers';
|
||||
@@ -24,9 +24,7 @@ export function SignUpMethodsContainer(props: {
|
||||
|
||||
inviteToken?: string;
|
||||
}) {
|
||||
const redirectUrl = isBrowser()
|
||||
? new URL(props.paths.callback, window?.location.origin).toString()
|
||||
: '';
|
||||
const redirectUrl = getCallbackUrl(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -42,7 +40,7 @@ export function SignUpMethodsContainer(props: {
|
||||
</If>
|
||||
|
||||
<If condition={props.providers.oAuth.length}>
|
||||
<Divider />
|
||||
<Separator />
|
||||
|
||||
<OauthProviders
|
||||
enabledProviders={props.providers.oAuth}
|
||||
@@ -56,3 +54,26 @@ export function SignUpMethodsContainer(props: {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getCallbackUrl(props: {
|
||||
paths: {
|
||||
callback: string;
|
||||
appHome: string;
|
||||
};
|
||||
|
||||
inviteToken?: string;
|
||||
}) {
|
||||
if (!isBrowser()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const redirectPath = props.paths.callback;
|
||||
const origin = window.location.origin;
|
||||
const url = new URL(redirectPath, origin);
|
||||
|
||||
if (props.inviteToken) {
|
||||
url.searchParams.set('invite_token', props.inviteToken);
|
||||
}
|
||||
|
||||
return url.href;
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './members/invite-members-dialog-container';
|
||||
export * from './settings/team-account-danger-zone';
|
||||
export * from './invitations/account-invitations-table';
|
||||
export * from './settings/team-account-settings-container';
|
||||
export * from './invitations/accept-invitation-container';
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { acceptInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
||||
import { InvitationSubmitButton } from './invitation-submit-button';
|
||||
import { SignOutInvitationButton } from './sign-out-invitation-button';
|
||||
|
||||
export function AcceptInvitationContainer(props: {
|
||||
inviteToken: string;
|
||||
|
||||
invitation: {
|
||||
id: string;
|
||||
|
||||
account: {
|
||||
name: string;
|
||||
id: string;
|
||||
picture_url: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
paths: {
|
||||
signOutNext: string;
|
||||
accountHome: string;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className={'flex flex-col items-center space-y-8'}>
|
||||
<Heading className={'text-center'} level={5}>
|
||||
<Trans
|
||||
i18nKey={'teams:acceptInvitationHeading'}
|
||||
values={{
|
||||
accountName: props.invitation.account.name,
|
||||
}}
|
||||
/>
|
||||
</Heading>
|
||||
|
||||
<If condition={props.invitation.account.picture_url}>
|
||||
{(url) => (
|
||||
<Image
|
||||
alt={`Logo`}
|
||||
src={url}
|
||||
width={64}
|
||||
height={64}
|
||||
className={'object-cover'}
|
||||
/>
|
||||
)}
|
||||
</If>
|
||||
|
||||
<div className={'text-muted-foreground text-center text-sm'}>
|
||||
<Trans
|
||||
i18nKey={'teams:acceptInvitationDescription'}
|
||||
values={{
|
||||
accountName: props.invitation.account.name,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col space-y-2.5'}>
|
||||
<form className={'w-full'} action={acceptInvitationAction}>
|
||||
<input type="hidden" name={'inviteToken'} value={props.inviteToken} />
|
||||
|
||||
<input
|
||||
type={'hidden'}
|
||||
name={'nextPath'}
|
||||
value={props.paths.accountHome}
|
||||
/>
|
||||
|
||||
<InvitationSubmitButton accountName={props.invitation.account.name} />
|
||||
</form>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SignOutInvitationButton nextPath={props.paths.signOutNext} />
|
||||
|
||||
<span className={'text-muted-foreground text-center text-xs'}>
|
||||
<Trans i18nKey={'teams:signInWithDifferentAccountDescription'} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export function InvitationSubmitButton(props: { accountName: string }) {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button className={'w-full'} disabled={pending}>
|
||||
<Trans
|
||||
i18nKey={pending ? 'teams:joiningTeam' : 'teams:joinTeam'}
|
||||
values={{
|
||||
accountName: props.accountName,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export function SignOutInvitationButton(
|
||||
props: React.PropsWithChildren<{
|
||||
nextPath: string;
|
||||
}>,
|
||||
) {
|
||||
const signOut = useSignOut();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={async () => {
|
||||
await signOut.mutateAsync();
|
||||
window.location.assign(props.nextPath);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey={'teams:signInWithDifferentAccount'} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,9 @@ type Role = Database['public']['Enums']['account_role'];
|
||||
const roleClassNameBuilder = cva('font-medium capitalize', {
|
||||
variants: {
|
||||
role: {
|
||||
owner: 'bg-primary',
|
||||
member: 'bg-blue-50 text-blue-500 dark:bg-blue-500/10',
|
||||
owner: '',
|
||||
member:
|
||||
'bg-blue-50 hover:bg-blue-50 text-blue-500 dark:bg-blue-500/10 dark:hover:bg-blue-500/10',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,14 +6,16 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@kit/ui/dialog';
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -38,17 +40,17 @@ export const TransferOwnershipDialog: React.FC<{
|
||||
targetDisplayName: string;
|
||||
}> = ({ isOpen, setIsOpen, targetDisplayName, accountId, userId }) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey="team:transferOwnership" />
|
||||
</DialogTitle>
|
||||
</AlertDialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey="team:transferOwnershipDescription" />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<TransferOrganizationOwnershipForm
|
||||
accountId={accountId}
|
||||
@@ -56,8 +58,8 @@ export const TransferOwnershipDialog: React.FC<{
|
||||
targetDisplayName={targetDisplayName}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,7 +102,7 @@ function TransferOrganizationOwnershipForm({
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={'flex flex-col space-y-2 text-sm'}
|
||||
className={'flex flex-col space-y-4 text-sm'}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<If condition={error}>
|
||||
@@ -117,10 +119,6 @@ function TransferOrganizationOwnershipForm({
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</p>
|
||||
|
||||
<FormField
|
||||
name={'confirmation'}
|
||||
render={({ field }) => {
|
||||
@@ -144,19 +142,31 @@ function TransferOrganizationOwnershipForm({
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type={'submit'}
|
||||
data-test={'confirm-transfer-ownership-button'}
|
||||
variant={'destructive'}
|
||||
disabled={pending}
|
||||
>
|
||||
<If
|
||||
condition={pending}
|
||||
fallback={<Trans i18nKey={'teams:transferOwnership'} />}
|
||||
<div>
|
||||
<p className={'text-muted-foreground'}>
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
type={'submit'}
|
||||
data-test={'confirm-transfer-ownership-button'}
|
||||
variant={'destructive'}
|
||||
disabled={pending}
|
||||
>
|
||||
<Trans i18nKey={'teams:transferringOwnership'} />
|
||||
</If>
|
||||
</Button>
|
||||
<If
|
||||
condition={pending}
|
||||
fallback={<Trans i18nKey={'teams:transferOwnership'} />}
|
||||
>
|
||||
<Trans i18nKey={'teams:transferringOwnership'} />
|
||||
</If>
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -124,7 +124,7 @@ function UpdateMemberForm({
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>{t('memberRole')}</FormLabel>
|
||||
<FormLabel>{t('roleLabel')}</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<MembershipRoleSelector
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const AcceptInvitationSchema = z.object({
|
||||
inviteToken: z.string().uuid(),
|
||||
nextPath: z.string().min(1),
|
||||
});
|
||||
@@ -1,14 +1,17 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { requireAuth } from '@kit/supabase/require-auth';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import { AcceptInvitationSchema } from '../../schema/accept-invitation.schema';
|
||||
import { DeleteInvitationSchema } from '../../schema/delete-invitation.schema';
|
||||
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||
import { UpdateInvitationSchema } from '../../schema/update-invitation-schema';
|
||||
@@ -80,10 +83,32 @@ export async function updateInvitationAction(
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function assertSession(client: SupabaseClient<Database>) {
|
||||
const { data, error } = await client.auth.getUser();
|
||||
export async function acceptInvitationAction(data: FormData) {
|
||||
const client = getSupabaseServerActionClient();
|
||||
|
||||
if (error ?? !data.user) {
|
||||
const { inviteToken, nextPath } = AcceptInvitationSchema.parse(
|
||||
Object.fromEntries(data),
|
||||
);
|
||||
|
||||
const { user } = await assertSession(client);
|
||||
|
||||
const service = new AccountInvitationsService(client);
|
||||
|
||||
await service.acceptInvitationToTeam({
|
||||
adminClient: getSupabaseServerActionClient({ admin: true }),
|
||||
inviteToken,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return redirect(nextPath);
|
||||
}
|
||||
|
||||
async function assertSession(client: SupabaseClient<Database>) {
|
||||
const { error, data } = await requireAuth(client);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Authentication required`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from 'zod';
|
||||
import { Mailer } from '@kit/mailers';
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { requireAuth } from '@kit/supabase/require-auth';
|
||||
|
||||
import { DeleteInvitationSchema } from '../../schema/delete-invitation.schema';
|
||||
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||
@@ -206,8 +207,28 @@ export class AccountInvitationsService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts an invitation to join a team.
|
||||
*/
|
||||
async acceptInvitationToTeam(params: {
|
||||
userId: string;
|
||||
inviteToken: string;
|
||||
adminClient: SupabaseClient<Database>;
|
||||
}) {
|
||||
const { error, data } = await params.adminClient.rpc('accept_invitation', {
|
||||
token: params.inviteToken,
|
||||
user_id: params.userId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private async getUser() {
|
||||
const { data, error } = await this.client.auth.getUser();
|
||||
const { data, error } = await requireAuth(this.client);
|
||||
|
||||
if (error ?? !data) {
|
||||
throw new Error('Authentication required');
|
||||
@@ -217,6 +238,6 @@ export class AccountInvitationsService {
|
||||
}
|
||||
|
||||
private getInvitationLink(token: string) {
|
||||
return new URL(env.invitePath, env.siteURL).href + `?token=${token}`;
|
||||
return new URL(env.siteURL, env.siteURL).href + `?invite_token=${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -119,7 +119,8 @@
|
||||
"./auth-change-listener": "./src/makerkit/auth-change-listener.tsx",
|
||||
"./loading-overlay": "./src/makerkit/loading-overlay.tsx",
|
||||
"./profile-avatar": "./src/makerkit/profile-avatar.tsx",
|
||||
"./mdx": "./src/makerkit/mdx/mdx-renderer.tsx"
|
||||
"./mdx": "./src/makerkit/mdx/mdx-renderer.tsx",
|
||||
"./mode-toggle": "./src/makerkit/mode-toggle.tsx"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
|
||||
99
packages/ui/src/makerkit/mode-toggle.tsx
Normal file
99
packages/ui/src/makerkit/mode-toggle.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Check, Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import { Button } from '../shadcn/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '../shadcn/dropdown-menu';
|
||||
import { If } from './if';
|
||||
import { Trans } from './trans';
|
||||
|
||||
const MODES = ['light', 'dark', 'system'];
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const Items = useMemo(() => {
|
||||
return MODES.map((mode) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={mode}
|
||||
onClick={() => {
|
||||
setTheme(mode);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey={`common:${mode}Theme`} />
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
});
|
||||
}, [setTheme]);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">{Items}</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubMenuModeToggle() {
|
||||
const { setTheme, theme, resolvedTheme } = useTheme();
|
||||
|
||||
const MenuItems = useMemo(
|
||||
() =>
|
||||
['light', 'dark', 'system'].map((item) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={'justify-between'}
|
||||
key={item}
|
||||
onClick={() => {
|
||||
setTheme(item);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey={`common:${item}Theme`} />
|
||||
|
||||
<If condition={theme === item}>
|
||||
<Check className={'mr-2 h-4'} />
|
||||
</If>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}),
|
||||
[setTheme, theme],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<span className={'flex w-full items-center space-x-2'}>
|
||||
{resolvedTheme === 'light' ? (
|
||||
<Sun className={'h-5'} />
|
||||
) : (
|
||||
<Moon className={'h-5'} />
|
||||
)}
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'common:theme'} />
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
|
||||
<DropdownMenuSubContent>{MenuItems}</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
import { cn } from '../utils';
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
|
||||
Reference in New Issue
Block a user