fix(css): fix version

This commit is contained in:
2026-02-10 12:23:47 +08:00
parent 8c033066af
commit 2696ec8692
21 changed files with 658 additions and 194 deletions

View File

@@ -1,6 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

View File

@@ -1,6 +1,6 @@
# iam-front
统一认证前端SSO 登录页),基于 Next.js 14App Router+ TypeScript + Tailwind CSS + shadcn/ui。
统一认证前端SSO 登录页),基于 Next.js 16App Router+ TypeScript + Tailwind CSS + shadcn/ui。
## 本地启动
@@ -17,3 +17,43 @@ npm install
npm run dev
```
> 说明:本项目使用 Turbopack 开发模式(`next dev --turbopack`)。遇到 Linux file watch 限制ENOSPC时会自动使用 polling不影响 Fast Refresh但可能略增 CPU
## 组件使用示例
统一登录页默认使用带 Tabs 切换的组件(登录/注册):
- 页面路由:[login/page.tsx](file:///home/shay/project/backend/iam-front/src/app/login/page.tsx)
- 组件入口:[login-form.tsx](file:///home/shay/project/backend/iam-front/src/components/login-form.tsx)
示例(页面内):
```tsx
<LoginFormCard
clientId={searchParams.clientId ?? ""}
tenantId={searchParams.tenantId ?? ""}
callback={searchParams.callback ?? ""}
initialEmail={rememberedEmail}
/>
```
## 常见问题
### 1) ENOSPC: System limit for number of file watchers reached
这是 Linux `inotify` 文件监听数量上限过低导致Next.js/Turbopack 在 dev 模式需要文件监听,超过上限会报错)。
本项目在 `next.config.js` 中做了自动降级:当检测到上限偏低时,会启用 polling watcher 来避免启动失败(可能会略增 CPU 占用)。
你也可以手动启用 polling
```bash
npm run dev:poll
```
如需从根源修复(需要 sudo 权限),可提高系统上限(示例):
```bash
sudo sysctl -w fs.inotify.max_user_watches=524288
sudo sysctl -w fs.inotify.max_user_instances=1024
```

15
eslint.config.mjs Normal file
View File

@@ -0,0 +1,15 @@
import tseslint from "typescript-eslint";
import next from "@next/eslint-plugin-next";
export default [
{ ignores: [".next/**", "node_modules/**", "scripts/**", "next.config.js"] },
...tseslint.configs.recommended,
{
files: ["**/*.{js,jsx,ts,tsx}"],
plugins: { "@next/next": next },
rules: {
...next.configs.recommended.rules,
...next.configs["core-web-vitals"].rules,
},
},
];

3
next-env.d.ts vendored
View File

@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,40 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
async headers() {
const isDev = process.env.NODE_ENV === "development";
const csp = [
"default-src 'self'",
"base-uri 'self'",
"frame-ancestors 'none'",
"object-src 'none'",
"form-action 'self'",
"img-src 'self' data:",
isDev
? "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
: "script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"connect-src 'self'",
].join("; ");
return [
{
source: "/(.*)",
headers: [
{ key: "Content-Security-Policy", value: csp },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "no-referrer" },
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{ key: "Permissions-Policy", value: "geolocation=(), microphone=(), camera=()" },
],
},
];
},
};
module.exports = nextConfig;

View File

@@ -1,36 +1,40 @@
{
"name": "iam-front",
"private": true,
"version": "0.1.0",
"scripts": {
"dev": "next dev -p 6020",
"build": "next build",
"start": "next start -p 6020",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jose": "^5.9.6",
"lucide-react": "^0.468.0",
"next": "^14.2.25",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"redis": "^4.7.0",
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@types/node": "^20.17.16",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "^14.2.25",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3"
}
}
"name": "iam-front",
"private": true,
"version": "0.1.0",
"scripts": {
"dev": "next dev --turbopack -p 6020",
"build": "next build",
"start": "next start -p 6020",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-slot": "^1.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jose": "^5.9.6",
"lucide-react": "^0.468.0",
"next": "^16.1.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^20.17.16",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@next/eslint-plugin-next": "^16.1.6",
"autoprefixer": "^10.4.20",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.6",
"postcss": "^8.5.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.7.3",
"typescript-eslint": "^8.54.0"
}
}

View File

@@ -1,7 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

6
postcss.config.mjs Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
autoprefixer: {},
},
}

View File

@@ -1,6 +1,101 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
/* Custom Container Max Width */
--container-max-width: 1200px;
}
/* Fluid Typography & Base Styles */
@layer base {
html {
/* Mobile base size */
font-size: 14px;
/* Desktop base size (md breakpoint approx 768px) */
@media (min-width: 768px) {
font-size: 16px;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
/* Fluid scaling using clamp/calc */
font-size: clamp(1.25rem, 1rem + 1vw, 2.5rem);
line-height: 1.2;
}
}
/* Utility for Grid System */
@utility grid-responsive {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
@media (min-width: 768px) {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
@media (min-width: 1024px) {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
}
/* Utility for Responsive Content Container */
@utility container-responsive {
width: 100%;
margin-left: auto;
margin-right: auto;
max-width: 1200px;
/* Mobile Padding */
padding-left: 1rem; /* 16px */
padding-right: 1rem;
@media (min-width: 768px) {
/* Tablet/Desktop Padding */
padding-left: 2.5rem; /* 40px */
padding-right: 2.5rem;
}
@media (min-width: 1024px) {
padding-left: 3.75rem; /* 60px */
padding-right: 3.75rem;
}
}
:root {
--background: 0 0% 100%;
@@ -27,4 +122,3 @@ body {
background: hsl(var(--background));
color: hsl(var(--foreground));
}

View File

@@ -1,25 +1,29 @@
import { cookies } from "next/headers";
import dynamic from "next/dynamic";
import LoginFormCard from "@/components/login-form";
const LoginFormNoSSR = dynamic(() => import("@/components/login-form"), {
ssr: false,
});
export default function LoginPage({
export default async function LoginPage({
searchParams,
}: {
searchParams: { tenantId?: string; callback?: string; clientId?: string };
searchParams: Promise<{
tenantId?: string;
callback?: string;
clientId?: string;
}>;
}) {
const rememberedEmail = cookies().get("iam_remember_email")?.value ?? "";
const cookieStore = await cookies();
const rememberedEmail = cookieStore.get("iam_remember_email")?.value ?? "";
const sp = await searchParams;
return (
<main className="min-h-screen flex items-center justify-center p-6">
<LoginFormNoSSR
clientId={searchParams.clientId ?? ""}
tenantId={searchParams.tenantId ?? ""}
callback={searchParams.callback ?? ""}
initialEmail={rememberedEmail}
/>
<main className="min-h-screen flex items-center justify-center container-responsive">
<div className="w-full max-w-md mx-auto">
<LoginFormCard
clientId={sp.clientId ?? ""}
tenantId={sp.tenantId ?? ""}
callback={sp.callback ?? ""}
initialEmail={rememberedEmail}
/>
</div>
</main>
);
}

View File

@@ -0,0 +1,50 @@
import * as React from "react";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function CaptchaField(props: {
captcha: string;
onCaptchaChange: (next: string) => void;
captchaKey: string;
onRefresh: () => void;
disabled?: boolean;
}) {
return (
<div className="space-y-2">
<Label htmlFor="captcha"></Label>
<div className="flex gap-2">
<Input
id="captcha"
inputMode="numeric"
value={props.captcha}
onChange={(e) => props.onCaptchaChange(e.target.value)}
disabled={props.disabled}
required
/>
<button
type="button"
className="shrink-0 rounded-md border border-input bg-background px-2"
onClick={props.onRefresh}
aria-label="刷新验证码"
disabled={props.disabled}
>
{props.captchaKey ? (
<Image
alt="captcha"
src={`/api/captcha?key=${encodeURIComponent(props.captchaKey)}`}
width={120}
height={40}
className="w-[120px] h-[40px] object-contain"
unoptimized
priority
/>
) : (
<div className="w-[120px] h-[40px] bg-muted/50 animate-pulse rounded-md" />
)}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,150 @@
import * as React from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { CaptchaField } from "@/components/auth/captcha-field"
export function LoginForm(props: {
clientId: string
tenantId: string
callback: string
initialEmail: string
prefillPassword?: string
disabled?: boolean
externalError?: string | null
onClearExternalError?: () => void
captcha: string
onCaptchaChange: (next: string) => void
captchaKey: string
onRefreshCaptcha: () => void
}) {
const [email, setEmail] = React.useState(props.initialEmail)
const [password, setPassword] = React.useState("")
const [rememberMe, setRememberMe] = React.useState(Boolean(props.initialEmail))
const [submitting, setSubmitting] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
React.useEffect(() => {
setEmail(props.initialEmail)
setRememberMe(Boolean(props.initialEmail))
}, [props.initialEmail])
React.useEffect(() => {
if (props.prefillPassword) {
setPassword(props.prefillPassword)
}
}, [props.prefillPassword])
const missingParams = !props.clientId || !props.tenantId || !props.callback
async function onSubmit(e: React.FormEvent) {
e.preventDefault()
setError(null)
props.onClearExternalError?.()
setSubmitting(true)
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clientId: props.clientId,
tenantId: props.tenantId,
callback: props.callback,
email,
password,
captcha: props.captcha,
rememberMe,
}),
})
const json = (await res.json()) as { redirectTo?: string; message?: string }
if (!res.ok || !json.redirectTo) {
throw new Error(json.message || "登录失败")
}
window.location.href = json.redirectTo
} catch (err) {
setError(err instanceof Error ? err.message : "登录失败")
props.onCaptchaChange("")
props.onRefreshCaptcha()
} finally {
setSubmitting(false)
}
}
const disabled = Boolean(props.disabled) || submitting
return (
<form onSubmit={onSubmit} className="space-y-4">
{missingParams ? (
<div className="text-sm text-destructive">
clientIdtenantId callback
</div>
) : null}
{props.externalError ? (
<div className="text-sm text-destructive">{props.externalError}</div>
) : null}
{error ? <div className="text-sm text-destructive">{error}</div> : null}
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
autoComplete="username"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="user@example.com"
disabled={disabled}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={disabled}
required
/>
</div>
<CaptchaField
captcha={props.captcha}
onCaptchaChange={props.onCaptchaChange}
captchaKey={props.captchaKey}
onRefresh={props.onRefreshCaptcha}
disabled={disabled}
/>
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={rememberMe}
onCheckedChange={(v) => setRememberMe(Boolean(v))}
disabled={disabled}
/>
</label>
<Link
className="text-sm underline underline-offset-4"
href={`/forgot-password?tenantId=${encodeURIComponent(props.tenantId)}`}
>
</Link>
</div>
<Button
type="submit"
className="w-full"
disabled={disabled || missingParams}
>
{submitting ? "登录中..." : "登录"}
</Button>
</form>
)
}

View File

@@ -0,0 +1,155 @@
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { CaptchaField } from "@/components/auth/captcha-field"
export function RegisterForm(props: {
tenantId: string
disabled?: boolean
captcha: string
onCaptchaChange: (next: string) => void
captchaKey: string
onRefreshCaptcha: () => void
onRegisterSuccessPrefillLogin: (payload: { email: string; password: string }) => void
onLoginAfterRegister: (payload: { email: string; password: string }) => Promise<void>
}) {
const [email, setEmail] = React.useState("")
const [password, setPassword] = React.useState("")
const [confirmPassword, setConfirmPassword] = React.useState("")
const [submitting, setSubmitting] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
const passwordMismatch =
confirmPassword.length > 0 && password !== confirmPassword
const missingTenant = !props.tenantId
async function onSubmit(e: React.FormEvent) {
e.preventDefault()
setError(null)
const nextEmail = email.trim()
if (!props.tenantId) {
setError("缺少 tenantId 参数,无法注册")
return
}
if (!nextEmail || !password || !confirmPassword) {
setError("请填写邮箱与密码")
return
}
if (passwordMismatch) {
setError("两次输入的密码不一致")
return
}
if (!props.captcha.trim()) {
setError("请填写验证码")
return
}
setSubmitting(true)
try {
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
tenantId: props.tenantId,
email: nextEmail,
password,
}),
})
const json = (await res.json()) as { message?: string; id?: string }
if (!res.ok) {
if (res.status === 409) {
throw new Error("该邮箱已注册,请直接登录")
}
throw new Error(json.message || "注册失败")
}
if (!json.id) {
throw new Error("注册失败")
}
props.onRegisterSuccessPrefillLogin({ email: nextEmail, password })
await props.onLoginAfterRegister({ email: nextEmail, password })
} catch (err) {
setError(err instanceof Error ? err.message : "注册失败")
props.onCaptchaChange("")
props.onRefreshCaptcha()
} finally {
setSubmitting(false)
}
}
const disabled = Boolean(props.disabled) || submitting
return (
<form onSubmit={onSubmit} className="space-y-4">
{missingTenant ? (
<div className="text-sm text-destructive">
tenantId
</div>
) : null}
{error ? <div className="text-sm text-destructive">{error}</div> : null}
<div className="space-y-2">
<Label htmlFor="registerEmail"></Label>
<Input
id="registerEmail"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="user@example.com"
disabled={disabled}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="registerPassword"></Label>
<Input
id="registerPassword"
type="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={disabled}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="registerConfirmPassword"></Label>
<Input
id="registerConfirmPassword"
type="password"
autoComplete="new-password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={disabled}
required
/>
{passwordMismatch ? (
<div className="text-sm text-destructive"></div>
) : null}
</div>
<CaptchaField
captcha={props.captcha}
onCaptchaChange={props.onCaptchaChange}
captchaKey={props.captchaKey}
onRefresh={props.onRefreshCaptcha}
disabled={disabled}
/>
<Button
type="submit"
className="w-full"
disabled={disabled || passwordMismatch || missingTenant}
>
{submitting ? "注册中..." : "注册并登录"}
</Button>
</form>
)
}

View File

@@ -21,7 +21,7 @@ const LoginFormCard = (props: {
}) => {
const [tab, setTab] = React.useState<"login" | "register">("login");
const [captcha, setCaptcha] = React.useState("");
const [captchaKey, setCaptchaKey] = React.useState(() => String(Date.now()));
const [captchaKey, setCaptchaKey] = React.useState("");
const [loginExternalError, setLoginExternalError] = React.useState<
string | null
>(null);
@@ -30,6 +30,10 @@ const LoginFormCard = (props: {
password: string;
} | null>(null);
React.useEffect(() => {
setCaptchaKey(String(Date.now()));
}, []);
function refreshCaptcha() {
setCaptchaKey(String(Date.now()));
}

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-base md:text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {

View File

@@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}

View File

@@ -0,0 +1,50 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn("mt-4 ring-offset-background focus-visible:outline-none", className)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -1,22 +0,0 @@
import { createClient } from "redis"
import { mustGetEnv } from "@/lib/env"
type AnyRedisClient = ReturnType<typeof createClient>
let client: AnyRedisClient | null = null
let connecting: Promise<AnyRedisClient> | null = null
export async function getRedis(): Promise<AnyRedisClient> {
if (client) return client
if (connecting) return connecting
connecting = (async () => {
const c = createClient({ url: mustGetEnv("REDIS_URL") })
await c.connect()
client = c
return c
})()
return connecting
}

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server"
export function middleware(req: NextRequest) {
export function proxy(req: NextRequest) {
if (process.env.NODE_ENV === "production") {
const proto = req.headers.get("x-forwarded-proto")
if (proto && proto !== "https") {
@@ -15,4 +15,3 @@ export function middleware(req: NextRequest) {
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
}

View File

@@ -1,57 +0,0 @@
import type { Config } from "tailwindcss"
const config: Config = {
darkMode: ["class"],
content: ["./src/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [],
}
export default config

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "es2022"],
"lib": [
"dom",
"dom.iterable",
"es2022"
],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
@@ -11,15 +15,28 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [{ "name": "next" }],
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": [
"src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}