sirius-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. sirius_cli/__init__.py +1 -0
  2. sirius_cli/cli.py +335 -0
  3. sirius_cli/generator.py +125 -0
  4. sirius_cli/parser.py +322 -0
  5. sirius_cli/templates/backend/Dockerfile.jinja2 +17 -0
  6. sirius_cli/templates/backend/alembic/env.py.jinja2 +70 -0
  7. sirius_cli/templates/backend/alembic/script.py.mako.jinja2 +26 -0
  8. sirius_cli/templates/backend/database.py.jinja2 +39 -0
  9. sirius_cli/templates/backend/main.py.jinja2 +178 -0
  10. sirius_cli/templates/backend/models.py.jinja2 +31 -0
  11. sirius_cli/templates/backend/requirements.txt.jinja2 +13 -0
  12. sirius_cli/templates/backend/schemas.py.jinja2 +80 -0
  13. sirius_cli/templates/docker-compose.yml.jinja2 +27 -0
  14. sirius_cli/templates/frontend/.env.jinja2 +3 -0
  15. sirius_cli/templates/frontend/Dockerfile.jinja2 +12 -0
  16. sirius_cli/templates/frontend/index.html.jinja2 +16 -0
  17. sirius_cli/templates/frontend/package.json.jinja2 +28 -0
  18. sirius_cli/templates/frontend/postcss.config.js.jinja2 +6 -0
  19. sirius_cli/templates/frontend/src/App.tsx.jinja2 +89 -0
  20. sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2 +210 -0
  21. sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2 +567 -0
  22. sirius_cli/templates/frontend/src/index.css.jinja2 +25 -0
  23. sirius_cli/templates/frontend/src/main.tsx.jinja2 +10 -0
  24. sirius_cli/templates/frontend/tailwind.config.js.jinja2 +21 -0
  25. sirius_cli/templates/frontend/tsconfig.json.jinja2 +25 -0
  26. sirius_cli/templates/frontend/vite.config.ts.jinja2 +10 -0
  27. sirius_cli-0.1.0.dist-info/METADATA +165 -0
  28. sirius_cli-0.1.0.dist-info/RECORD +32 -0
  29. sirius_cli-0.1.0.dist-info/WHEEL +5 -0
  30. sirius_cli-0.1.0.dist-info/entry_points.txt +2 -0
  31. sirius_cli-0.1.0.dist-info/licenses/LICENSE +619 -0
  32. sirius_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ fastapi>=0.100.0
