feat: 增加 shadcn 样式

This commit is contained in:
hzm
2026-03-06 22:34:10 +08:00
parent 1f9a4ff97a
commit 792986dd79
11 changed files with 6357 additions and 33 deletions

View File

@@ -1,5 +1,6 @@
'use client'; 'use client';
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
import { MailIcon } from './Icons'; import { MailIcon } from './Icons';
export default function LoginModal({ export default function LoginModal({
@@ -56,15 +57,21 @@ export default function LoginModal({
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}> <div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}>
请输入邮箱验证码以完成注册/登录 请输入邮箱验证码以完成注册/登录
</div> </div>
<input <InputOTP
className="input"
type="text"
placeholder="输入验证码"
value={loginOtp}
onChange={(e) => setLoginOtp(e.target.value)}
disabled={loginLoading}
maxLength={6} maxLength={6}
/> value={loginOtp}
onChange={(value) => setLoginOtp(value)}
disabled={loginLoading}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</div> </div>
)} )}
{loginError && ( {loginError && (

View File

@@ -3,6 +3,13 @@
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { CloseIcon } from './Icons'; import { CloseIcon } from './Icons';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
export default function ScanImportConfirmModal({ export default function ScanImportConfirmModal({
scannedFunds, scannedFunds,
@@ -121,18 +128,18 @@ export default function ScanImportConfirmModal({
</div> </div>
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="muted" style={{ fontSize: 13, whiteSpace: 'nowrap' }}>添加到分组</span> <span className="muted" style={{ fontSize: 13, whiteSpace: 'nowrap' }}>添加到分组</span>
<select <Select value={selectedGroupId} onValueChange={(value) => setSelectedGroupId(value)}>
className="select" <SelectTrigger className="flex-1">
value={selectedGroupId} <SelectValue placeholder="选择分组" />
onChange={(e) => setSelectedGroupId(e.target.value)} </SelectTrigger>
style={{ flex: 1 }} <SelectContent>
> <SelectItem value="all">全部</SelectItem>
<option value="all">全部</option> <SelectItem value="fav">自选</SelectItem>
<option value="fav">自选</option> {groups.filter(g => g.id !== 'all' && g.id !== 'fav').map(g => (
{groups.filter(g => g.id !== 'all' && g.id !== 'fav').map(g => ( <SelectItem key={g.id} value={g.id}>{g.name}</SelectItem>
<option key={g.id} value={g.id}>{g.name}</option> ))}
))} </SelectContent>
</select> </Select>
</div> </div>
</> </>
)} )}

View File

@@ -1,3 +1,9 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
:root { :root {
--bg: #0f172a; --bg: #0f172a;
--card: #111827; --card: #111827;
@@ -10,6 +16,33 @@
--border: #1f2937; --border: #1f2937;
--table-pinned-header-bg: #2a394b; --table-pinned-header-bg: #2a394b;
--table-row-hover-bg: #2a394b; --table-row-hover-bg: #2a394b;
--radius: 0.625rem;
--background: #0f172a;
--foreground: #e5e7eb;
--card-foreground: #e5e7eb;
--popover: #111827;
--popover-foreground: #e5e7eb;
--primary-foreground: #0f172a;
--secondary: #1f2937;
--secondary-foreground: #e5e7eb;
--muted-foreground: #9ca3af;
--accent-foreground: #e5e7eb;
--destructive: #f87171;
--input: #1f2937;
--ring: #22d3ee;
--chart-1: #22d3ee;
--chart-2: #60a5fa;
--chart-3: #34d399;
--chart-4: #f472b6;
--chart-5: #fbbf24;
--sidebar: #111827;
--sidebar-foreground: #e5e7eb;
--sidebar-primary: #22d3ee;
--sidebar-primary-foreground: #0f172a;
--sidebar-accent: #1f2937;
--sidebar-accent-foreground: #e5e7eb;
--sidebar-border: #1f2937;
--sidebar-ring: #22d3ee;
} }
/* 亮色主题ui-ux-pro-max 规范 - 正文 #0F172A、弱化 #475569、玻璃 bg-white/80+、边框可见 */ /* 亮色主题ui-ux-pro-max 规范 - 正文 #0F172A、弱化 #475569、玻璃 bg-white/80+、边框可见 */
@@ -25,6 +58,32 @@
--border: #e2e8f0; --border: #e2e8f0;
--table-pinned-header-bg: #e2e8f0; --table-pinned-header-bg: #e2e8f0;
--table-row-hover-bg: #e2e8f0; --table-row-hover-bg: #e2e8f0;
--background: #ffffff;
--foreground: #0f172a;
--card-foreground: #0f172a;
--popover: #ffffff;
--popover-foreground: #0f172a;
--primary-foreground: #ffffff;
--secondary: #f1f5f9;
--secondary-foreground: #0f172a;
--muted-foreground: #475569;
--accent-foreground: #ffffff;
--destructive: #dc2626;
--input: #e2e8f0;
--ring: #0891b2;
--chart-1: #0891b2;
--chart-2: #2563eb;
--chart-3: #059669;
--chart-4: #db2777;
--chart-5: #ca8a04;
--sidebar: #f8fafc;
--sidebar-foreground: #0f172a;
--sidebar-primary: #0891b2;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f1f5f9;
--sidebar-accent-foreground: #0f172a;
--sidebar-border: #e2e8f0;
--sidebar-ring: #0891b2;
} }
* { * {
@@ -698,7 +757,6 @@ body::before {
display: none; display: none;
} }
.navbar-input-field { .navbar-input-field {
flex: 1; flex: 1;
min-width: 120px; min-width: 120px;
@@ -3205,3 +3263,87 @@ input[type="number"] {
background: rgba(8, 145, 178, 0.12); background: rgba(8, 145, 178, 0.12);
color: var(--primary); color: var(--primary);
} }
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
.dark {
--background: #020617;
--foreground: #f8fafc;
--card: #0f172a;
--card-foreground: #f8fafc;
--popover: #0f172a;
--popover-foreground: #f8fafc;
--primary: #22d3ee;
--primary-foreground: #0f172a;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--muted: #1e293b;
--muted-foreground: #94a3af;
--accent: #60a5fa;
--accent-foreground: #f8fafc;
--destructive: #f87171;
--border: #1f2937;
--input: #1e293b;
--ring: #22d3ee;
--chart-1: #22d3ee;
--chart-2: #60a5fa;
--chart-3: #34d399;
--chart-4: #f472b6;
--chart-5: #fbbf24;
--sidebar: #0f172a;
--sidebar-foreground: #f8fafc;
--sidebar-primary: #22d3ee;
--sidebar-primary-foreground: #0f172a;
--sidebar-accent: #1e293b;
--sidebar-accent-foreground: #f8fafc;
--sidebar-border: #1f2937;
--sidebar-ring: #22d3ee;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

23
components.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": false,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@@ -0,0 +1,79 @@
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function InputOTP({
className,
containerClassName,
...props
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn("flex items-center gap-2 has-disabled:opacity-50", containerClassName)}
className={cn("disabled:cursor-not-allowed disabled:opacity-50", className)}
{...props} />
);
}
function InputOTPGroup({
className,
...props
}) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props} />
);
}
function InputOTPSlot({
index,
className,
...props
}) {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"relative flex h-12 w-10 items-center justify-center rounded-md border-2 bg-background text-lg font-semibold shadow-sm transition-all duration-200",
"border-input/60 dark:border-input/80",
"text-foreground dark:text-foreground",
"first:rounded-l-md last:rounded-r-md",
"focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary",
"data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-primary/30 dark:data-[active=true]:ring-primary/40",
"aria-invalid:border-destructive aria-invalid:text-destructive",
"dark:bg-slate-900/50 dark:data-[active=true]:bg-slate-800/50",
className
)}
{...props}>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-6 w-px animate-caret-blink bg-primary duration-1000" />
</div>
)}
</div>
);
}
function InputOTPSeparator({
...props
}) {
return (
<div data-slot="input-otp-separator" role="separator" className="text-muted-foreground dark:text-muted-foreground/50" {...props}>
<MinusIcon className="h-4 w-4" />
</div>
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

197
components/ui/select.jsx Normal file
View File

@@ -0,0 +1,197 @@
"use client"
import * as React from "react"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Select({
...props
}) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-full items-center justify-between gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium whitespace-nowrap shadow-sm transition-all duration-200 outline-none",
"border-input bg-background text-foreground",
"hover:border-primary/60 hover:ring-1 hover:ring-primary/30",
"focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/50",
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-input disabled:hover:ring-0",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
"data-[placeholder]:text-muted-foreground",
"data-[size=default]:h-11 data-[size=sm]:h-10",
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-60 transition-transform duration-200" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
sideOffset = 4,
...props
}) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"relative z-[100] max-h-(--radix-select-content-available-height) min-w-[var(--radix-select-trigger-width)] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-xl border shadow-2xl",
"bg-popover/80 text-popover-foreground dark:bg-popover/70",
"backdrop-blur-xl backdrop-saturate-[180%]",
"border-border/60",
"ring-1 ring-black/5 dark:ring-white/10",
"shadow-black/5 dark:shadow-black/60",
"animate-in fade-in zoom-in-95 duration-200 ease-out",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:duration-150",
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
sideOffset={sideOffset}
{...props}>
<SelectScrollUpButton className="bg-transparent text-muted-foreground/50" />
<SelectPrimitive.Viewport
className={cn("p-1.5", position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1")}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton className="bg-transparent text-muted-foreground/50" />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props} />
);
}
function SelectItem({
className,
children,
...props
}) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-pointer select-none items-center rounded-lg py-2.5 px-3 text-sm font-medium transition-colors duration-150 outline-none",
"text-foreground",
"hover:bg-primary/10 dark:hover:bg-primary/20",
"focus:bg-primary/10 dark:focus:bg-primary/20",
"data-[highlighted]:bg-primary/10 dark:data-[highlighted]:bg-primary/20",
"data-[state=checked]:bg-primary/10 dark:data-[state=checked]:bg-primary/20",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"*:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}>
<span
data-slot="select-item-indicator"
className="absolute right-3 flex size-4 items-center justify-center text-primary">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border/60", className)}
{...props} />
);
}
function SelectScrollUpButton({
className,
...props
}) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

