@cogito.ai/cli 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/dist/templates/web-nextjs/.env.example +4 -0
- package/dist/templates/web-nextjs/.vscode/settings.json +3 -0
- package/dist/templates/web-nextjs/README.md +25 -1
- package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +1 -1
- package/dist/templates/web-nextjs/apps/docs/.source/server.ts +4 -3
- package/dist/templates/web-nextjs/apps/docs/content/docs/features/auth.mdx +139 -0
- package/dist/templates/web-nextjs/apps/web/components.json +25 -0
- package/dist/templates/web-nextjs/apps/web/messages/en.json +28 -0
- package/dist/templates/web-nextjs/apps/web/messages/zh.json +28 -0
- package/dist/templates/web-nextjs/apps/web/middleware.ts +53 -9
- package/dist/templates/web-nextjs/apps/web/package.json +13 -1
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/login/page.tsx +142 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/signup/page.tsx +151 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/dashboard/page.tsx +42 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/layout.tsx +22 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/globals.css +129 -3
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/layout.tsx +2 -4
- package/dist/templates/web-nextjs/apps/web/src/app/auth/callback/route.ts +21 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/alert.tsx +76 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/button.tsx +58 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/form.tsx +154 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/input.tsx +20 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/label.tsx +20 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sonner.tsx +50 -0
- package/dist/templates/web-nextjs/apps/web/src/core/repositories/IAuthRepository.ts +9 -0
- package/dist/templates/web-nextjs/apps/web/src/core/types/auth.ts +14 -0
- package/dist/templates/web-nextjs/apps/web/src/features/auth/__contract__.ts +14 -0
- package/dist/templates/web-nextjs/apps/web/src/features/auth/actions.ts +86 -0
- package/dist/templates/web-nextjs/apps/web/src/features/auth/index.ts +1 -0
- package/dist/templates/web-nextjs/apps/web/src/i18n/config.ts +12 -0
- package/dist/templates/web-nextjs/apps/web/src/i18n/request.ts +3 -1
- package/dist/templates/web-nextjs/apps/web/src/infra/db/SupabaseAuthRepository.ts +63 -0
- package/dist/templates/web-nextjs/apps/web/src/infra/db/client.ts +38 -0
- package/dist/templates/web-nextjs/apps/web/src/infra/providers.ts +6 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/utils.ts +6 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/validations/auth.ts +20 -0
- package/dist/templates/web-nextjs/apps/web/src/styles/shadcn-tailwind.css +95 -0
- package/dist/templates/web-nextjs/apps/web/src/styles/tw-animate.css +1 -0
- package/dist/templates/web-nextjs/pnpm-lock.yaml +2327 -17
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
Controller,
|
|
6
|
+
type ControllerProps,
|
|
7
|
+
type FieldPath,
|
|
8
|
+
type FieldValues,
|
|
9
|
+
FormProvider,
|
|
10
|
+
useFormContext,
|
|
11
|
+
type UseFormReturn,
|
|
12
|
+
} from "react-hook-form"
|
|
13
|
+
|
|
14
|
+
import { cn } from "@/lib/utils"
|
|
15
|
+
import { Label } from "@/components/ui/label"
|
|
16
|
+
|
|
17
|
+
const Form = FormProvider
|
|
18
|
+
|
|
19
|
+
type FormFieldContextValue<
|
|
20
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
21
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
22
|
+
> = {
|
|
23
|
+
name: TName
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
|
27
|
+
{} as FormFieldContextValue,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
function FormField<
|
|
31
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
32
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
33
|
+
>({ ...props }: ControllerProps<TFieldValues, TName>) {
|
|
34
|
+
return (
|
|
35
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
36
|
+
<Controller {...props} />
|
|
37
|
+
</FormFieldContext.Provider>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function useFormField() {
|
|
42
|
+
const fieldContext = React.useContext(FormFieldContext)
|
|
43
|
+
const itemContext = React.useContext(FormItemContext)
|
|
44
|
+
const { getFieldState, formState } = useFormContext()
|
|
45
|
+
|
|
46
|
+
const fieldState = getFieldState(fieldContext.name, formState)
|
|
47
|
+
|
|
48
|
+
if (!fieldContext) {
|
|
49
|
+
throw new Error("useFormField should be used within <FormField>")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { id } = itemContext
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
name: fieldContext.name,
|
|
57
|
+
formItemId: `${id}-form-item`,
|
|
58
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
59
|
+
formMessageId: `${id}-form-item-message`,
|
|
60
|
+
...fieldState,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type FormItemContextValue = {
|
|
65
|
+
id: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
|
69
|
+
{} as FormItemContextValue,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
|
+
const id = React.useId()
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<FormItemContext.Provider value={{ id }}>
|
|
77
|
+
<div className={cn("grid gap-2", className)} {...props} />
|
|
78
|
+
</FormItemContext.Provider>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function FormLabel({
|
|
83
|
+
className,
|
|
84
|
+
...props
|
|
85
|
+
}: React.ComponentProps<typeof Label>) {
|
|
86
|
+
const { error, formItemId } = useFormField()
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Label
|
|
90
|
+
className={cn(error && "text-destructive", className)}
|
|
91
|
+
htmlFor={formItemId}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function FormControl({ ...props }: React.ComponentProps<"div">) {
|
|
98
|
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
id={formItemId}
|
|
103
|
+
aria-describedby={
|
|
104
|
+
!error
|
|
105
|
+
? `${formDescriptionId}`
|
|
106
|
+
: `${formDescriptionId} ${formMessageId}`
|
|
107
|
+
}
|
|
108
|
+
aria-invalid={!!error}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
115
|
+
const { formDescriptionId } = useFormField()
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<p
|
|
119
|
+
id={formDescriptionId}
|
|
120
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function FormMessage({ className, children, ...props }: React.ComponentProps<"p">) {
|
|
127
|
+
const { error, formMessageId } = useFormField()
|
|
128
|
+
const body = error ? String(error?.message ?? "") : children
|
|
129
|
+
|
|
130
|
+
if (!body) {
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<p
|
|
136
|
+
id={formMessageId}
|
|
137
|
+
className={cn("text-sm font-medium text-destructive", className)}
|
|
138
|
+
{...props}
|
|
139
|
+
>
|
|
140
|
+
{body}
|
|
141
|
+
</p>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
useFormField,
|
|
147
|
+
Form,
|
|
148
|
+
FormItem,
|
|
149
|
+
FormLabel,
|
|
150
|
+
FormControl,
|
|
151
|
+
FormDescription,
|
|
152
|
+
FormMessage,
|
|
153
|
+
FormField,
|
|
154
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Input as InputPrimitive } from "@base-ui/react/input"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
7
|
+
return (
|
|
8
|
+
<InputPrimitive
|
|
9
|
+
type={type}
|
|
10
|
+
data-slot="input"
|
|
11
|
+
className={cn(
|
|
12
|
+
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Input }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
|
8
|
+
return (
|
|
9
|
+
<label
|
|
10
|
+
data-slot="label"
|
|
11
|
+
className={cn(
|
|
12
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Label }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useTheme } from "next-themes"
|
|
4
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
|
5
|
+
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
8
|
+
const { resolvedTheme } = useTheme()
|
|
9
|
+
const theme: ToasterProps['theme'] = resolvedTheme === 'dark' ? 'dark' : resolvedTheme === 'light' ? 'light' : 'system'
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Sonner
|
|
13
|
+
theme={theme}
|
|
14
|
+
className="toaster group"
|
|
15
|
+
icons={{
|
|
16
|
+
success: (
|
|
17
|
+
<CircleCheckIcon className="size-4" />
|
|
18
|
+
),
|
|
19
|
+
info: (
|
|
20
|
+
<InfoIcon className="size-4" />
|
|
21
|
+
),
|
|
22
|
+
warning: (
|
|
23
|
+
<TriangleAlertIcon className="size-4" />
|
|
24
|
+
),
|
|
25
|
+
error: (
|
|
26
|
+
<OctagonXIcon className="size-4" />
|
|
27
|
+
),
|
|
28
|
+
loading: (
|
|
29
|
+
<Loader2Icon className="size-4 animate-spin" />
|
|
30
|
+
),
|
|
31
|
+
}}
|
|
32
|
+
style={
|
|
33
|
+
{
|
|
34
|
+
"--normal-bg": "var(--popover)",
|
|
35
|
+
"--normal-text": "var(--popover-foreground)",
|
|
36
|
+
"--normal-border": "var(--border)",
|
|
37
|
+
"--border-radius": "var(--radius)",
|
|
38
|
+
} as React.CSSProperties
|
|
39
|
+
}
|
|
40
|
+
toastOptions={{
|
|
41
|
+
classNames: {
|
|
42
|
+
toast: "cn-toast",
|
|
43
|
+
},
|
|
44
|
+
}}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { Toaster }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AuthResult, AuthUser } from '@/core/types/auth'
|
|
2
|
+
|
|
3
|
+
export interface IAuthRepository {
|
|
4
|
+
signInWithPassword(email: string, password: string): Promise<AuthResult>
|
|
5
|
+
signUp(email: string, password: string): Promise<AuthResult>
|
|
6
|
+
signOut(): Promise<void>
|
|
7
|
+
getSession(): Promise<AuthUser | null>
|
|
8
|
+
signInWithOAuth(provider: 'github', nextPath?: string): Promise<{ url: string }>
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AuthUser = {
|
|
2
|
+
id: string
|
|
3
|
+
email: string
|
|
4
|
+
createdAt: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type AuthResult = {
|
|
8
|
+
user: AuthUser | null
|
|
9
|
+
error: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ActionResult<T = void> =
|
|
13
|
+
| { data: T; error: null }
|
|
14
|
+
| { data: null; error: string }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ActionResult } from '@/core/types/auth'
|
|
2
|
+
|
|
3
|
+
export type SignInArgs = { email: string; password: string }
|
|
4
|
+
export type SignUpArgs = { email: string; password: string; confirmPassword: string }
|
|
5
|
+
export type SignUpSuccessData = { success: true; email: string }
|
|
6
|
+
export type OAuthData = { url: string }
|
|
7
|
+
|
|
8
|
+
export interface AuthFeatureContract {
|
|
9
|
+
signIn(args: SignInArgs): Promise<ActionResult>
|
|
10
|
+
signUp(args: SignUpArgs): Promise<ActionResult<SignUpSuccessData>>
|
|
11
|
+
signOut(): Promise<void>
|
|
12
|
+
signInWithGithub(): Promise<ActionResult<OAuthData>>
|
|
13
|
+
signInWithGithubForLocale(locale: string): Promise<ActionResult<OAuthData>>
|
|
14
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { redirect } from 'next/navigation'
|
|
4
|
+
import { getAuthRepository } from '@/infra/providers'
|
|
5
|
+
import { signInSchema, signUpSchema } from '@/lib/validations/auth'
|
|
6
|
+
import type { ActionResult } from '@/core/types/auth'
|
|
7
|
+
import { defaultLocale, isLocale } from '@/i18n/config'
|
|
8
|
+
import type { SignUpSuccessData, OAuthData } from './__contract__'
|
|
9
|
+
|
|
10
|
+
function normalizeLocale(input: FormDataEntryValue | null): string {
|
|
11
|
+
const locale = typeof input === 'string' ? input : defaultLocale
|
|
12
|
+
return isLocale(locale) ? locale : defaultLocale
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function signIn(
|
|
16
|
+
_prevState: ActionResult | null,
|
|
17
|
+
formData: FormData,
|
|
18
|
+
): Promise<ActionResult> {
|
|
19
|
+
const parsed = signInSchema.safeParse({
|
|
20
|
+
email: formData.get('email'),
|
|
21
|
+
password: formData.get('password'),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (!parsed.success) {
|
|
25
|
+
return { data: null, error: parsed.error.issues[0]?.message ?? '输入无效' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const repo = getAuthRepository()
|
|
29
|
+
const result = await repo.signInWithPassword(parsed.data.email, parsed.data.password)
|
|
30
|
+
|
|
31
|
+
if (result.error) {
|
|
32
|
+
return { data: null, error: result.error }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const locale = normalizeLocale(formData.get('locale'))
|
|
36
|
+
redirect(`/${locale}/dashboard`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function signUp(
|
|
40
|
+
_prevState: ActionResult<SignUpSuccessData> | null,
|
|
41
|
+
formData: FormData,
|
|
42
|
+
): Promise<ActionResult<SignUpSuccessData>> {
|
|
43
|
+
const parsed = signUpSchema.safeParse({
|
|
44
|
+
email: formData.get('email'),
|
|
45
|
+
password: formData.get('password'),
|
|
46
|
+
confirmPassword: formData.get('confirmPassword'),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
return { data: null, error: parsed.error.issues[0]?.message ?? '输入无效' }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const repo = getAuthRepository()
|
|
54
|
+
const result = await repo.signUp(parsed.data.email, parsed.data.password)
|
|
55
|
+
|
|
56
|
+
if (result.error) {
|
|
57
|
+
return { data: null, error: result.error }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { data: { success: true, email: parsed.data.email }, error: null }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function signOut(formData: FormData): Promise<void> {
|
|
64
|
+
const repo = getAuthRepository()
|
|
65
|
+
await repo.signOut()
|
|
66
|
+
const locale = normalizeLocale(formData.get('locale'))
|
|
67
|
+
redirect(`/${locale}/login`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function signInWithGithub(): Promise<ActionResult<OAuthData>> {
|
|
71
|
+
return signInWithGithubForLocale(defaultLocale)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function signInWithGithubForLocale(
|
|
75
|
+
localeInput: string,
|
|
76
|
+
): Promise<ActionResult<OAuthData>> {
|
|
77
|
+
const repo = getAuthRepository()
|
|
78
|
+
const locale = isLocale(localeInput) ? localeInput : defaultLocale
|
|
79
|
+
try {
|
|
80
|
+
const { url } = await repo.signInWithOAuth('github', `/${locale}/dashboard`)
|
|
81
|
+
return { data: { url }, error: null }
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : 'OAuth 初始化失败'
|
|
84
|
+
return { data: null, error: msg }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { signIn, signUp, signOut, signInWithGithub, signInWithGithubForLocale } from './actions'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const locales = ['en', 'zh'] as const
|
|
2
|
+
|
|
3
|
+
export type AppLocale = (typeof locales)[number]
|
|
4
|
+
|
|
5
|
+
const envDefaultLocale = process.env.APP_DEFAULT_LOCALE
|
|
6
|
+
|
|
7
|
+
export const defaultLocale: AppLocale =
|
|
8
|
+
envDefaultLocale === 'en' || envDefaultLocale === 'zh' ? envDefaultLocale : 'zh'
|
|
9
|
+
|
|
10
|
+
export function isLocale(value: string): value is AppLocale {
|
|
11
|
+
return locales.includes(value as AppLocale)
|
|
12
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getRequestConfig } from 'next-intl/server'
|
|
2
|
+
import { defaultLocale, isLocale } from './config'
|
|
2
3
|
|
|
3
4
|
export default getRequestConfig(async ({ requestLocale }) => {
|
|
4
|
-
const
|
|
5
|
+
const candidateLocale = await requestLocale
|
|
6
|
+
const locale = candidateLocale && isLocale(candidateLocale) ? candidateLocale : defaultLocale
|
|
5
7
|
|
|
6
8
|
return {
|
|
7
9
|
locale,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { IAuthRepository } from '@/core/repositories/IAuthRepository'
|
|
2
|
+
import type { AuthResult, AuthUser } from '@/core/types/auth'
|
|
3
|
+
import { getServerClient } from '@/infra/db/client'
|
|
4
|
+
|
|
5
|
+
export class SupabaseAuthRepository implements IAuthRepository {
|
|
6
|
+
async signInWithPassword(email: string, password: string): Promise<AuthResult> {
|
|
7
|
+
const supabase = await getServerClient()
|
|
8
|
+
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
|
|
9
|
+
if (error) {
|
|
10
|
+
return { user: null, error: error.message }
|
|
11
|
+
}
|
|
12
|
+
const u = data.user
|
|
13
|
+
return {
|
|
14
|
+
user: u ? { id: u.id, email: u.email ?? '', createdAt: u.created_at } : null,
|
|
15
|
+
error: null,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async signUp(email: string, password: string): Promise<AuthResult> {
|
|
20
|
+
const supabase = await getServerClient()
|
|
21
|
+
const { data, error } = await supabase.auth.signUp({ email, password })
|
|
22
|
+
if (error) {
|
|
23
|
+
const msg = error.message.toLowerCase().includes('already')
|
|
24
|
+
? '该邮箱已被注册'
|
|
25
|
+
: error.message
|
|
26
|
+
return { user: null, error: msg }
|
|
27
|
+
}
|
|
28
|
+
const u = data.user
|
|
29
|
+
return {
|
|
30
|
+
user: u ? { id: u.id, email: u.email ?? '', createdAt: u.created_at } : null,
|
|
31
|
+
error: null,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async signOut(): Promise<void> {
|
|
36
|
+
const supabase = await getServerClient()
|
|
37
|
+
await supabase.auth.signOut()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getSession(): Promise<AuthUser | null> {
|
|
41
|
+
const supabase = await getServerClient()
|
|
42
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
43
|
+
if (!user) return null
|
|
44
|
+
return { id: user.id, email: user.email ?? '', createdAt: user.created_at }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async signInWithOAuth(provider: 'github', nextPath = '/zh/dashboard'): Promise<{ url: string }> {
|
|
48
|
+
const supabase = await getServerClient()
|
|
49
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'
|
|
50
|
+
const safeNextPath = nextPath.startsWith('/') ? nextPath : '/zh/dashboard'
|
|
51
|
+
const redirectTo = `${appUrl}/auth/callback?next=${encodeURIComponent(safeNextPath)}`
|
|
52
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
53
|
+
provider,
|
|
54
|
+
options: {
|
|
55
|
+
redirectTo,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
if (error || !data.url) {
|
|
59
|
+
throw new Error(error?.message ?? 'OAuth redirect URL unavailable')
|
|
60
|
+
}
|
|
61
|
+
return { url: data.url }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServerClient } from '@supabase/ssr'
|
|
2
2
|
import { createBrowserClient } from '@supabase/ssr'
|
|
3
3
|
import { cookies } from 'next/headers'
|
|
4
|
+
import type { NextRequest, NextResponse } from 'next/server'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Returns a Supabase client safe for use in Server Components and Server Actions.
|
|
@@ -53,3 +54,40 @@ export function getBrowserClient() {
|
|
|
53
54
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
54
55
|
)
|
|
55
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns a Supabase client for use in middleware.
|
|
60
|
+
* Reads and writes cookies via the provided request/response objects.
|
|
61
|
+
*
|
|
62
|
+
* Usage:
|
|
63
|
+
* ```ts
|
|
64
|
+
* // In middleware.ts:
|
|
65
|
+
* const { supabase, response } = createMiddlewareClient(request, response)
|
|
66
|
+
* await supabase.auth.getUser() // refreshes session token
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createMiddlewareClient(
|
|
70
|
+
request: NextRequest,
|
|
71
|
+
response: NextResponse,
|
|
72
|
+
) {
|
|
73
|
+
const supabase = createServerClient(
|
|
74
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
75
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
76
|
+
{
|
|
77
|
+
cookies: {
|
|
78
|
+
getAll() {
|
|
79
|
+
return request.cookies.getAll()
|
|
80
|
+
},
|
|
81
|
+
setAll(cookiesToSet) {
|
|
82
|
+
for (const { name, value } of cookiesToSet) {
|
|
83
|
+
request.cookies.set(name, value)
|
|
84
|
+
}
|
|
85
|
+
for (const { name, value, options } of cookiesToSet) {
|
|
86
|
+
response.cookies.set(name, value, options)
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
return { supabase, response }
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const signInSchema = z.object({
|
|
4
|
+
email: z.string().email('请输入有效的邮箱地址'),
|
|
5
|
+
password: z.string().min(8, '密码至少 8 位'),
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export const signUpSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
email: z.string().email('请输入有效的邮箱地址'),
|
|
11
|
+
password: z.string().min(8, '密码至少 8 位'),
|
|
12
|
+
confirmPassword: z.string(),
|
|
13
|
+
})
|
|
14
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
15
|
+
message: '两次密码不一致',
|
|
16
|
+
path: ['confirmPassword'],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export type SignInInput = z.infer<typeof signInSchema>
|
|
20
|
+
export type SignUpInput = z.infer<typeof signUpSchema>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
@theme inline {
|
|
2
|
+
@keyframes accordion-down {
|
|
3
|
+
from {
|
|
4
|
+
height: 0;
|
|
5
|
+
}
|
|
6
|
+
to {
|
|
7
|
+
height: var(
|
|
8
|
+
--radix-accordion-content-height,
|
|
9
|
+
var(--accordion-panel-height, auto)
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@keyframes accordion-up {
|
|
15
|
+
from {
|
|
16
|
+
height: var(
|
|
17
|
+
--radix-accordion-content-height,
|
|
18
|
+
var(--accordion-panel-height, auto)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
to {
|
|
22
|
+
height: 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Custom variants */
|
|
28
|
+
@custom-variant data-open {
|
|
29
|
+
&:where([data-state="open"]),
|
|
30
|
+
&:where([data-open]:not([data-open="false"])) {
|
|
31
|
+
@slot;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@custom-variant data-closed {
|
|
36
|
+
&:where([data-state="closed"]),
|
|
37
|
+
&:where([data-closed]:not([data-closed="false"])) {
|
|
38
|
+
@slot;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@custom-variant data-checked {
|
|
43
|
+
&:where([data-state="checked"]),
|
|
44
|
+
&:where([data-checked]:not([data-checked="false"])) {
|
|
45
|
+
@slot;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@custom-variant data-unchecked {
|
|
50
|
+
&:where([data-state="unchecked"]),
|
|
51
|
+
&:where([data-unchecked]:not([data-unchecked="false"])) {
|
|
52
|
+
@slot;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@custom-variant data-selected {
|
|
57
|
+
&:where([data-selected="true"]) {
|
|
58
|
+
@slot;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@custom-variant data-disabled {
|
|
63
|
+
&:where([data-disabled="true"]),
|
|
64
|
+
&:where([data-disabled]:not([data-disabled="false"])) {
|
|
65
|
+
@slot;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@custom-variant data-active {
|
|
70
|
+
&:where([data-state="active"]),
|
|
71
|
+
&:where([data-active]:not([data-active="false"])) {
|
|
72
|
+
@slot;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@custom-variant data-horizontal {
|
|
77
|
+
&:where([data-orientation="horizontal"]) {
|
|
78
|
+
@slot;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@custom-variant data-vertical {
|
|
83
|
+
&:where([data-orientation="vertical"]) {
|
|
84
|
+
@slot;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@utility no-scrollbar {
|
|
89
|
+
-ms-overflow-style: none;
|
|
90
|
+
scrollbar-width: none;
|
|
91
|
+
|
|
92
|
+
&::-webkit-scrollbar {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}@theme inline{--animation-delay-0: 0s; --animation-delay-75: 75ms; --animation-delay-100: .1s; --animation-delay-150: .15s; --animation-delay-200: .2s; --animation-delay-300: .3s; --animation-delay-500: .5s; --animation-delay-700: .7s; --animation-delay-1000: 1s; --animation-repeat-0: 0; --animation-repeat-1: 1; --animation-repeat-infinite: infinite; --animation-direction-normal: normal; --animation-direction-reverse: reverse; --animation-direction-alternate: alternate; --animation-direction-alternate-reverse: alternate-reverse; --animation-fill-mode-none: none; --animation-fill-mode-forwards: forwards; --animation-fill-mode-backwards: backwards; --animation-fill-mode-both: both; --percentage-0: 0; --percentage-5: .05; --percentage-10: .1; --percentage-15: .15; --percentage-20: .2; --percentage-25: .25; --percentage-30: .3; --percentage-35: .35; --percentage-40: .4; --percentage-45: .45; --percentage-50: .5; --percentage-55: .55; --percentage-60: .6; --percentage-65: .65; --percentage-70: .7; --percentage-75: .75; --percentage-80: .8; --percentage-85: .85; --percentage-90: .9; --percentage-95: .95; --percentage-100: 1; --percentage-translate-full: 1; --animate-in: enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none); --animate-out: exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none); @keyframes enter { from { opacity: var(--tw-enter-opacity,1); transform: translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0)); filter: blur(var(--tw-enter-blur,0)); }}@keyframes exit { to { opacity: var(--tw-exit-opacity,1); transform: translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0)); filter: blur(var(--tw-exit-blur,0)); }}--animate-accordion-down: accordion-down var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none); --animate-accordion-up: accordion-up var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none); --animate-collapsible-down: collapsible-down var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none); --animate-collapsible-up: collapsible-up var(--tw-animation-duration,var(--tw-duration,.2s))var(--tw-ease,ease-out)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none); @keyframes accordion-down { from { height: 0; }to { height: var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,var(--ngp-accordion-content-height,auto))))); }}@keyframes accordion-up { from { height: var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,var(--ngp-accordion-content-height,auto))))); }to { height: 0; }}@keyframes collapsible-down { from { height: 0; }to { height: var(--radix-collapsible-content-height,var(--bits-collapsible-content-height,var(--reka-collapsible-content-height,var(--kb-collapsible-content-height,auto)))); }}@keyframes collapsible-up { from { height: var(--radix-collapsible-content-height,var(--bits-collapsible-content-height,var(--reka-collapsible-content-height,var(--kb-collapsible-content-height,auto)))); }to { height: 0; }}--animate-caret-blink: caret-blink 1.25s ease-out infinite; @keyframes caret-blink { 0%,70%,100% { opacity: 1; }20%,50% { opacity: 0; }}}@utility animation-duration-*{--tw-animation-duration: calc(--value(number)*1ms); --tw-animation-duration: --value(--animation-duration-*,[duration],"initial",[*]); animation-duration: calc(--value(number)*1ms); animation-duration: --value(--animation-duration-*,[duration],"initial",[*]);}@utility delay-*{animation-delay: calc(--value(number)*1ms); animation-delay: --value(--animation-delay-*,[duration],"initial",[*]); --tw-animation-delay: calc(--value(number)*1ms); --tw-animation-delay: --value(--animation-delay-*,[duration],"initial",[*]);}@utility repeat-*{animation-iteration-count: --value(--animation-repeat-*,number,"initial",[*]); --tw-animation-iteration-count: --value(--animation-repeat-*,number,"initial",[*]);}@utility direction-*{animation-direction: --value(--animation-direction-*,"initial",[*]); --tw-animation-direction: --value(--animation-direction-*,"initial",[*]);}@utility fill-mode-*{animation-fill-mode: --value(--animation-fill-mode-*,"initial",[*]); --tw-animation-fill-mode: --value(--animation-fill-mode-*,"initial",[*]);}@utility running{animation-play-state: running;}@utility paused{animation-play-state: paused;}@utility play-state-*{animation-play-state: --value("initial",[*]);}@utility blur-in{--tw-enter-blur: 20px;}@utility blur-in-*{--tw-enter-blur: calc(--value(number)*1px); --tw-enter-blur: --value(--blur-*,[*]);}@utility blur-out{--tw-exit-blur: 20px;}@utility blur-out-*{--tw-exit-blur: calc(--value(number)*1px); --tw-exit-blur: --value(--blur-*,[*]);}@utility fade-in{--tw-enter-opacity: 0;}@utility fade-in-*{--tw-enter-opacity: calc(--value(number)/100); --tw-enter-opacity: --value(--percentage-*,[*]);}@utility fade-out{--tw-exit-opacity: 0;}@utility fade-out-*{--tw-exit-opacity: calc(--value(number)/100); --tw-exit-opacity: --value(--percentage-*,[*]);}@utility zoom-in{--tw-enter-scale: 0;}@utility zoom-in-*{--tw-enter-scale: calc(--value(number)*1%); --tw-enter-scale: calc(--value(ratio)); --tw-enter-scale: --value(--percentage-*,[*]);}@utility -zoom-in-*{--tw-enter-scale: calc(--value(number)*-1%); --tw-enter-scale: calc(--value(ratio)*-1); --tw-enter-scale: --value(--percentage-*,[*]);}@utility zoom-out{--tw-exit-scale: 0;}@utility zoom-out-*{--tw-exit-scale: calc(--value(number)*1%); --tw-exit-scale: calc(--value(ratio)); --tw-exit-scale: --value(--percentage-*,[*]);}@utility -zoom-out-*{--tw-exit-scale: calc(--value(number)*-1%); --tw-exit-scale: calc(--value(ratio)*-1); --tw-exit-scale: --value(--percentage-*,[*]);}@utility spin-in{--tw-enter-rotate: 30deg;}@utility spin-in-*{--tw-enter-rotate: calc(--value(number)*1deg); --tw-enter-rotate: calc(--value(ratio)*360deg); --tw-enter-rotate: --value(--rotate-*,[*]);}@utility -spin-in{--tw-enter-rotate: -30deg;}@utility -spin-in-*{--tw-enter-rotate: calc(--value(number)*-1deg); --tw-enter-rotate: calc(--value(ratio)*-360deg); --tw-enter-rotate: --value(--rotate-*,[*]);}@utility spin-out{--tw-exit-rotate: 30deg;}@utility spin-out-*{--tw-exit-rotate: calc(--value(number)*1deg); --tw-exit-rotate: calc(--value(ratio)*360deg); --tw-exit-rotate: --value(--rotate-*,[*]);}@utility -spin-out{--tw-exit-rotate: -30deg;}@utility -spin-out-*{--tw-exit-rotate: calc(--value(number)*-1deg); --tw-exit-rotate: calc(--value(ratio)*-360deg); --tw-exit-rotate: --value(--rotate-*,[*]);}@utility slide-in-from-top{--tw-enter-translate-y: -100%;}@utility slide-in-from-top-*{--tw-enter-translate-y: calc(--value(integer)*var(--spacing)*-1); --tw-enter-translate-y: calc(--value(--percentage-*,--percentage-translate-*)*-100%); --tw-enter-translate-y: calc(--value(ratio)*-100%); --tw-enter-translate-y: calc(--value(--translate-*,[percentage],[length])*-1);}@utility slide-in-from-bottom{--tw-enter-translate-y: 100%;}@utility slide-in-from-bottom-*{--tw-enter-translate-y: calc(--value(integer)*var(--spacing)); --tw-enter-translate-y: calc(--value(--percentage-*,--percentage-translate-*)*100%); --tw-enter-translate-y: calc(--value(ratio)*100%); --tw-enter-translate-y: --value(--translate-*,[percentage],[length]);}@utility slide-in-from-left{--tw-enter-translate-x: -100%;}@utility slide-in-from-left-*{--tw-enter-translate-x: calc(--value(integer)*var(--spacing)*-1); --tw-enter-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*-100%); --tw-enter-translate-x: calc(--value(ratio)*-100%); --tw-enter-translate-x: calc(--value(--translate-*,[percentage],[length])*-1);}@utility slide-in-from-right{--tw-enter-translate-x: 100%;}@utility slide-in-from-right-*{--tw-enter-translate-x: calc(--value(integer)*var(--spacing)); --tw-enter-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*100%); --tw-enter-translate-x: calc(--value(ratio)*100%); --tw-enter-translate-x: --value(--translate-*,[percentage],[length]);}@utility slide-in-from-start{&:dir(ltr){ --tw-enter-translate-x: -100%; }&:dir(rtl){ --tw-enter-translate-x: 100%; }}@utility slide-in-from-start-*{&:where(:dir(ltr),[dir="ltr"],[dir="ltr"]*){ --tw-enter-translate-x: calc(--value(integer)*var(--spacing)*-1); --tw-enter-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*-100%); --tw-enter-translate-x: calc(--value(ratio)*-100%); --tw-enter-translate-x: calc(--value(--translate-*,[percentage],[length])*-1); }&:where(:dir(rtl),[dir="rtl"],[dir="rtl"]*){ --tw-enter-translate-x: calc(--value(integer)*var(--spacing)); --tw-enter-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*100%); --tw-enter-translate-x: calc(--value(ratio)*100%); --tw-enter-translate-x: --value(--translate-*,[percentage],[length]); }}@utility slide-in-from-end{&:dir(ltr){ --tw-enter-translate-x: 100%; }&:dir(rtl){ --tw-enter-translate-x: -100%; }}@utility slide-in-from-end-*{&:where(:dir(ltr),[dir="ltr"],[dir="ltr"]*){ --tw-enter-translate-x: calc(--value(integer)*var(--spacing)); --tw-enter-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*100%); --tw-enter-translate-x: calc(--value(ratio)*100%); --tw-enter-translate-x: --value(--translate-*,[percentage],[length]); }&:where(:dir(rtl),[dir="rtl"],[dir="rtl"]*){ --tw-enter-translate-x: calc(--value(integer)*var(--spacing)*-1); --tw-enter-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*-100%); --tw-enter-translate-x: calc(--value(ratio)*-100%); --tw-enter-translate-x: calc(--value(--translate-*,[percentage],[length])*-1); }}@utility slide-out-to-top{--tw-exit-translate-y: -100%;}@utility slide-out-to-top-*{--tw-exit-translate-y: calc(--value(integer)*var(--spacing)*-1); --tw-exit-translate-y: calc(--value(--percentage-*,--percentage-translate-*)*-100%); --tw-exit-translate-y: calc(--value(ratio)*-100%); --tw-exit-translate-y: calc(--value(--translate-*,[percentage],[length])*-1);}@utility slide-out-to-bottom{--tw-exit-translate-y: 100%;}@utility slide-out-to-bottom-*{--tw-exit-translate-y: calc(--value(integer)*var(--spacing)); --tw-exit-translate-y: calc(--value(--percentage-*,--percentage-translate-*)*100%); --tw-exit-translate-y: calc(--value(ratio)*100%); --tw-exit-translate-y: --value(--translate-*,[percentage],[length]);}@utility slide-out-to-left{--tw-exit-translate-x: -100%;}@utility slide-out-to-left-*{--tw-exit-translate-x: calc(--value(integer)*var(--spacing)*-1); --tw-exit-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*-100%); --tw-exit-translate-x: calc(--value(ratio)*-100%); --tw-exit-translate-x: calc(--value(--translate-*,[percentage],[length])*-1);}@utility slide-out-to-right{--tw-exit-translate-x: 100%;}@utility slide-out-to-right-*{--tw-exit-translate-x: calc(--value(integer)*var(--spacing)); --tw-exit-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*100%); --tw-exit-translate-x: calc(--value(ratio)*100%); --tw-exit-translate-x: --value(--translate-*,[percentage],[length]);}@utility slide-out-to-start{&:dir(ltr){ --tw-exit-translate-x: -100%; }&:dir(rtl){ --tw-exit-translate-x: 100%; }}@utility slide-out-to-start-*{&:where(:dir(ltr),[dir="ltr"],[dir="ltr"]*){ --tw-exit-translate-x: calc(--value(integer)*var(--spacing)*-1); --tw-exit-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*-100%); --tw-exit-translate-x: calc(--value(ratio)*-100%); --tw-exit-translate-x: calc(--value(--translate-*,[percentage],[length])*-1); }&:where(:dir(rtl),[dir="rtl"],[dir="rtl"]*){ --tw-exit-translate-x: calc(--value(integer)*var(--spacing)); --tw-exit-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*100%); --tw-exit-translate-x: calc(--value(ratio)*100%); --tw-exit-translate-x: --value(--translate-*,[percentage],[length]); }}@utility slide-out-to-end{&:dir(ltr){ --tw-exit-translate-x: 100%; }&:dir(rtl){ --tw-exit-translate-x: -100%; }}@utility slide-out-to-end-*{&:where(:dir(ltr),[dir="ltr"],[dir="ltr"]*){ --tw-exit-translate-x: calc(--value(integer)*var(--spacing)); --tw-exit-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*100%); --tw-exit-translate-x: calc(--value(ratio)*100%); --tw-exit-translate-x: --value(--translate-*,[percentage],[length]); }&:where(:dir(rtl),[dir="rtl"],[dir="rtl"]*){ --tw-exit-translate-x: calc(--value(integer)*var(--spacing)*-1); --tw-exit-translate-x: calc(--value(--percentage-*,--percentage-translate-*)*-100%); --tw-exit-translate-x: calc(--value(ratio)*-100%); --tw-exit-translate-x: calc(--value(--translate-*,[percentage],[length])*-1); }}
|