2
+ uvicorn>=0.20.0
3
+ sqlalchemy>=2.0.0
4
+ alembic>=1.11.0
5
+ pydantic>=2.0.0
6
+ pandas>=1.5.0
7
+ openpyxl>=3.0.0
8
+ email-validator>=2.0.0
9
+ {% if db_type == 'pg' %}
10
+ psycopg2-binary>=2.9.0
11
+ {% elif db_type == 'mysql' %}
12
+ pymysql>=1.1.0
13
+ {% endif %}
@@ -0,0 +1,80 @@
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ try:
6
+ from pydantic import EmailStr
7
+ _has_email_validator = True
8
+ except ImportError:
9
+ _has_email_validator = False
10
+ EmailStr = str # graceful fallback if email-validator not installed
11
+
12
+ {% for table_name, columns in schemas.items() %}
13
+ {% set class_name = table_name.replace('_', ' ').title().replace(' ', '') %}
14
+ class {{ class_name }}Base(BaseModel):
15
+ {% for col in columns %}
16
+ {% if not col.is_pk %}
17
+ {% if col.type == 'Integer' %}
18
+ {% if col.name in ['amount', 'price', 'quantity', 'count', 'total', 'balance', 'score', 'rating', 'age', 'year'] or col.name.endswith(('_amount', '_price', '_qty', '_count', '_total', '_score')) %}
19
+ {{ col.name }}: Optional[int] = Field(default=None, ge=0)
20
+ {% else %}
21
+ {{ col.name }}: Optional[int] = None
22
+ {% endif %}
23
+ {% elif col.type == 'Float' %}
24
+ {% if col.name in ['amount', 'price', 'quantity', 'total', 'balance', 'score', 'rating', 'weight', 'cost'] or col.name.endswith(('_amount', '_price', '_qty', '_total', '_score', '_cost')) %}
25
+ {{ col.name }}: Optional[float] = Field(default=None, ge=0)
26
+ {% else %}
27
+ {{ col.name }}: Optional[float] = None
28
+ {% endif %}
29
+ {% elif col.type == 'Boolean' %}
30
+ {{ col.name }}: Optional[bool] = None
31
+ {% elif col.type == 'DateTime' %}
32
+ {{ col.name }}: Optional[datetime] = None
33
+ {% else %}
34
+ {% if 'email' in col.name %}
35
+ {{ col.name }}: Optional[EmailStr] = None
36
+ {% else %}
37
+ {{ col.name }}: Optional[str] = None
38
+ {% endif %}
39
+ {% endif %}
40
+ {% endif %}
41
+ {% endfor %}
42
+
43
+ class {{ class_name }}Create({{ class_name }}Base):
44
+ pass
45
+
46
+ class {{ class_name }}Update(BaseModel):
47
+ {% for col in columns %}
48
+ {% if not col.is_pk %}
49
+ {% if col.type == 'Integer' %}
50
+ {% if col.name in ['amount', 'price', 'quantity', 'count', 'total', 'balance', 'score', 'rating', 'age', 'year'] or col.name.endswith(('_amount', '_price', '_qty', '_count', '_total', '_score')) %}
51
+ {{ col.name }}: Optional[int] = Field(default=None, ge=0)
52
+ {% else %}
53
+ {{ col.name }}: Optional[int] = None
54
+ {% endif %}
55
+ {% elif col.type == 'Float' %}
56
+ {% if col.name in ['amount', 'price', 'quantity', 'total', 'balance', 'score', 'rating', 'weight', 'cost'] or col.name.endswith(('_amount', '_price', '_qty', '_total', '_score', '_cost')) %}
57
+ {{ col.name }}: Optional[float] = Field(default=None, ge=0)
58
+ {% else %}
59
+ {{ col.name }}: Optional[float] = None
60
+ {% endif %}
61
+ {% elif col.type == 'Boolean' %}
62
+ {{ col.name }}: Optional[bool] = None
63
+ {% elif col.type == 'DateTime' %}
64
+ {{ col.name }}: Optional[datetime] = None
65
+ {% else %}
66
+ {% if 'email' in col.name %}
67
+ {{ col.name }}: Optional[EmailStr] = None
68
+ {% else %}
69
+ {{ col.name }}: Optional[str] = None
70
+ {% endif %}
71
+ {% endif %}
72
+ {% endif %}
73
+ {% endfor %}
74
+
75
+ class {{ class_name }}Response({{ class_name }}Base):
76
+ id: int
77
+
78
+ model_config = ConfigDict(from_attributes=True)
79
+
80
+ {% endfor %}
@@ -0,0 +1,27 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ backend:
5
+ build:
6
+ context: ./backend
7
+ dockerfile: Dockerfile
8
+ ports:
9
+ - "{{ port }}:{{ port }}"
10
+ volumes:
11
+ - ./backend:/app
12
+ environment:
13
+ - PORT={{ port }}
14
+
15
+ frontend:
16
+ build:
17
+ context: ./frontend
18
+ dockerfile: Dockerfile
19
+ ports:
20
+ - "5173:5173"
21
+ volumes:
22
+ - ./frontend:/app
23
+ - /app/node_modules
24
+ environment:
25
+ - VITE_API_URL={{ api_url }}
26
+ depends_on:
27
+ - backend
@@ -0,0 +1,3 @@
1
+ # Auto-generated by Sirius-CLI
2
+ # Change VITE_API_URL if your backend runs on a different host or port.
3
+ VITE_API_URL={{ api_url }}
@@ -0,0 +1,12 @@
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json .
6
+ RUN npm install
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 5173
11
+
12
+ CMD ["npm", "run", "dev", "--", "--host"]
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Proto-CLI Generated UI</title>
7
+ <!-- Google Fonts for premium typography -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+ </head>
12
+ <body class="bg-slate-950 text-slate-100 antialiased font-sans">
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.tsx"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "proto-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0",
14
+ "react-router-dom": "^6.22.0",
15
+ "lucide-react": "^0.320.0",
16
+ "recharts": "^2.12.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.2.55",
20
+ "@types/react-dom": "^18.2.19",
21
+ "@vitejs/plugin-react": "^4.2.1",
22
+ "autoprefixer": "^10.4.17",
23
+ "postcss": "^8.4.35",
24
+ "tailwindcss": "^3.4.1",
25
+ "typescript": "^5.2.2",
26
+ "vite": "^5.1.0"
27
+ }
28
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {}
5
+ }
6
+ }
@@ -0,0 +1,89 @@
1
+ import React from 'react'
2
+ import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'
3
+ import { Database, Table, LayoutDashboard, ChevronRight } from 'lucide-react'
4
+ import Dashboard from './Dashboard'
5
+ {% for table_name, columns in schemas.items() %}
6
+ import {{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud from './pages/{{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud'
7
+ {% endfor %}
8
+
9
+ const SidebarLink = ({ to, children, icon: Icon }: { to: string; children: React.ReactNode; icon: any }) => {
10
+ const location = useLocation()
11
+ const isActive = location.pathname === to
12
+ return (
13
+ <Link
14
+ to={to}
15
+ className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group ${
16
+ isActive
17
+ ? 'bg-gradient-to-r from-brand-600 to-brand-700 text-white font-medium shadow-lg shadow-brand-500/20'
18
+ : 'text-slate-400 hover:text-slate-100 hover:bg-slate-900/60'
19
+ }`}
20
+ >
21
+ <Icon className={`w-5 h-5 transition-transform duration-200 ${isActive ? 'scale-110' : 'group-hover:scale-110'}`} />
22
+ <span className="text-sm font-outfit">{children}</span>
23
+ {isActive && <ChevronRight className="w-4 h-4 ml-auto" />}
24
+ </Link>
25
+ )
26
+ }
27
+
28
+ function AppContent() {
29
+ return (
30
+ <div className="flex min-h-screen bg-slate-950 text-slate-100 font-sans">
31
+ {/* Sidebar */}
32
+ <aside className="w-64 border-r border-slate-900 bg-slate-950/70 backdrop-blur-xl flex flex-col p-6 fixed h-full z-10">
33
+ <div className="flex items-center gap-3 mb-8 px-2">
34
+ <div className="p-2.5 bg-brand-500/10 text-brand-400 rounded-xl border border-brand-500/20">
35
+ <Database className="w-6 h-6 animate-pulse" />
36
+ </div>
37
+ <div>
38
+ <h1 className="font-outfit font-bold text-lg bg-gradient-to-r from-brand-400 to-brand-300 bg-clip-text text-transparent">Proto Console</h1>
39
+ <p className="text-[10px] text-slate-500 tracking-wider uppercase font-semibold">Generated Admin</p>
40
+ </div>
41
+ </div>
42
+
43
+ <nav className="flex-1 space-y-1.5 overflow-y-auto pr-1">
44
+ <SidebarLink to="/" icon={LayoutDashboard}>Dashboard</SidebarLink>
45
+ <div className="pt-4 pb-2">
46
+ <p className="text-[10px] font-semibold text-slate-500 uppercase tracking-widest px-4">Tables</p>
47
+ </div>
48
+ {% for table_name, columns in schemas.items() %}
49
+ <SidebarLink to="/{{ table_name }}" icon={Table}>
50
+ {{ table_name.replace('_', ' ').title() }}
51
+ </SidebarLink>
52
+ {% endfor %}
53
+ </nav>
54
+
55
+ <div className="mt-auto border-t border-slate-900 pt-4 px-2">
56
+ <p className="text-xs text-slate-500">FastAPI + React SPA</p>
57
+ <p className="text-[10px] text-slate-600 mt-1">Status: <span className="text-emerald-500 font-semibold">Online</span></p>
58
+ </div>
59
+ </aside>
60
+
61
+ {/* Main Content Area */}
62
+ <main className="flex-1 ml-64 min-h-screen flex flex-col bg-gradient-to-b from-slate-900/50 to-slate-950">
63
+ <header className="h-16 border-b border-slate-900/50 flex items-center px-8 bg-slate-950/50 backdrop-blur-md sticky top-0 z-20 justify-between">
64
+ <div className="flex items-center gap-2">
65
+ <span className="h-2 w-2 rounded-full bg-emerald-500 animate-ping"></span>
66
+ <span className="text-xs text-slate-400 font-medium">Development Instance</span>
67
+ </div>
68
+ </header>
69
+
70
+ <div className="p-8 flex-1">
71
+ <Routes>
72
+ <Route path="/" element={<Dashboard />} />
73
+ {% for table_name, columns in schemas.items() %}
74
+ <Route path="/{{ table_name }}" element={<{{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud />} />
75
+ {% endfor %}
76
+ </Routes>
77
+ </div>
78
+ </main>
79
+ </div>
80
+ )
81
+ }
82
+
83
+ export default function App() {
84
+ return (
85
+ <Router>
86
+ <AppContent />
87
+ </Router>
88
+ )
89
+ }
@@ -0,0 +1,210 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import { Link } from 'react-router-dom'
3
+ import { Table, Layers, Hash, Calendar, ArrowRight, RefreshCw, BarChart2 } from 'lucide-react'
4
+ import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'
5
+
6
+ const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'
7
+
8
+ interface TableMetadata {
9
+ name: string
10
+ count: number
11
+ columns: Array<{ name: string; type: string; is_pk: boolean }>
12
+ }
13
+
14
+ export default function Dashboard() {
15
+ const [metadata, setMetadata] = useState<TableMetadata[]>([])
16
+ const [loading, setLoading] = useState(true)
17
+ const [error, setError] = useState<string | null>(null)
18
+
19
+ const fetchMetadata = async () => {
20
+ try {
21
+ setLoading(true)
22
+ const res = await fetch(`${API_BASE}/api/metadata`)
23
+ if (!res.ok) throw new Error('Failed to fetch system metadata')
24
+ const data = await res.json()
25
+ setMetadata(data)
26
+ setError(null)
27
+ } catch (err: any) {
28
+ setError(err.message || 'Something went wrong')
29
+ } finally {
30
+ setLoading(false)
31
+ }
32
+ }
33
+
34
+ useEffect(() => {
35
+ fetchMetadata()
36
+ }, [])
37
+
38
+ const totalTables = metadata.length
39
+ const totalRows = metadata.reduce((acc, curr) => acc + curr.count, 0)
40
+
41
+ return (
42
+ <div className="space-y-8">
43
+ {/* Top Banner */}
44
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
45
+ <div>
46
+ <h2 className="text-3xl font-outfit font-bold tracking-tight bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent">
47
+ System Overview
48
+ </h2>
49
+ <p className="text-slate-400 mt-1 text-sm font-sans">
50
+ Overview of autogenerated tables, records, and database stats.
51
+ </p>
52
+ </div>
53
+ <button
54
+ onClick={fetchMetadata}
55
+ className="flex items-center gap-2 px-4 py-2 bg-slate-900 border border-slate-800 rounded-xl text-xs font-semibold hover:bg-slate-800 transition-all text-slate-300"
56
+ >
57
+ <RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
58
+ Refresh Stats
59
+ </button>
60
+ </div>
61
+
62
+ {/* Stats Grid */}
63
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
64
+ <div className="p-6 bg-slate-900/40 border border-slate-900 rounded-2xl relative overflow-hidden group hover:border-slate-800 transition-all duration-300">
65
+ <div className="absolute top-0 right-0 p-8 text-brand-500/10 transition-transform duration-300 group-hover:scale-110">
66
+ <Layers className="w-24 h-24" />
67
+ </div>
68
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-widest">Total Tables</p>
69
+ <p className="text-4xl font-bold font-outfit mt-2">{totalTables}</p>
70
+ <p className="text-xs text-slate-400 mt-2">Discovered in schema parse</p>
71
+ </div>
72
+
73
+ <div className="p-6 bg-slate-900/40 border border-slate-900 rounded-2xl relative overflow-hidden group hover:border-slate-800 transition-all duration-300">
74
+ <div className="absolute top-0 right-0 p-8 text-brand-500/10 transition-transform duration-300 group-hover:scale-110">
75
+ <Hash className="w-24 h-24" />
76
+ </div>
77
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-widest">Total Rows</p>
78
+ <p className="text-4xl font-bold font-outfit mt-2">{totalRows}</p>
79
+ <p className="text-xs text-slate-400 mt-2">Stored across all tables</p>
80
+ </div>
81
+
82
+ <div className="p-6 bg-slate-900/40 border border-slate-900 rounded-2xl relative overflow-hidden group hover:border-slate-800 transition-all duration-300 md:col-span-2 lg:col-span-1">
83
+ <div className="absolute top-0 right-0 p-8 text-emerald-500/10 transition-transform duration-300 group-hover:scale-110">
84
+ <Calendar className="w-24 h-24" />
85
+ </div>
86
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-widest">Server Status</p>
87
+ <p className="text-4xl font-bold font-outfit mt-2">Active</p>
88
+ <p className="text-xs text-slate-400 mt-2 flex items-center gap-1.5">
89
+ <span className="h-1.5 w-1.5 rounded-full bg-emerald-500"></span> Live sync connection
90
+ </p>
91
+ </div>
92
+ </div>
93
+
94
+ {/* Row Distribution Chart */}
95
+ {!loading && !error && metadata.length > 0 && (
96
+ <div className="p-6 bg-slate-900/30 border border-slate-900 rounded-2xl">
97
+ <h3 className="text-lg font-bold font-outfit text-slate-200 mb-6 flex items-center gap-2">
98
+ <BarChart2 className="w-5 h-5 text-brand-400" />
99
+ Record Distribution
100
+ </h3>
101
+ <div className="h-72 w-full">
102
+ <ResponsiveContainer width="100%" height="100%">
103
+ <BarChart data={metadata} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
104
+ <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
105
+ <XAxis
106
+ dataKey="name"
107
+ stroke="#64748b"
108
+ fontSize={12}
109
+ tickLine={false}
110
+ axisLine={false}
111
+ tickFormatter={(val) => val.replace('_', ' ')}
112
+ />
113
+ <YAxis
114
+ stroke="#64748b"
115
+ fontSize={12}
116
+ tickLine={false}
117
+ axisLine={false}
118
+ tickFormatter={(val) => val >= 1000 ? `${(val / 1000).toFixed(1)}k` : val}
119
+ />
120
+ <Tooltip
121
+ cursor={{ fill: '#0f172a' }}
122
+ contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', borderRadius: '0.75rem' }}
123
+ itemStyle={{ color: '#38bdf8' }}
124
+ labelStyle={{ color: '#94a3b8', textTransform: 'capitalize', marginBottom: '4px' }}
125
+ formatter={(value: number) => [value, 'Records']}
126
+ labelFormatter={(label: string) => label.replace('_', ' ')}
127
+ />
128
+ <Bar
129
+ dataKey="count"
130
+ fill="#0ea5e9"
131
+ radius={[4, 4, 0, 0]}
132
+ barSize={40}
133
+ animationDuration={1500}
134
+ />
135
+ </BarChart>
136
+ </ResponsiveContainer>
137
+ </div>
138
+ </div>
139
+ )}
140
+
141
+ {/* Tables section */}
142
+ <div>
143
+ <h3 className="text-lg font-bold font-outfit text-slate-200 mb-6 flex items-center gap-2">
144
+ <Table className="w-5 h-5 text-brand-400" />
145
+ Entity Navigator
146
+ </h3>
147
+
148
+ {loading ? (
149
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
150
+ {[1, 2].map((n) => (
151
+ <div key={n} className="h-48 rounded-2xl bg-slate-900/20 border border-slate-900/50 animate-pulse" />
152
+ ))}
153
+ </div>
154
+ ) : error ? (
155
+ <div className="p-6 border border-red-500/20 bg-red-500/5 text-red-400 rounded-2xl">
156
+ <p className="font-semibold text-sm">Failed to connect to backend server</p>
157
+ <p className="text-xs mt-1 text-red-400/80">Make sure the FastAPI backend is running on {API_BASE}.</p>
158
+ </div>
159
+ ) : (
160
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
161
+ {metadata.map((table) => (
162
+ <div
163
+ key={table.name}
164
+ className="p-6 bg-slate-900/30 border border-slate-900 rounded-2xl hover:border-slate-800 transition-all duration-300 flex flex-col justify-between group"
165
+ >
166
+ <div>
167
+ <div className="flex items-center justify-between mb-4">
168
+ <h4 className="text-lg font-bold text-white group-hover:text-brand-400 transition-colors capitalize font-outfit">
169
+ {table.name.replace('_', ' ')}
170
+ </h4>
171
+ <span className="px-2.5 py-1 bg-brand-500/10 text-brand-400 border border-brand-500/10 rounded-lg text-xs font-semibold">
172
+ {table.count} row{table.count !== 1 ? 's' : ''}
173
+ </span>
174
+ </div>
175
+
176
+ <p className="text-xs text-slate-400 mb-4">
177
+ Contains {table.columns.length} columns:
178
+ </p>
179
+
180
+ <div className="flex flex-wrap gap-1.5 mb-6">
181
+ {table.columns.map((col) => (
182
+ <span
183
+ key={col.name}
184
+ className={`text-[10px] px-2 py-0.5 rounded-md font-mono ${
185
+ col.is_pk
186
+ ? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
187
+ : 'bg-slate-800 text-slate-300'
188
+ }`}
189
+ >
190
+ {col.name}: {col.type}
191
+ </span>
192
+ ))}
193
+ </div>
194
+ </div>
195
+
196
+ <Link
197
+ to={`/${table.name}`}
198
+ className="flex items-center gap-2 text-xs font-semibold text-slate-300 hover:text-white mt-auto group/btn transition-colors"
199
+ >
200
+ Manage Records
201
+ <ArrowRight className="w-3.5 h-3.5 transform group-hover/btn:translate-x-1 transition-transform" />
202
+ </Link>
203
+ </div>
204
+ ))}
205
+ </div>
206
+ )}
207
+ </div>
208
+ </div>
209
+ )
210
+ }