9
jsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"jsx": "react"
}
}

6
lib/utils.js Normal file
View File

@@ -0,0 +1,6 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

5859
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,14 +20,20 @@
"@supabase/supabase-js": "^2.78.0", "@supabase/supabase-js": "^2.78.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"input-otp": "^1.4.2",
"lodash": "^4.17.23", "lodash": "^4.17.23",
"lucide-react": "^0.577.0",
"next": "^16.1.5", "next": "^16.1.5",
"radix-ui": "^1.4.3",
"react": "18.3.1", "react": "18.3.1",
"react-chartjs-2": "^5.3.1", "react-chartjs-2": "^5.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"tailwind-merge": "^3.5.0",
"tesseract.js": "^5.1.1", "tesseract.js": "^5.1.1",
"uuid": "^13.0.0" "uuid": "^13.0.0"
}, },
@@ -35,11 +41,17 @@
"node": ">=20.9.0" "node": ">=20.9.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.2.1",
"autoprefixer": "^10.4.27",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"eslint-config-next": "^16.1.5", "eslint-config-next": "^16.1.5",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.7" "lint-staged": "^16.2.7",
"postcss": "^8.5.8",
"shadcn": "^3.8.5",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0"
}, },
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx}": [ "*.{js,jsx,ts,tsx}": [

5
postcss.config.mjs Normal file
View File

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