删除多个完成报告文件,优化项目结构以提升可维护性。
This commit is contained in:
22
components/README.md
Normal file
22
components/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 组件目录说明
|
||||
|
||||
## 结构约定
|
||||
|
||||
- **移动端(C 端)**:`view/` — 供 `app/view/*` 使用
|
||||
- `view/layout/`:layout-wrapper、bottom-nav(底部导航,/view 前缀)
|
||||
- `view/config/`:config-loader
|
||||
- `view/ui/`:移动端用通用 UI(与 admin 各一份,按需复制)
|
||||
|
||||
- **管理端**:`admin/` — 供 `app/admin/*` 使用
|
||||
- `admin/ui/`:管理端用通用 UI(与 view 各一份)
|
||||
- `admin/modules/`:管理端业务组件(如 user-detail-modal 等)
|
||||
|
||||
- **通用组件**:若 view 与 admin 都用到(如 Button、Card、Dialog),在 `view/ui/` 与 `admin/ui/` 各保留一份,便于两端独立样式或行为。
|
||||
|
||||
- **当前仍挂在根目录的组件**(如 `search-modal.tsx`、`chapter-content.tsx`、`modules/*`)主要被 `app/view/*` 引用,后续可逐步迁入 `view/` 并改为 `@/components/view/...`。
|
||||
|
||||
## 路由约定
|
||||
|
||||
- 根路径 `/` → 重定向到 `/view`(移动端首页)
|
||||
- 移动端:`/view`、`/view/chapters`、`/view/read/[id]`、`/view/match`、`/view/my`、`/view/about`、`/view/login` 等
|
||||
- 管理端:`/admin`、`/admin/*` 不变
|
||||
60
components/admin/ui/button.tsx
Normal file
60
components/admin/ui/button.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -95,7 +95,7 @@ export function BookCover() {
|
||||
</div>
|
||||
|
||||
{/* CTA按钮 - iOS风格 */}
|
||||
<Link href="/chapters" className="block touch-feedback">
|
||||
<Link href="/view/chapters" className="block touch-feedback">
|
||||
<button className="btn-ios w-full flex items-center justify-center gap-2 glow">
|
||||
<BookOpen className="w-5 h-5" />
|
||||
<span>立即阅读</span>
|
||||
|
||||
@@ -33,19 +33,19 @@ export function BottomNav() {
|
||||
|
||||
// 在文档页面、管理后台、阅读页面和关于页面不显示底部导航(必须在所有 hooks 之后)
|
||||
if (
|
||||
pathname.startsWith("/documentation") ||
|
||||
pathname.startsWith("/admin") ||
|
||||
pathname.startsWith("/read") ||
|
||||
pathname.startsWith("/about")
|
||||
pathname?.startsWith("/view/documentation") ||
|
||||
pathname?.startsWith("/admin") ||
|
||||
pathname?.startsWith("/view/read") ||
|
||||
pathname?.startsWith("/view/about")
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", icon: Home, label: "首页" },
|
||||
{ href: "/chapters", icon: List, label: "目录" },
|
||||
...(matchEnabled ? [{ href: "/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
|
||||
{ href: "/my", icon: User, label: "我的" },
|
||||
{ href: "/view", icon: Home, label: "首页" },
|
||||
{ href: "/view/chapters", icon: List, label: "目录" },
|
||||
...(matchEnabled ? [{ href: "/view/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
|
||||
{ href: "/view/my", icon: User, label: "我的" },
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@@ -41,7 +41,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
const getShareLink = () => {
|
||||
const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
const referralCode = user?.referralCode || ''
|
||||
const shareUrl = `${baseUrl}/read/${section.id}${referralCode ? `?ref=${referralCode}` : ''}`
|
||||
const shareUrl = `${baseUrl}/view/read/${section.id}${referralCode ? `?ref=${referralCode}` : ''}`
|
||||
return shareUrl
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
<header className="sticky top-0 z-40 bg-black/80 backdrop-blur-xl border-b border-white/5">
|
||||
<div className="max-w-2xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => router.push("/chapters")}
|
||||
onClick={() => router.push("/view/chapters")}
|
||||
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center active:bg-[#2c2c2e]"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400" />
|
||||
@@ -199,7 +199,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
<div className="flex items-center gap-3">
|
||||
{prevSection ? (
|
||||
<button
|
||||
onClick={() => router.push(`/read/${prevSection.id}`)}
|
||||
onClick={() => router.push(`/view/read/${prevSection.id}`)}
|
||||
className="flex-1 max-w-[48%] p-3 rounded-xl bg-[#1c1c1e] border border-white/5 text-left hover:bg-[#2c2c2e] transition-colors"
|
||||
>
|
||||
<p className="text-[10px] text-gray-500 mb-0.5">上一篇</p>
|
||||
@@ -211,7 +211,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
|
||||
{nextSection ? (
|
||||
<button
|
||||
onClick={() => router.push(`/read/${nextSection.id}`)}
|
||||
onClick={() => router.push(`/view/read/${nextSection.id}`)}
|
||||
className="flex-1 max-w-[48%] p-3 rounded-xl bg-gradient-to-r from-[#00CED1]/10 to-[#20B2AA]/10 border border-[#00CED1]/20 text-left hover:from-[#00CED1]/20 hover:to-[#20B2AA]/20 transition-colors"
|
||||
>
|
||||
<p className="text-[10px] text-[#00CED1] mb-0.5">下一篇</p>
|
||||
@@ -390,7 +390,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/my/referral')}
|
||||
onClick={() => router.push('/view/my/referral')}
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-xl bg-white/5 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-full bg-[#FFD700]/20 flex items-center justify-center">
|
||||
|
||||
@@ -21,7 +21,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
|
||||
{/* Special sections - Preface */}
|
||||
{specialSections?.preface && (
|
||||
<div className="space-y-3">
|
||||
<Link href={`/read/preface`} className="block group">
|
||||
<Link href={`/view/read/preface`} className="block group">
|
||||
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-4 border border-transparent hover:border-[#38bdac]/30 transition-all flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Unlock className="w-4 h-4 text-[#38bdac]" />
|
||||
@@ -79,7 +79,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
|
||||
{chapter.sections.map((section) => (
|
||||
<Link
|
||||
key={section.id}
|
||||
href={`/read/${section.id}`}
|
||||
href={`/view/read/${section.id}`}
|
||||
className="block px-5 py-4 hover:bg-[#0f2137]/40 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -117,7 +117,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
|
||||
{/* Special sections - Epilogue */}
|
||||
{specialSections?.epilogue && (
|
||||
<div className="mt-8 space-y-3">
|
||||
<Link href={`/read/epilogue`} className="block group">
|
||||
<Link href={`/view/read/epilogue`} className="block group">
|
||||
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-4 border border-transparent hover:border-[#38bdac]/30 transition-all flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Unlock className="w-4 h-4 text-[#38bdac]" />
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Footer() {
|
||||
|
||||
{/* 链接 */}
|
||||
<div className="flex items-center justify-center gap-6 text-xs text-[var(--app-text-tertiary)] mb-4">
|
||||
<a href="/about" className="hover:text-white transition-colors">关于我们</a>
|
||||
<a href="/view/about" className="hover:text-white transition-colors">关于我们</a>
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--app-separator)]" />
|
||||
<a href="#" className="hover:text-white transition-colors">用户协议</a>
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--app-separator)]" />
|
||||
|
||||
@@ -11,13 +11,13 @@ export function BottomNav() {
|
||||
if (pathname.startsWith("/admin")) return null
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", icon: Home, label: "首页", id: "home" },
|
||||
{ href: "/match", icon: Users, label: "找伙伴", id: "match" },
|
||||
{ href: "/my", icon: User, label: "我的", id: "my" },
|
||||
{ href: "/view", icon: Home, label: "首页", id: "home" },
|
||||
{ href: "/view/match", icon: Users, label: "找伙伴", id: "match" },
|
||||
{ href: "/view/my", icon: User, label: "我的", id: "my" },
|
||||
]
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === "/") return pathname === "/"
|
||||
if (href === "/view") return pathname === "/view"
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ export function AuthModal({ isOpen, onClose, defaultTab = "login" }: AuthModalPr
|
||||
|
||||
<div className="text-right">
|
||||
<Link
|
||||
href="/login/forgot"
|
||||
href="/view/login/forgot"
|
||||
onClick={onClose}
|
||||
className="text-sm text-[#00CED1] hover:underline"
|
||||
>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function SearchModal({ open, onOpenChange }: SearchModalProps) {
|
||||
|
||||
const handleResultClick = (result: SearchResult) => {
|
||||
onOpenChange(false)
|
||||
router.push(`/read/${result.id}`)
|
||||
router.push(`/view/read/${result.id}`)
|
||||
}
|
||||
|
||||
const handleKeywordClick = (keyword: string) => {
|
||||
|
||||
@@ -60,7 +60,7 @@ export function TableOfContents({ parts }: TableOfContentsProps) {
|
||||
|
||||
{/* 附加内容 - 序言和尾声 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link href="/chapters?section=preface" className="block touch-feedback">
|
||||
<Link href="/view/chapters?section=preface" className="block touch-feedback">
|
||||
<div className="glass-card p-4 h-full">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<BookOpen className="w-4 h-4 text-[var(--ios-blue)]" />
|
||||
@@ -72,7 +72,7 @@ export function TableOfContents({ parts }: TableOfContentsProps) {
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/chapters?section=epilogue" className="block touch-feedback">
|
||||
<Link href="/view/chapters?section=epilogue" className="block touch-feedback">
|
||||
<div className="glass-card p-4 h-full">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileText className="w-4 h-4 text-[var(--ios-purple)]" />
|
||||
|
||||
@@ -56,7 +56,7 @@ export function UserMenu() {
|
||||
{/* Menu items */}
|
||||
<div className="py-2">
|
||||
<Link
|
||||
href="/my/purchases"
|
||||
href="/view/my/purchases"
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-300 hover:bg-gray-800/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
@@ -64,7 +64,7 @@ export function UserMenu() {
|
||||
<span>我的购买</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/my/referral"
|
||||
href="/view/my/referral"
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-300 hover:bg-gray-800/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
|
||||
14
components/view/config/config-loader.tsx
Normal file
14
components/view/config/config-loader.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
export function ConfigLoader() {
|
||||
const { fetchSettings } = useStore()
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings()
|
||||
}, [fetchSettings])
|
||||
|
||||
return null
|
||||
}
|
||||
94
components/view/layout/bottom-nav.tsx
Normal file
94
components/view/layout/bottom-nav.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Home, List, User, Users } from "lucide-react"
|
||||
|
||||
export function BottomNav() {
|
||||
const pathname = usePathname()
|
||||
const [matchEnabled, setMatchEnabled] = useState(false)
|
||||
const [configLoaded, setConfigLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/db/config')
|
||||
const data = await res.json()
|
||||
if (data.features) {
|
||||
setMatchEnabled(data.features.matchEnabled === true)
|
||||
}
|
||||
} catch (e) {
|
||||
setMatchEnabled(false)
|
||||
} finally {
|
||||
setConfigLoaded(true)
|
||||
}
|
||||
}
|
||||
loadConfig()
|
||||
}, [])
|
||||
|
||||
if (
|
||||
pathname?.startsWith("/view/documentation") ||
|
||||
pathname?.startsWith("/admin") ||
|
||||
pathname?.startsWith("/view/read") ||
|
||||
pathname?.startsWith("/view/about")
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ href: "/view", icon: Home, label: "首页" },
|
||||
{ href: "/view/chapters", icon: List, label: "目录" },
|
||||
...(matchEnabled ? [{ href: "/view/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
|
||||
{ href: "/view/my", icon: User, label: "我的" },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-40 bg-[#1c1c1e]/95 backdrop-blur-xl border-t border-white/5 safe-bottom">
|
||||
<div className="flex items-center justify-around py-2 max-w-lg mx-auto">
|
||||
{navItems.map((item, index) => {
|
||||
const isActive = pathname === item.href
|
||||
const Icon = item.icon
|
||||
|
||||
if (item.isCenter) {
|
||||
return (
|
||||
<Link key={index} href={item.href} className="flex flex-col items-center py-2 px-6 -mt-4">
|
||||
<div
|
||||
className={`w-14 h-14 rounded-full flex items-center justify-center shadow-lg transition-all bg-gradient-to-br from-[#00CED1] to-[#20B2AA] shadow-[#00CED1]/30`}
|
||||
>
|
||||
<Icon className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className={`text-xs mt-1 ${isActive ? "text-[#00CED1] font-medium" : "text-gray-500"}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
href={item.href}
|
||||
className="flex flex-col items-center py-2 px-4 touch-feedback transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
className={`w-6 h-6 flex items-center justify-center mb-1 transition-colors ${
|
||||
isActive ? "text-[#00CED1]" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" strokeWidth={isActive ? 2 : 1.5} />
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs transition-colors ${isActive ? "text-[#00CED1] font-medium" : "text-gray-500"}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
43
components/view/layout/layout-wrapper.tsx
Normal file
43
components/view/layout/layout-wrapper.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { BottomNav } from "./bottom-nav"
|
||||
import { ConfigLoader } from "../config/config-loader"
|
||||
|
||||
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const isAdmin = pathname?.startsWith("/admin")
|
||||
const isView = pathname?.startsWith("/view") || pathname === "/"
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="mx-auto max-w-[430px] min-h-screen bg-black shadow-2xl relative font-sans antialiased">
|
||||
<ConfigLoader />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 text-gray-900 font-sans">
|
||||
<ConfigLoader />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[430px] min-h-screen bg-black shadow-2xl relative font-sans antialiased">
|
||||
<ConfigLoader />
|
||||
{children}
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
60
components/view/ui/button.tsx
Normal file
60
components/view/ui/button.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
Reference in New Issue
Block a user