mirror of
https://github.com/graphql-hive/console
synced 2026-05-23 17:18:23 +00:00
Hint recent authentication method (#5255)
This commit is contained in:
parent
95304fb338
commit
214a795eb4
6 changed files with 292 additions and 182 deletions
31
packages/web/app/src/lib/supertokens/last-auth-method.ts
Normal file
31
packages/web/app/src/lib/supertokens/last-auth-method.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { useCallback, useMemo, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const LAST_USED_AUTH_METHOD_KEY = 'hive:last-used-auth-method';
|
||||
|
||||
type AuthProvider = 'github' | 'google' | 'email' | 'okta' | 'oidc';
|
||||
|
||||
export function updateLastAuthMethod(provider: AuthProvider) {
|
||||
Cookies.set(LAST_USED_AUTH_METHOD_KEY, provider);
|
||||
}
|
||||
|
||||
export function useLastAuthMethod() {
|
||||
const [authProvider, setAuthProvider] = useState<AuthProvider | null>(
|
||||
(Cookies.get(LAST_USED_AUTH_METHOD_KEY) as AuthProvider) ?? null,
|
||||
);
|
||||
|
||||
const updateAuthProvider = useCallback(
|
||||
(provider: AuthProvider) => {
|
||||
setAuthProvider(provider);
|
||||
Cookies.set(LAST_USED_AUTH_METHOD_KEY, provider);
|
||||
},
|
||||
[setAuthProvider],
|
||||
);
|
||||
|
||||
const api = useMemo(
|
||||
() => [authProvider, updateAuthProvider] as const,
|
||||
[authProvider, updateAuthProvider],
|
||||
);
|
||||
|
||||
return api;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { getAuthorisationURLWithQueryParamsAndSetState } from 'supertokens-auth-react/recipe/thirdpartyemailpassword';
|
||||
import { env } from '@/env/frontend';
|
||||
import { updateLastAuthMethod } from './last-auth-method';
|
||||
|
||||
/**
|
||||
* utility for starting the login flow manually without clicking a button
|
||||
|
|
@ -25,5 +26,7 @@ export const startAuthFlowForProvider = async (
|
|||
frontendRedirectURI: `${env.appBaseUrl}/auth/callback/${thirdPartyId}${redirectPart}`,
|
||||
});
|
||||
|
||||
updateLastAuthMethod(thirdPartyId);
|
||||
|
||||
window.location.assign(authUrl);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { UserInput } from 'supertokens-auth-react/lib/build/recipe/thirdpartyemailpassword/types';
|
||||
import { getAuthorisationURLWithQueryParamsAndSetState } from 'supertokens-auth-react/recipe/thirdpartyemailpassword';
|
||||
import { env } from '@/env/frontend';
|
||||
import { updateLastAuthMethod } from './last-auth-method';
|
||||
|
||||
export const createThirdPartyEmailPasswordReactOIDCProvider = () => ({
|
||||
id: 'oidc',
|
||||
|
|
@ -79,6 +80,8 @@ export const startAuthFlowForOIDCProvider = async (oidcId: string) => {
|
|||
},
|
||||
});
|
||||
|
||||
updateLastAuthMethod('oidc');
|
||||
|
||||
// Redirects to the OIDC provider
|
||||
window.location.assign(authUrl);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,14 +23,48 @@ import {
|
|||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Meta } from '@/components/ui/meta';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { useLastAuthMethod } from '@/lib/supertokens/last-auth-method';
|
||||
import { startAuthFlowForProvider } from '@/lib/supertokens/start-auth-flow-for-provider';
|
||||
import { enabledProviders, isProviderEnabled } from '@/lib/supertokens/thirdparty';
|
||||
import { exhaustiveGuard } from '@/lib/utils';
|
||||
import { cn, exhaustiveGuard } from '@/lib/utils';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Link, Navigate, useRouter } from '@tanstack/react-router';
|
||||
|
||||
export function SignInButton(props: {
|
||||
children: React.ReactNode;
|
||||
previousSignIn: boolean;
|
||||
variant?: 'outline' | 'default';
|
||||
tooltipClassName?: string;
|
||||
}) {
|
||||
if (props.previousSignIn) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Slot
|
||||
className={cn(
|
||||
'animate-shimmer bg-[length:200%_100%] transition-colors',
|
||||
props.variant === 'outline'
|
||||
? 'bg-[linear-gradient(110deg,transparent,48%,#202020,52%,transparent)]'
|
||||
: 'bg-[linear-gradient(110deg,transparent,30%,#a9a9a9,70%,transparent)]',
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</Slot>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={cn('text-muted bg-white', props.tooltipClassName)} side="top">
|
||||
You signed in with it last time.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
const SignInFormSchema = z.object({
|
||||
email: z
|
||||
.string({
|
||||
|
|
@ -44,6 +78,7 @@ type SignInFormValues = z.infer<typeof SignInFormSchema>;
|
|||
|
||||
export function AuthSignInPage(props: { redirectToPath: string }) {
|
||||
const session = useSessionContext();
|
||||
const [lastAuthMethod, setLastAuthMethod] = useLastAuthMethod();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const emailPasswordSignIn = useMutation({
|
||||
|
|
@ -53,6 +88,7 @@ export function AuthSignInPage(props: { redirectToPath: string }) {
|
|||
|
||||
switch (status) {
|
||||
case 'OK': {
|
||||
setLastAuthMethod('email');
|
||||
void router.navigate({
|
||||
to: props.redirectToPath,
|
||||
});
|
||||
|
|
@ -157,99 +193,112 @@ export function AuthSignInPage(props: { redirectToPath: string }) {
|
|||
<AuthCardHeader title="Login" description="Sign in to your account" />
|
||||
<AuthCardContent>
|
||||
<AuthCardStack>
|
||||
<Form {...form}>
|
||||
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<Link
|
||||
tabIndex={-1}
|
||||
to="/auth/reset-password"
|
||||
search={{
|
||||
email: form.getValues().email || undefined,
|
||||
redirectToPath: props.redirectToPath,
|
||||
}}
|
||||
className="ml-auto inline-block text-sm underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{emailPasswordSignIn.data?.status === 'OK'
|
||||
? 'Redirecting...'
|
||||
: emailPasswordSignIn.isPending
|
||||
? 'Signing in...'
|
||||
: 'Sign in'}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{enabledProviders.length ? <AuthOrSeparator /> : null}
|
||||
{isProviderEnabled('google') ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('google')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiGoogle className="mr-4 size-4" /> Login with Google
|
||||
</Button>
|
||||
) : null}
|
||||
{isProviderEnabled('github') ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('github')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiGithub className="mr-4 size-4" /> Login with Github
|
||||
</Button>
|
||||
) : null}
|
||||
{isProviderEnabled('okta') ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('okta')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiOkta className="mr-4 size-4" /> Login with Okta
|
||||
</Button>
|
||||
) : null}
|
||||
{isProviderEnabled('oidc') ? (
|
||||
<Button asChild variant="outline" className="w-full" disabled={isPending}>
|
||||
<Link
|
||||
to="/auth/sso"
|
||||
search={{
|
||||
redirectToPath: props.redirectToPath,
|
||||
}}
|
||||
>
|
||||
<FaRegUserCircle className="mr-4 size-4" /> Login with SSO
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Form {...form}>
|
||||
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<Link
|
||||
tabIndex={-1}
|
||||
to="/auth/reset-password"
|
||||
search={{
|
||||
email: form.getValues().email || undefined,
|
||||
redirectToPath: props.redirectToPath,
|
||||
}}
|
||||
className="ml-auto inline-block text-sm underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SignInButton previousSignIn={lastAuthMethod === 'email'}>
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{emailPasswordSignIn.data?.status === 'OK'
|
||||
? 'Redirecting...'
|
||||
: emailPasswordSignIn.isPending
|
||||
? 'Signing in...'
|
||||
: 'Sign in'}
|
||||
</Button>
|
||||
</SignInButton>
|
||||
</form>
|
||||
</Form>
|
||||
{enabledProviders.length ? <AuthOrSeparator /> : null}
|
||||
{isProviderEnabled('google') ? (
|
||||
<SignInButton variant="outline" previousSignIn={lastAuthMethod === 'google'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('google')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiGoogle className="mr-4 size-4" /> Login with Google
|
||||
</Button>
|
||||
</SignInButton>
|
||||
) : null}
|
||||
{isProviderEnabled('github') ? (
|
||||
<SignInButton variant="outline" previousSignIn={lastAuthMethod === 'github'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('github')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiGithub className="mr-4 size-4" /> Login with Github
|
||||
</Button>
|
||||
</SignInButton>
|
||||
) : null}
|
||||
|
||||
{isProviderEnabled('okta') ? (
|
||||
<SignInButton variant="outline" previousSignIn={lastAuthMethod === 'okta'}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('okta')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiOkta className="mr-4 size-4" /> Login with Okta
|
||||
</Button>
|
||||
</SignInButton>
|
||||
) : null}
|
||||
{isProviderEnabled('oidc') ? (
|
||||
<SignInButton variant="outline" previousSignIn={lastAuthMethod === 'oidc'}>
|
||||
<Button asChild variant="outline" className="w-full" disabled={isPending}>
|
||||
<Link
|
||||
to="/auth/sso"
|
||||
search={{
|
||||
redirectToPath: props.redirectToPath,
|
||||
}}
|
||||
>
|
||||
<FaRegUserCircle className="mr-4 size-4" /> Login with SSO
|
||||
</Link>
|
||||
</Button>
|
||||
</SignInButton>
|
||||
) : null}
|
||||
</TooltipProvider>
|
||||
</AuthCardStack>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{' '}
|
||||
|
|
|
|||
|
|
@ -24,14 +24,17 @@ import {
|
|||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Meta } from '@/components/ui/meta';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { env } from '@/env/frontend';
|
||||
import { useLastAuthMethod } from '@/lib/supertokens/last-auth-method';
|
||||
import { startAuthFlowForProvider } from '@/lib/supertokens/start-auth-flow-for-provider';
|
||||
import { enabledProviders, isProviderEnabled } from '@/lib/supertokens/thirdparty';
|
||||
import { exhaustiveGuard } from '@/lib/utils';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Link, Navigate, useRouter } from '@tanstack/react-router';
|
||||
import { SignInButton } from './auth-sign-in';
|
||||
|
||||
const SignUpFormSchema = z.object({
|
||||
firstName: z.string({
|
||||
|
|
@ -51,6 +54,7 @@ const SignUpFormSchema = z.object({
|
|||
type SignUpFormValues = z.infer<typeof SignUpFormSchema>;
|
||||
|
||||
export function AuthSignUpPage(props: { redirectToPath: string }) {
|
||||
const [lastAuthMethod] = useLastAuthMethod();
|
||||
const router = useRouter();
|
||||
const session = useSessionContext();
|
||||
|
||||
|
|
@ -199,17 +203,45 @@ export function AuthSignUpPage(props: { redirectToPath: string }) {
|
|||
/>
|
||||
<AuthCardContent>
|
||||
<AuthCardStack>
|
||||
<Form {...form}>
|
||||
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Form {...form}>
|
||||
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>First name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Max" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Robinson" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>First name</FormLabel>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Max" {...field} />
|
||||
<Input placeholder="m@example.com" type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -217,96 +249,78 @@ export function AuthSignUpPage(props: { redirectToPath: string }) {
|
|||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last name</FormLabel>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Robinson" {...field} />
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{signUp.isSuccess && signUp.data.status === 'OK' && isVerificationSettled
|
||||
? 'Redirecting...'
|
||||
: signUp.isPending
|
||||
? 'Creating account...'
|
||||
: 'Create an account'}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{enabledProviders.length ? <AuthOrSeparator /> : null}
|
||||
{isProviderEnabled('google') ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('google')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiGoogle className="mr-4 size-4" /> Sign up with Google
|
||||
</Button>
|
||||
) : null}
|
||||
{isProviderEnabled('github') ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('github')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiGithub className="mr-4 size-4" /> Sign up with Github
|
||||
</Button>
|
||||
) : null}
|
||||
{isProviderEnabled('okta') ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('okta')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiOkta className="mr-4 size-4" /> Sign up with Okta
|
||||
</Button>
|
||||
) : null}
|
||||
{isProviderEnabled('oidc') ? (
|
||||
<Button asChild variant="outline" className="w-full" disabled={isPending}>
|
||||
<Link
|
||||
to="/auth/sso"
|
||||
search={{
|
||||
redirectToPath: props.redirectToPath,
|
||||
}}
|
||||
>
|
||||
<FaRegUserCircle className="mr-4 size-4" /> Sign up with SSO
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{signUp.isSuccess && signUp.data.status === 'OK' && isVerificationSettled
|
||||
? 'Redirecting...'
|
||||
: signUp.isPending
|
||||
? 'Creating account...'
|
||||
: 'Create an account'}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{enabledProviders.length ? <AuthOrSeparator /> : null}
|
||||
{isProviderEnabled('google') ? (
|
||||
<SignInButton previousSignIn={lastAuthMethod === 'google'} variant="outline">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('google')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiGoogle className="mr-4 size-4" /> Sign up with Google
|
||||
</Button>
|
||||
</SignInButton>
|
||||
) : null}
|
||||
{isProviderEnabled('github') ? (
|
||||
<SignInButton previousSignIn={lastAuthMethod === 'github'} variant="outline">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('github')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiGithub className="mr-4 size-4" /> Sign up with Github
|
||||
</Button>
|
||||
</SignInButton>
|
||||
) : null}
|
||||
{isProviderEnabled('okta') ? (
|
||||
<SignInButton previousSignIn={lastAuthMethod === 'okta'} variant="outline">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => thirdPartySignIn.mutate('okta')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SiOkta className="mr-4 size-4" /> Sign up with Okta
|
||||
</Button>
|
||||
</SignInButton>
|
||||
) : null}
|
||||
{isProviderEnabled('oidc') ? (
|
||||
<SignInButton previousSignIn={lastAuthMethod === 'oidc'} variant="outline">
|
||||
<Button asChild variant="outline" className="w-full" disabled={isPending}>
|
||||
<Link
|
||||
to="/auth/sso"
|
||||
search={{
|
||||
redirectToPath: props.redirectToPath,
|
||||
}}
|
||||
>
|
||||
<FaRegUserCircle className="mr-4 size-4" /> Sign up with SSO
|
||||
</Link>
|
||||
</Button>
|
||||
</SignInButton>
|
||||
) : null}
|
||||
</TooltipProvider>
|
||||
</AuthCardStack>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
|
|
|
|||
|
|
@ -227,6 +227,14 @@ module.exports = {
|
|||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: 0 },
|
||||
},
|
||||
shimmer: {
|
||||
from: {
|
||||
backgroundPosition: '0 0',
|
||||
},
|
||||
to: {
|
||||
backgroundPosition: '-200% 0',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
// Dropdown menu
|
||||
|
|
@ -255,6 +263,8 @@ module.exports = {
|
|||
'toast-swipe-out-y': 'toast-swipe-out-y 100ms ease-out forwards',
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
//
|
||||
shimmer: 'shimmer 1.5s linear infinite',
|
||||
},
|
||||
minHeight: {
|
||||
content: 'var(--content-height)',
|
||||
|
|
|
|||
Loading…
Reference in a new issue