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 OpenAI from "openai"; |
||||
import { |
||||
CopilotRuntime, |
||||
GoogleGenerativeAIAdapter, |
||||
copilotRuntimeNextJSAppRouterEndpoint |
||||
} from "@copilotkit/runtime"; |
||||
import { GoogleGenerativeAI } from "@google/generative-ai"; |
||||
|
||||
// 将您的 API Key 放在同级目录或应用根目录的 .env 文件中
|
||||
export const runtime = 'edge'; |
||||
export const POST = async (req: Request) => { |
||||
// 1. 初始化 Google Gemini 客户端
|
||||
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY || ""); |
||||
|
||||
export async function POST(req: Request) { |
||||
// 如果环境变量未就绪,使用默认的容错模式(要求客户稍后补齐)
|
||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY || "dummy-key-for-local-test" }); |
||||
// 2. 选择模型 (使用 3.1 Pro)
|
||||
const model = genAI.getGenerativeModel({
|
||||
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"; |
||||
@import "tw-animate-css"; |
||||
@import "shadcn/tailwind.css"; |
||||
|
||||
@custom-variant dark (&:is(.dark *)); |
||||
|
||||
@theme inline { |
||||
--color-background: var(--background); |
||||
--color-foreground: var(--foreground); |
||||
--font-sans: var(--font-sans); |
||||
--font-mono: var(--font-geist-mono); |
||||
--font-heading: var(--font-sans); |
||||
--color-sidebar-ring: var(--sidebar-ring); |
||||
--color-sidebar-border: var(--sidebar-border); |
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); |
||||
--color-sidebar-accent: var(--sidebar-accent); |
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); |
||||
--color-sidebar-primary: var(--sidebar-primary); |
||||
--color-sidebar-foreground: var(--sidebar-foreground); |
||||
--color-sidebar: var(--sidebar); |
||||
--color-chart-5: var(--chart-5); |
||||
--color-chart-4: var(--chart-4); |
||||
--color-chart-3: var(--chart-3); |
||||
--color-chart-2: var(--chart-2); |
||||
--color-chart-1: var(--chart-1); |
||||
--color-ring: var(--ring); |
||||
--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); |
||||
} |
||||
@tailwind base; |
||||
@tailwind components; |
||||
@tailwind utilities; |
||||
|
||||
@layer base { |
||||
: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); |
||||
--background: 0 0% 100%; |
||||
--foreground: 222.2 84% 4.9%; |
||||
--card: 0 0% 100%; |
||||
--card-foreground: 222.2 84% 4.9%; |
||||
--popover: 0 0% 100%; |
||||
--popover-foreground: 222.2 84% 4.9%; |
||||
--primary: 221.2 83.2% 53.3%; |
||||
--primary-foreground: 210 40% 98%; |
||||
--secondary: 210 40% 96.1%; |
||||
--secondary-foreground: 222.2 47.4% 11.2%; |
||||
--muted: 210 40% 96.1%; |
||||
--muted-foreground: 215.4 16.3% 46.9%; |
||||
--accent: 210 40% 96.1%; |
||||
--accent-foreground: 222.2 47.4% 11.2%; |
||||
--destructive: 0 84.2% 60.2%; |
||||
--destructive-foreground: 210 40% 98%; |
||||
--border: 214.3 31.8% 91.4%; |
||||
--input: 214.3 31.8% 91.4%; |
||||
--ring: 221.2 83.2% 53.3%; |
||||
--radius: 0.5rem; |
||||
} |
||||
|
||||
.dark { |
||||
--background: oklch(0.145 0 0); |
||||
--foreground: oklch(0.985 0 0); |
||||
--card: oklch(0.205 0 0); |
||||
--card-foreground: oklch(0.985 0 0); |
||||
--popover: oklch(0.205 0 0); |
||||
--popover-foreground: oklch(0.985 0 0); |
||||
--primary: oklch(0.922 0 0); |
||||
--primary-foreground: oklch(0.205 0 0); |
||||
--secondary: oklch(0.269 0 0); |
||||
--secondary-foreground: oklch(0.985 0 0); |
||||
--muted: oklch(0.269 0 0); |
||||
--muted-foreground: oklch(0.708 0 0); |
||||
--accent: oklch(0.269 0 0); |
||||
--accent-foreground: oklch(0.985 0 0); |
||||
--destructive: oklch(0.704 0.191 22.216); |
||||
--border: oklch(1 0 0 / 10%); |
||||
--input: oklch(1 0 0 / 15%); |
||||
--ring: oklch(0.556 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); |
||||
--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); |
||||
--background: 222.2 84% 4.9%; |
||||
--foreground: 210 40% 98%; |
||||
--card: 222.2 84% 4.9%; |
||||
--card-foreground: 210 40% 98%; |
||||
--popover: 222.2 84% 4.9%; |
||||
--popover-foreground: 210 40% 98%; |
||||
--primary: 217.2 91.2% 59.8%; |
||||
--primary-foreground: 222.2 47.4% 11.2%; |
||||
--secondary: 217.2 32.6% 17.5%; |
||||
--secondary-foreground: 210 40% 98%; |
||||
--muted: 217.2 32.6% 17.5%; |
||||
--muted-foreground: 215 20.2% 65.1%; |
||||
--accent: 217.2 32.6% 17.5%; |
||||
--accent-foreground: 210 40% 98%; |
||||
--destructive: 0 62.8% 30.6%; |
||||
--destructive-foreground: 210 40% 98%; |
||||
--border: 217.2 32.6% 17.5%; |
||||
--input: 217.2 32.6% 17.5%; |
||||
--ring: 224.3 76.3% 48%; |
||||
} |
||||
} |
||||
|
||||
@layer base { |
||||
* { |
||||
@apply border-border outline-ring/50; |
||||
@apply border-border; |
||||
} |
||||
body { |
||||
@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