17 changed files with 14195 additions and 6573 deletions
@ -0,0 +1,6 @@ |
|||||||
|
module.exports = { |
||||||
|
plugins: { |
||||||
|
tailwindcss: {}, |
||||||
|
autoprefixer: {}, |
||||||
|
}, |
||||||
|
} |
||||||
@ -1,14 +1,32 @@ |
|||||||
import { CopilotRuntime, OpenAIAdapter } from "@copilotkit/backend"; |
import { |
||||||
import OpenAI from "openai"; |
CopilotRuntime, |
||||||
|
GoogleGenerativeAIAdapter, |
||||||
|
copilotRuntimeNextJSAppRouterEndpoint |
||||||
|
} from "@copilotkit/runtime"; |
||||||
|
import { GoogleGenerativeAI } from "@google/generative-ai"; |
||||||
|
|
||||||
// 将您的 API Key 放在同级目录或应用根目录的 .env 文件中
|
export const POST = async (req: Request) => { |
||||||
export const runtime = 'edge'; |
// 1. 初始化 Google Gemini 客户端
|
||||||
|
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY || ""); |
||||||
|
|
||||||
export async function POST(req: Request) { |
// 2. 选择模型 (使用 3.1 Pro)
|
||||||
// 如果环境变量未就绪,使用默认的容错模式(要求客户稍后补齐)
|
const model = genAI.getGenerativeModel({
|
||||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY || "dummy-key-for-local-test" }); |
model: "gemini-3.1-pro", |
||||||
|
// 如果需要启用思维模型等高级参数,可以在这里或 adapter 层级配置
|
||||||
|
}); |
||||||
|
|
||||||
const copilotKit = new CopilotRuntime(); |
const runtime = new CopilotRuntime(); |
||||||
|
|
||||||
return copilotKit.response(req, new OpenAIAdapter({ openai })); |
// 3. 使用 GoogleGenerativeAIAdapter
|
||||||
} |
const serviceAdapter = new GoogleGenerativeAIAdapter({ |
||||||
|
model: "gemini-1.5-pro" |
||||||
|
}); |
||||||
|
|
||||||
|
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ |
||||||
|
runtime, |
||||||
|
serviceAdapter, |
||||||
|
endpoint: "/api/copilotkit", |
||||||
|
}); |
||||||
|
|
||||||
|
return handleRequest(req); |
||||||
|
}; |
||||||
@ -0,0 +1,224 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import { useEffect, useState, use } from "react"; |
||||||
|
import { |
||||||
|
ColumnDef, |
||||||
|
flexRender, |
||||||
|
getCoreRowModel, |
||||||
|
useReactTable, |
||||||
|
getPaginationRowModel, |
||||||
|
getSortedRowModel, |
||||||
|
SortingState, |
||||||
|
} from "@tanstack/react-table"; |
||||||
|
import { |
||||||
|
Table, |
||||||
|
TableBody, |
||||||
|
TableCell, |
||||||
|
TableHead, |
||||||
|
TableHeader, |
||||||
|
TableRow, |
||||||
|
} from "@/components/ui/table"; |
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
|
||||||
|
interface DataPageProps { |
||||||
|
params: Promise<{ |
||||||
|
entityCode: string; |
||||||
|
}>; |
||||||
|
} |
||||||
|
|
||||||
|
export default function EntityDataPage({ params: paramsPromise }: DataPageProps) { |
||||||
|
// Use React.use() to unwrap Next.js 15+ dynamic params Promise
|
||||||
|
const params = use(paramsPromise); |
||||||
|
const { entityCode } = params; |
||||||
|
|
||||||
|
const [entity, setEntity] = useState<any>(null); |
||||||
|
const [data, setData] = useState<any[]>([]); |
||||||
|
const [loading, setLoading] = useState(true); |
||||||
|
const [sorting, setSorting] = useState<SortingState>([]); |
||||||
|
const [columns, setColumns] = useState<ColumnDef<any>[]>([]); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
async function loadData() { |
||||||
|
setLoading(true); |
||||||
|
try { |
||||||
|
const [entityRes, dataRes] = await Promise.all([ |
||||||
|
fetch(`http://localhost:8080/api/entities/code/${entityCode}`), |
||||||
|
fetch(`http://localhost:8080/api/dynamic/code/${entityCode}`) |
||||||
|
]); |
||||||
|
|
||||||
|
if (entityRes.ok && dataRes.ok) { |
||||||
|
const entityData = await entityRes.json(); |
||||||
|
const records = await dataRes.json(); |
||||||
|
setEntity(entityData); |
||||||
|
|
||||||
|
// Map backend entities to flat row format
|
||||||
|
const mappedData = records.map((r: any) => { |
||||||
|
const parsedData = (typeof r.data === 'string') ? JSON.parse(r.data) : r.data; |
||||||
|
return { |
||||||
|
_id: r.id,
|
||||||
|
_createdAt: r.createdAt,
|
||||||
|
...parsedData
|
||||||
|
}; |
||||||
|
}); |
||||||
|
setData(mappedData); |
||||||
|
|
||||||
|
// Build generic columns directly from Schema
|
||||||
|
const dynamicColumns: ColumnDef<any>[] = []; |
||||||
|
|
||||||
|
if (entityData && entityData.schemaDefinition) { |
||||||
|
try { |
||||||
|
const schema = JSON.parse(entityData.schemaDefinition); |
||||||
|
if (schema.properties) { |
||||||
|
Object.keys(schema.properties).forEach((key) => { |
||||||
|
const prop = schema.properties[key]; |
||||||
|
dynamicColumns.push({ |
||||||
|
accessorKey: key, |
||||||
|
header: prop.title || key, |
||||||
|
cell: ({ row }) => { |
||||||
|
const cellValue = row.getValue(key); |
||||||
|
return cellValue !== undefined && cellValue !== null ? String(cellValue) : "-"; |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
} catch(e) {
|
||||||
|
console.error("Schema Parsing Error:", e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// System column created at
|
||||||
|
dynamicColumns.push({ |
||||||
|
accessorKey: "_createdAt", |
||||||
|
header: "创建时间", |
||||||
|
cell: ({ row }) => { |
||||||
|
const val: string = row.getValue("_createdAt"); |
||||||
|
return val ? new Date(val).toLocaleString() : "-"; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
setColumns(dynamicColumns); |
||||||
|
} else { |
||||||
|
console.error("Failed to fetch entity or data"); |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
console.error("Error loading table data:", err); |
||||||
|
} finally { |
||||||
|
setLoading(false); |
||||||
|
} |
||||||
|
} |
||||||
|
loadData(); |
||||||
|
}, [entityCode]); |
||||||
|
|
||||||
|
const table = useReactTable({ |
||||||
|
data, |
||||||
|
columns, |
||||||
|
getCoreRowModel: getCoreRowModel(), |
||||||
|
getPaginationRowModel: getPaginationRowModel(), |
||||||
|
getSortedRowModel: getSortedRowModel(), |
||||||
|
onSortingChange: setSorting, |
||||||
|
state: { |
||||||
|
sorting, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
if (loading) { |
||||||
|
return ( |
||||||
|
<div className="flex h-full w-full items-center justify-center bg-slate-50"> |
||||||
|
<div className="flex flex-col items-center gap-4"> |
||||||
|
<span className="animate-spin text-4xl text-indigo-500">❖</span> |
||||||
|
<p className="text-slate-500 font-bold tracking-widest animate-pulse">LOADING DYNAMIC RECORDS</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (!entity) return <div className="p-8 text-rose-500 font-bold">实体配置未找到或服务不可用。</div>; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="p-8 md:p-12 max-w-7xl mx-auto flex flex-col h-full gap-8 max-h-screen"> |
||||||
|
<div className="flex flex-col gap-2 border-b border-slate-200 pb-6 shrink-0"> |
||||||
|
<h1 className="text-4xl font-black text-slate-800 tracking-tight flex items-center gap-3"> |
||||||
|
<span className="p-3 bg-gradient-to-br from-indigo-50 to-indigo-100/50 text-indigo-600 rounded-2xl shadow-sm border border-indigo-100">📋</span>
|
||||||
|
{entity.entityName} |
||||||
|
</h1> |
||||||
|
<p className="text-slate-500 font-medium">模块标识: <code className="bg-slate-100 border border-slate-200 px-2 py-0.5 rounded text-sm text-slate-700 mx-2">{entity.entityCode}</code> <span className="text-sm">系统自动映射的 JSONB 动态持久化视图</span></p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="rounded-2xl border border-slate-200 bg-white shadow-xl shadow-slate-200/40 flex-1 flex flex-col overflow-hidden relative"> |
||||||
|
<div className="flex-1 overflow-auto bg-slate-50/30"> |
||||||
|
<Table> |
||||||
|
<TableHeader className="bg-slate-100/80 sticky top-0 z-10 backdrop-blur-md shadow-sm"> |
||||||
|
{table.getHeaderGroups().map((headerGroup) => ( |
||||||
|
<TableRow key={headerGroup.id} className="border-b-slate-200 hover:bg-transparent"> |
||||||
|
{headerGroup.headers.map((header) => ( |
||||||
|
<TableHead key={header.id} className="font-bold text-slate-600 whitespace-nowrap h-12 uppercase text-xs tracking-wider"> |
||||||
|
{header.isPlaceholder |
||||||
|
? null |
||||||
|
: flexRender( |
||||||
|
header.column.columnDef.header, |
||||||
|
header.getContext() |
||||||
|
)} |
||||||
|
</TableHead> |
||||||
|
))} |
||||||
|
</TableRow> |
||||||
|
))} |
||||||
|
</TableHeader> |
||||||
|
<TableBody> |
||||||
|
{table.getRowModel().rows?.length ? ( |
||||||
|
table.getRowModel().rows.map((row) => ( |
||||||
|
<TableRow |
||||||
|
key={row.id} |
||||||
|
data-state={row.getIsSelected() && "selected"} |
||||||
|
className="hover:bg-indigo-50/50 border-b-slate-100 transition-colors" |
||||||
|
> |
||||||
|
{row.getVisibleCells().map((cell) => ( |
||||||
|
<TableCell key={cell.id} className="whitespace-nowrap py-4 font-medium text-slate-800"> |
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())} |
||||||
|
</TableCell> |
||||||
|
))} |
||||||
|
</TableRow> |
||||||
|
)) |
||||||
|
) : ( |
||||||
|
<TableRow> |
||||||
|
<TableCell colSpan={columns.length} className="h-64 text-center"> |
||||||
|
<div className="flex flex-col items-center justify-center gap-2 opacity-60"> |
||||||
|
<span className="text-4xl mb-2">🎈</span> |
||||||
|
<p className="font-bold text-slate-500 text-lg">暂无数据落库记录</p> |
||||||
|
<p className="text-sm text-slate-400">目前没有任何 JSONB 数据。请到首页对 AI 输入需求填充表单。</p> |
||||||
|
</div> |
||||||
|
</TableCell> |
||||||
|
</TableRow> |
||||||
|
)} |
||||||
|
</TableBody> |
||||||
|
</Table> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-4 px-6 border-t border-slate-200 bg-white shrink-0"> |
||||||
|
<div className="text-sm font-medium text-slate-500"> |
||||||
|
总计解析并挂载 <span className="font-bold text-indigo-600">{data.length}</span> 条动态记录 |
||||||
|
</div> |
||||||
|
<div className="flex gap-2"> |
||||||
|
<Button |
||||||
|
variant="outline" |
||||||
|
size="sm" |
||||||
|
onClick={() => table.previousPage()} |
||||||
|
disabled={!table.getCanPreviousPage()} |
||||||
|
className="rounded-xl font-bold shadow-sm" |
||||||
|
> |
||||||
|
上页 |
||||||
|
</Button> |
||||||
|
<Button |
||||||
|
variant="outline" |
||||||
|
size="sm" |
||||||
|
onClick={() => table.nextPage()} |
||||||
|
disabled={!table.getCanNextPage()} |
||||||
|
className="rounded-xl font-bold shadow-sm" |
||||||
|
> |
||||||
|
下页 |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -1,130 +1,59 @@ |
|||||||
@import "tailwindcss"; |
@tailwind base; |
||||||
@import "tw-animate-css"; |
@tailwind components; |
||||||
@import "shadcn/tailwind.css"; |
@tailwind utilities; |
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *)); |
@layer base { |
||||||
|
:root { |
||||||
@theme inline { |
--background: 0 0% 100%; |
||||||
--color-background: var(--background); |
--foreground: 222.2 84% 4.9%; |
||||||
--color-foreground: var(--foreground); |
--card: 0 0% 100%; |
||||||
--font-sans: var(--font-sans); |
--card-foreground: 222.2 84% 4.9%; |
||||||
--font-mono: var(--font-geist-mono); |
--popover: 0 0% 100%; |
||||||
--font-heading: var(--font-sans); |
--popover-foreground: 222.2 84% 4.9%; |
||||||
--color-sidebar-ring: var(--sidebar-ring); |
--primary: 221.2 83.2% 53.3%; |
||||||
--color-sidebar-border: var(--sidebar-border); |
--primary-foreground: 210 40% 98%; |
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); |
--secondary: 210 40% 96.1%; |
||||||
--color-sidebar-accent: var(--sidebar-accent); |
--secondary-foreground: 222.2 47.4% 11.2%; |
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); |
--muted: 210 40% 96.1%; |
||||||
--color-sidebar-primary: var(--sidebar-primary); |
--muted-foreground: 215.4 16.3% 46.9%; |
||||||
--color-sidebar-foreground: var(--sidebar-foreground); |
--accent: 210 40% 96.1%; |
||||||
--color-sidebar: var(--sidebar); |
--accent-foreground: 222.2 47.4% 11.2%; |
||||||
--color-chart-5: var(--chart-5); |
--destructive: 0 84.2% 60.2%; |
||||||
--color-chart-4: var(--chart-4); |
--destructive-foreground: 210 40% 98%; |
||||||
--color-chart-3: var(--chart-3); |
--border: 214.3 31.8% 91.4%; |
||||||
--color-chart-2: var(--chart-2); |
--input: 214.3 31.8% 91.4%; |
||||||
--color-chart-1: var(--chart-1); |
--ring: 221.2 83.2% 53.3%; |
||||||
--color-ring: var(--ring); |
--radius: 0.5rem; |
||||||
--color-input: var(--input); |
} |
||||||
--color-border: var(--border); |
|
||||||
--color-destructive: var(--destructive); |
|
||||||
--color-accent-foreground: var(--accent-foreground); |
|
||||||
--color-accent: var(--accent); |
|
||||||
--color-muted-foreground: var(--muted-foreground); |
|
||||||
--color-muted: var(--muted); |
|
||||||
--color-secondary-foreground: var(--secondary-foreground); |
|
||||||
--color-secondary: var(--secondary); |
|
||||||
--color-primary-foreground: var(--primary-foreground); |
|
||||||
--color-primary: var(--primary); |
|
||||||
--color-popover-foreground: var(--popover-foreground); |
|
||||||
--color-popover: var(--popover); |
|
||||||
--color-card-foreground: var(--card-foreground); |
|
||||||
--color-card: var(--card); |
|
||||||
--radius-sm: calc(var(--radius) * 0.6); |
|
||||||
--radius-md: calc(var(--radius) * 0.8); |
|
||||||
--radius-lg: var(--radius); |
|
||||||
--radius-xl: calc(var(--radius) * 1.4); |
|
||||||
--radius-2xl: calc(var(--radius) * 1.8); |
|
||||||
--radius-3xl: calc(var(--radius) * 2.2); |
|
||||||
--radius-4xl: calc(var(--radius) * 2.6); |
|
||||||
} |
|
||||||
|
|
||||||
:root { |
|
||||||
--background: oklch(1 0 0); |
|
||||||
--foreground: oklch(0.145 0 0); |
|
||||||
--card: oklch(1 0 0); |
|
||||||
--card-foreground: oklch(0.145 0 0); |
|
||||||
--popover: oklch(1 0 0); |
|
||||||
--popover-foreground: oklch(0.145 0 0); |
|
||||||
--primary: oklch(0.205 0 0); |
|
||||||
--primary-foreground: oklch(0.985 0 0); |
|
||||||
--secondary: oklch(0.97 0 0); |
|
||||||
--secondary-foreground: oklch(0.205 0 0); |
|
||||||
--muted: oklch(0.97 0 0); |
|
||||||
--muted-foreground: oklch(0.556 0 0); |
|
||||||
--accent: oklch(0.97 0 0); |
|
||||||
--accent-foreground: oklch(0.205 0 0); |
|
||||||
--destructive: oklch(0.577 0.245 27.325); |
|
||||||
--border: oklch(0.922 0 0); |
|
||||||
--input: oklch(0.922 0 0); |
|
||||||
--ring: oklch(0.708 0 0); |
|
||||||
--chart-1: oklch(0.87 0 0); |
|
||||||
--chart-2: oklch(0.556 0 0); |
|
||||||
--chart-3: oklch(0.439 0 0); |
|
||||||
--chart-4: oklch(0.371 0 0); |
|
||||||
--chart-5: oklch(0.269 0 0); |
|
||||||
--radius: 0.625rem; |
|
||||||
--sidebar: oklch(0.985 0 0); |
|
||||||
--sidebar-foreground: oklch(0.145 0 0); |
|
||||||
--sidebar-primary: oklch(0.205 0 0); |
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0); |
|
||||||
--sidebar-accent: oklch(0.97 0 0); |
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0); |
|
||||||
--sidebar-border: oklch(0.922 0 0); |
|
||||||
--sidebar-ring: oklch(0.708 0 0); |
|
||||||
} |
|
||||||
|
|
||||||
.dark { |
.dark { |
||||||
--background: oklch(0.145 0 0); |
--background: 222.2 84% 4.9%; |
||||||
--foreground: oklch(0.985 0 0); |
--foreground: 210 40% 98%; |
||||||
--card: oklch(0.205 0 0); |
--card: 222.2 84% 4.9%; |
||||||
--card-foreground: oklch(0.985 0 0); |
--card-foreground: 210 40% 98%; |
||||||
--popover: oklch(0.205 0 0); |
--popover: 222.2 84% 4.9%; |
||||||
--popover-foreground: oklch(0.985 0 0); |
--popover-foreground: 210 40% 98%; |
||||||
--primary: oklch(0.922 0 0); |
--primary: 217.2 91.2% 59.8%; |
||||||
--primary-foreground: oklch(0.205 0 0); |
--primary-foreground: 222.2 47.4% 11.2%; |
||||||
--secondary: oklch(0.269 0 0); |
--secondary: 217.2 32.6% 17.5%; |
||||||
--secondary-foreground: oklch(0.985 0 0); |
--secondary-foreground: 210 40% 98%; |
||||||
--muted: oklch(0.269 0 0); |
--muted: 217.2 32.6% 17.5%; |
||||||
--muted-foreground: oklch(0.708 0 0); |
--muted-foreground: 215 20.2% 65.1%; |
||||||
--accent: oklch(0.269 0 0); |
--accent: 217.2 32.6% 17.5%; |
||||||
--accent-foreground: oklch(0.985 0 0); |
--accent-foreground: 210 40% 98%; |
||||||
--destructive: oklch(0.704 0.191 22.216); |
--destructive: 0 62.8% 30.6%; |
||||||
--border: oklch(1 0 0 / 10%); |
--destructive-foreground: 210 40% 98%; |
||||||
--input: oklch(1 0 0 / 15%); |
--border: 217.2 32.6% 17.5%; |
||||||
--ring: oklch(0.556 0 0); |
--input: 217.2 32.6% 17.5%; |
||||||
--chart-1: oklch(0.87 0 0); |
--ring: 224.3 76.3% 48%; |
||||||
--chart-2: oklch(0.556 0 0); |
} |
||||||
--chart-3: oklch(0.439 0 0); |
|
||||||
--chart-4: oklch(0.371 0 0); |
|
||||||
--chart-5: oklch(0.269 0 0); |
|
||||||
--sidebar: oklch(0.205 0 0); |
|
||||||
--sidebar-foreground: oklch(0.985 0 0); |
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376); |
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0); |
|
||||||
--sidebar-accent: oklch(0.269 0 0); |
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0); |
|
||||||
--sidebar-border: oklch(1 0 0 / 10%); |
|
||||||
--sidebar-ring: oklch(0.556 0 0); |
|
||||||
} |
} |
||||||
|
|
||||||
@layer base { |
@layer base { |
||||||
* { |
* { |
||||||
@apply border-border outline-ring/50; |
@apply border-border; |
||||||
} |
} |
||||||
body { |
body { |
||||||
@apply bg-background text-foreground; |
@apply bg-background text-foreground; |
||||||
} |
} |
||||||
html { |
|
||||||
@apply font-sans; |
|
||||||
} |
|
||||||
} |
} |
||||||
@ -0,0 +1,85 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import { useEffect, useState } from "react"; |
||||||
|
import Link from "next/link"; |
||||||
|
import { usePathname } from "next/navigation"; |
||||||
|
|
||||||
|
export default function Sidebar() { |
||||||
|
const [entities, setEntities] = useState<any[]>([]); |
||||||
|
const [loading, setLoading] = useState(true); |
||||||
|
const pathname = usePathname(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
// Asynchronously fetch from API
|
||||||
|
fetch("http://localhost:8080/api/entities") |
||||||
|
.then(res => res.json()) |
||||||
|
.then(data => { |
||||||
|
setEntities(data); |
||||||
|
setLoading(false); |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
console.error("Failed to fetch entities", err); |
||||||
|
setLoading(false); |
||||||
|
}); |
||||||
|
}, []); |
||||||
|
|
||||||
|
return ( |
||||||
|
<aside className="w-72 bg-slate-900 border-r border-slate-800 text-slate-300 flex flex-col h-full shrink-0 shadow-2xl transition-all relative z-10"> |
||||||
|
<div className="p-8 border-b border-slate-800/50 flex items-center gap-4"> |
||||||
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-cyan-500 flex items-center justify-center shadow-lg shadow-indigo-500/20"> |
||||||
|
<span className="text-white text-xl font-black">AI</span> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<h2 className="text-xl font-black tracking-tight text-white">Copilot CRM</h2> |
||||||
|
<p className="text-xs text-slate-500 font-medium">智能生成架构</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<nav className="flex-1 overflow-y-auto p-4 space-y-2"> |
||||||
|
<Link href="/" className={`flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 font-bold ${pathname === '/' ? 'bg-indigo-600/10 text-indigo-400' : 'text-slate-400 hover:bg-slate-800 hover:text-slate-200'}`}> |
||||||
|
<span>🏠</span> <span>大盘与生成</span> |
||||||
|
</Link> |
||||||
|
|
||||||
|
<div className="pt-8 pb-3 px-4 text-xs font-black text-slate-600 uppercase tracking-widest flex items-center justify-between"> |
||||||
|
<span>您的动态表单</span> |
||||||
|
{loading && <span className="animate-spin text-indigo-400">❖</span>} |
||||||
|
</div> |
||||||
|
|
||||||
|
{!loading && entities.length === 0 ? ( |
||||||
|
<div className="px-4 py-8 text-sm text-slate-500 text-center bg-slate-800/20 rounded-2xl border border-slate-800/50 border-dashed"> |
||||||
|
尚未生成任何模块<br/><span className="text-xs mt-2 block">试着跟右下角 AI 说说话</span> |
||||||
|
</div> |
||||||
|
) : ( |
||||||
|
entities.map((ent, idx) => { |
||||||
|
const href = `/data/${ent.entityCode}`; |
||||||
|
const isActive = pathname === href; |
||||||
|
return ( |
||||||
|
<Link
|
||||||
|
key={ent.entityCode || idx}
|
||||||
|
href={href}
|
||||||
|
className={`flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 font-medium ${isActive ? 'bg-gradient-to-r from-indigo-500 to-indigo-600 text-white shadow-xl shadow-indigo-500/20 translate-x-1' : 'text-slate-400 hover:bg-slate-800 hover:text-slate-200'}`} |
||||||
|
> |
||||||
|
<span>✦</span>
|
||||||
|
<span className="truncate">{ent.entityName}</span> |
||||||
|
</Link> |
||||||
|
); |
||||||
|
}) |
||||||
|
)} |
||||||
|
</nav> |
||||||
|
|
||||||
|
<div className="p-6 border-t border-slate-800/50 relative overflow-hidden"> |
||||||
|
<div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-bl-full -z-10 blur-xl"></div> |
||||||
|
<div className="flex items-center gap-3"> |
||||||
|
<div className="w-10 h-10 rounded-full bg-slate-800 border-2 border-slate-700 flex items-center justify-center font-bold text-white relative"> |
||||||
|
AD |
||||||
|
<div className="absolute bottom-0 right-0 w-3 h-3 bg-green-400 rounded-full border-2 border-slate-900"></div> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<p className="text-sm font-bold text-white uppercase tracking-wider">ADMIN 1</p> |
||||||
|
<p className="text-xs text-slate-500">超级管理员租户</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</aside> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
import * as React from "react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
function Card({ |
||||||
|
className, |
||||||
|
size = "default", |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
data-slot="card" |
||||||
|
data-size={size} |
||||||
|
className={cn( |
||||||
|
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
data-slot="card-header" |
||||||
|
className={cn( |
||||||
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
data-slot="card-title" |
||||||
|
className={cn( |
||||||
|
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
data-slot="card-description" |
||||||
|
className={cn("text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
data-slot="card-action" |
||||||
|
className={cn( |
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
data-slot="card-content" |
||||||
|
className={cn("px-4 group-data-[size=sm]/card:px-3", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
data-slot="card-footer" |
||||||
|
className={cn( |
||||||
|
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export { |
||||||
|
Card, |
||||||
|
CardHeader, |
||||||
|
CardFooter, |
||||||
|
CardTitle, |
||||||
|
CardAction, |
||||||
|
CardDescription, |
||||||
|
CardContent, |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
import * as React from "react" |
||||||
|
import { Input as InputPrimitive } from "@base-ui/react/input" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) { |
||||||
|
return ( |
||||||
|
<InputPrimitive |
||||||
|
type={type} |
||||||
|
data-slot="input" |
||||||
|
className={cn( |
||||||
|
"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", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export { Input } |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
function Label({ className, ...props }: React.ComponentProps<"label">) { |
||||||
|
return ( |
||||||
|
<label |
||||||
|
data-slot="label" |
||||||
|
className={cn( |
||||||
|
"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", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export { Label } |
||||||
@ -0,0 +1,201 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
import { Select as SelectPrimitive } from "@base-ui/react/select" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" |
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root |
||||||
|
|
||||||
|
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.Group |
||||||
|
data-slot="select-group" |
||||||
|
className={cn("scroll-my-1 p-1", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.Value |
||||||
|
data-slot="select-value" |
||||||
|
className={cn("flex flex-1 text-left", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function SelectTrigger({ |
||||||
|
className, |
||||||
|
size = "default", |
||||||
|
children, |
||||||
|
...props |
||||||
|
}: SelectPrimitive.Trigger.Props & { |
||||||
|
size?: "sm" | "default" |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.Trigger |
||||||
|
data-slot="select-trigger" |
||||||
|
data-size={size} |
||||||
|
className={cn( |
||||||
|
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
<SelectPrimitive.Icon |
||||||
|
render={ |
||||||
|
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" /> |
||||||
|
} |
||||||
|
/> |
||||||
|
</SelectPrimitive.Trigger> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function SelectContent({ |
||||||
|
className, |
||||||
|
children, |
||||||
|
side = "bottom", |
||||||
|
sideOffset = 4, |
||||||
|
align = "center", |
||||||
|
alignOffset = 0, |
||||||
|
alignItemWithTrigger = true, |
||||||
|
...props |
||||||
|
}: SelectPrimitive.Popup.Props & |
||||||
|
Pick< |
||||||
|
SelectPrimitive.Positioner.Props, |
||||||
|
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" |
||||||
|
>) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.Portal> |
||||||
|
<SelectPrimitive.Positioner |
||||||
|
side={side} |
||||||
|
sideOffset={sideOffset} |
||||||
|
align={align} |
||||||
|
alignOffset={alignOffset} |
||||||
|
alignItemWithTrigger={alignItemWithTrigger} |
||||||
|
className="isolate z-50" |
||||||
|
> |
||||||
|
<SelectPrimitive.Popup |
||||||
|
data-slot="select-content" |
||||||
|
data-align-trigger={alignItemWithTrigger} |
||||||
|
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<SelectScrollUpButton /> |
||||||
|
<SelectPrimitive.List>{children}</SelectPrimitive.List> |
||||||
|
<SelectScrollDownButton /> |
||||||
|
</SelectPrimitive.Popup> |
||||||
|
</SelectPrimitive.Positioner> |
||||||
|
</SelectPrimitive.Portal> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function SelectLabel({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: SelectPrimitive.GroupLabel.Props) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.GroupLabel |
||||||
|
data-slot="select-label" |
||||||
|
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function SelectItem({ |
||||||
|
className, |
||||||
|
children, |
||||||
|
...props |
||||||
|
}: SelectPrimitive.Item.Props) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.Item |
||||||
|
data-slot="select-item" |
||||||
|
className={cn( |
||||||
|
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_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} |
||||||
|
> |
||||||
|
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap"> |
||||||
|
{children} |
||||||
|
</SelectPrimitive.ItemText> |
||||||
|
<SelectPrimitive.ItemIndicator |
||||||
|
render={ |
||||||
|
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> |
||||||
|
} |
||||||
|
> |
||||||
|
<CheckIcon className="pointer-events-none" /> |
||||||
|
</SelectPrimitive.ItemIndicator> |
||||||
|
</SelectPrimitive.Item> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function SelectSeparator({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: SelectPrimitive.Separator.Props) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.Separator |
||||||
|
data-slot="select-separator" |
||||||
|
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function SelectScrollUpButton({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.ScrollUpArrow |
||||||
|
data-slot="select-scroll-up-button" |
||||||
|
className={cn( |
||||||
|
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ChevronUpIcon |
||||||
|
/> |
||||||
|
</SelectPrimitive.ScrollUpArrow> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function SelectScrollDownButton({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) { |
||||||
|
return ( |
||||||
|
<SelectPrimitive.ScrollDownArrow |
||||||
|
data-slot="select-scroll-down-button" |
||||||
|
className={cn( |
||||||
|
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
<ChevronDownIcon |
||||||
|
/> |
||||||
|
</SelectPrimitive.ScrollDownArrow> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export { |
||||||
|
Select, |
||||||
|
SelectContent, |
||||||
|
SelectGroup, |
||||||
|
SelectItem, |
||||||
|
SelectLabel, |
||||||
|
SelectScrollDownButton, |
||||||
|
SelectScrollUpButton, |
||||||
|
SelectSeparator, |
||||||
|
SelectTrigger, |
||||||
|
SelectValue, |
||||||
|
} |
||||||
@ -0,0 +1,116 @@ |
|||||||
|
"use client" |
||||||
|
|
||||||
|
import * as React from "react" |
||||||
|
|
||||||
|
import { cn } from "@/lib/utils" |
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
data-slot="table-container" |
||||||
|
className="relative w-full overflow-x-auto" |
||||||
|
> |
||||||
|
<table |
||||||
|
data-slot="table" |
||||||
|
className={cn("w-full caption-bottom text-sm", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { |
||||||
|
return ( |
||||||
|
<thead |
||||||
|
data-slot="table-header" |
||||||
|
className={cn("[&_tr]:border-b", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { |
||||||
|
return ( |
||||||
|
<tbody |
||||||
|
data-slot="table-body" |
||||||
|
className={cn("[&_tr:last-child]:border-0", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { |
||||||
|
return ( |
||||||
|
<tfoot |
||||||
|
data-slot="table-footer" |
||||||
|
className={cn( |
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) { |
||||||
|
return ( |
||||||
|
<tr |
||||||
|
data-slot="table-row" |
||||||
|
className={cn( |
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) { |
||||||
|
return ( |
||||||
|
<th |
||||||
|
data-slot="table-head" |
||||||
|
className={cn( |
||||||
|
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) { |
||||||
|
return ( |
||||||
|
<td |
||||||
|
data-slot="table-cell" |
||||||
|
className={cn( |
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", |
||||||
|
className |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function TableCaption({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<"caption">) { |
||||||
|
return ( |
||||||
|
<caption |
||||||
|
data-slot="table-caption" |
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export { |
||||||
|
Table, |
||||||
|
TableHeader, |
||||||
|
TableBody, |
||||||
|
TableFooter, |
||||||
|
TableHead, |
||||||
|
TableRow, |
||||||
|
TableCell, |
||||||
|
TableCaption, |
||||||
|
} |
||||||
@ -0,0 +1,63 @@ |
|||||||
|
import type { Config } from "tailwindcss"; |
||||||
|
|
||||||
|
const config: Config = { |
||||||
|
darkMode: ["class"], |
||||||
|
content: [ |
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}", |
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}", |
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}", |
||||||
|
], |
||||||
|
theme: { |
||||||
|
extend: { |
||||||
|
colors: { |
||||||
|
background: 'var(--background)', |
||||||
|
foreground: 'var(--foreground)', |
||||||
|
card: { |
||||||
|
DEFAULT: 'var(--card)', |
||||||
|
foreground: 'var(--card-foreground)' |
||||||
|
}, |
||||||
|
popover: { |
||||||
|
DEFAULT: 'var(--popover)', |
||||||
|
foreground: 'var(--popover-foreground)' |
||||||
|
}, |
||||||
|
primary: { |
||||||
|
DEFAULT: 'var(--primary)', |
||||||
|
foreground: 'var(--primary-foreground)' |
||||||
|
}, |
||||||
|
secondary: { |
||||||
|
DEFAULT: 'var(--secondary)', |
||||||
|
foreground: 'var(--secondary-foreground)' |
||||||
|
}, |
||||||
|
muted: { |
||||||
|
DEFAULT: 'var(--muted)', |
||||||
|
foreground: 'var(--muted-foreground)' |
||||||
|
}, |
||||||
|
accent: { |
||||||
|
DEFAULT: 'var(--accent)', |
||||||
|
foreground: 'var(--accent-foreground)' |
||||||
|
}, |
||||||
|
destructive: { |
||||||
|
DEFAULT: 'var(--destructive)', |
||||||
|
foreground: 'var(--destructive-foreground)' |
||||||
|
}, |
||||||
|
border: 'var(--border)', |
||||||
|
input: 'var(--input)', |
||||||
|
ring: 'var(--ring)', |
||||||
|
chart: { |
||||||
|
'1': 'var(--chart-1)', |
||||||
|
'2': 'var(--chart-2)', |
||||||
|
'3': 'var(--chart-3)', |
||||||
|
'4': 'var(--chart-4)', |
||||||
|
'5': 'var(--chart-5)' |
||||||
|
} |
||||||
|
}, |
||||||
|
borderRadius: { |
||||||
|
lg: 'var(--radius)', |
||||||
|
md: 'calc(var(--radius) - 2px)', |
||||||
|
sm: 'calc(var(--radius) - 4px)' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
plugins: [require("tailwindcss-animate")], |
||||||
|
}; |
||||||
|
export default config; |
||||||
Loading…
Reference in new issue