create-mantiq 0.1.2 → 0.1.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mantiq",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Scaffold a new MantiqJS application",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -5,11 +5,12 @@ import { randomBytes } from 'node:crypto'
5
5
  import { getTemplates } from './templates.ts'
6
6
 
7
7
  // ── ANSI helpers ─────────────────────────────────────────────────────────────
8
- const bold = (s: string) => `\x1b[1m${s}\x1b[0m`
9
- const green = (s: string) => `\x1b[32m${s}\x1b[0m`
10
- const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`
11
- const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
12
- const red = (s: string) => `\x1b[31m${s}\x1b[0m`
8
+ const R = '\x1b[0m'
9
+ const bold = (s: string) => `\x1b[1m${s}${R}`
10
+ const dim = (s: string) => `\x1b[2m${s}${R}`
11
+ const red = (s: string) => `\x1b[31m${s}${R}`
12
+ const emerald = (s: string) => `\x1b[38;2;52;211;153m${s}${R}`
13
+ const gray = (s: string) => `\x1b[90m${s}${R}`
13
14
 
14
15
  // ── Parse args ───────────────────────────────────────────────────────────────
15
16
  const rawArgs = process.argv.slice(2)
@@ -38,13 +39,13 @@ type Kit = typeof validKits[number]
38
39
 
39
40
  if (!projectName) {
40
41
  console.log(`
41
- ${bold('create-mantiq')} — Scaffold a new MantiqJS application
42
+ ${emerald('mantiq')} ${dim('framework')}
42
43
 
43
44
  ${bold('Usage:')}
44
- bun create mantiq ${cyan('<project-name>')} [options]
45
+ bun create mantiq ${emerald('<project-name>')} [options]
45
46
 
46
47
  ${bold('Options:')}
47
- --kit=${cyan('react|vue|svelte')} Add a frontend starter kit
48
+ --kit=${emerald('react|vue|svelte')} Add a frontend starter kit
48
49
  --no-git Skip git initialization
49
50
 
50
51
  ${bold('Examples:')}
@@ -69,8 +70,8 @@ if (existsSync(projectDir)) {
69
70
  }
70
71
 
71
72
  // ── Generate ─────────────────────────────────────────────────────────────────
72
- const kitLabel = kit ? ` with ${bold(kit)} starter kit` : ''
73
- console.log(`\n ${bold('Creating')} ${cyan(projectName)}${kitLabel}...\n`)
73
+ const kitLabel = kit ? ` ${dim('with')} ${bold(kit)}` : ''
74
+ console.log(`\n ${emerald('mantiq')} Creating ${bold(projectName)}${kitLabel}\n`)
74
75
 
75
76
  const appKey = `base64:${randomBytes(32).toString('base64')}`
76
77
  const templates = getTemplates({ name: projectName, appKey, kit: kit as Kit | undefined })
@@ -82,8 +83,8 @@ for (const relativePath of files) {
82
83
  mkdirSync(dirname(fullPath), { recursive: true })
83
84
  await Bun.write(fullPath, templates[relativePath]!)
84
85
 
85
- const display = relativePath.endsWith('.gitkeep') ? dim(relativePath) : green(relativePath)
86
- console.log(` ${green('+')} ${display}`)
86
+ const display = relativePath.endsWith('.gitkeep') ? dim(relativePath) : relativePath
87
+ console.log(` ${emerald('+')} ${display}`)
87
88
  }
88
89
 
89
90
  // ── Install dependencies ─────────────────────────────────────────────────────
@@ -100,7 +101,6 @@ await install.exited
100
101
  if (kit) {
101
102
  console.log(`\n ${bold('Building frontend assets...')}\n`)
102
103
 
103
- // Client build
104
104
  const viteBuild = Bun.spawn(['npx', 'vite', 'build'], {
105
105
  cwd: projectDir,
106
106
  stdout: 'inherit',
@@ -108,7 +108,6 @@ if (kit) {
108
108
  })
109
109
  await viteBuild.exited
110
110
 
111
- // SSR build
112
111
  const ssrEntry = kit === 'react' ? 'src/ssr.tsx' : 'src/ssr.ts'
113
112
  console.log(`\n ${bold('Building SSR bundle...')}\n`)
114
113
 
@@ -148,32 +147,23 @@ if (!noGit) {
148
147
 
149
148
  // ── Done ─────────────────────────────────────────────────────────────────────
150
149
  const frontendSteps = kit
151
- ? ` ${cyan('bun run')} dev:frontend ${dim('# start Vite dev server (in a second terminal)')}\n`
150
+ ? ` ${emerald('bun run')} dev:frontend ${dim('# start Vite dev server')}\n`
152
151
  : ''
153
152
 
154
153
  console.log(`
155
- ${green('')} ${bold('Created')} ${cyan(projectName)} ${bold('successfully!')}
154
+ ${emerald('mantiq')} ${bold(projectName)} ready.
156
155
 
157
156
  ${bold('Next steps:')}
158
157
 
159
- ${cyan('cd')} ${projectName}
160
- ${cyan('bun mantiq')} migrate ${dim('# run database migrations')}
161
- ${cyan('bun run')} dev ${dim('# start development server')}
162
- ${frontendSteps} ${cyan('bun mantiq')} tinker ${dim('# interactive REPL')}
163
-
164
- ${bold('Included packages:')}
165
-
166
- ${dim('core · database · auth · validation · filesystem · logging')}
167
- ${dim('events · queue · realtime · heartbeat · helpers · cli')}
168
-
158
+ ${emerald('cd')} ${projectName}
159
+ ${emerald('bun mantiq')} migrate ${dim('# run database migrations')}
160
+ ${emerald('bun run')} dev ${dim('# start development server')}
161
+ ${frontendSteps}
169
162
  ${bold('Useful commands:')}
170
163
 
171
- ${cyan('bun mantiq')} route:list ${dim('# list all registered routes')}
172
- ${cyan('bun mantiq')} queue:work ${dim('# start processing queued jobs')}
173
- ${cyan('bun mantiq')} make:model ${dim('# generate a new model')}
174
- ${cyan('bun mantiq')} make:job ${dim('# generate a new job class')}
175
-
176
- ${dim('Dashboard: http://localhost:3000/_heartbeat')}
164
+ ${emerald('bun mantiq')} route:list ${dim('# list all registered routes')}
165
+ ${emerald('bun mantiq')} make:model ${dim('# generate a new model')}
166
+ ${emerald('bun mantiq')} about ${dim('# framework environment info')}
177
167
 
178
- ${dim('Happy building!')}
168
+ ${dim('Dashboard http://localhost:3000/_heartbeat')}
179
169
  `)
package/src/kits/react.ts CHANGED
@@ -21,6 +21,12 @@ export default defineConfig({
21
21
  `,
22
22
 
23
23
  'src/style.css': `@import "tailwindcss";
24
+
25
+ @keyframes fadeUp {
26
+ from { opacity: 0; transform: translateY(8px); }
27
+ to { opacity: 1; transform: translateY(0); }
28
+ }
29
+ .animate-fade-up { animation: fadeUp 0.4s ease-out; }
24
30
  `,
25
31
 
26
32
  'src/pages.ts': `import Login from './pages/Login.tsx'
@@ -73,6 +79,15 @@ interface MantiqAppProps {
73
79
  initialData?: Record<string, any>
74
80
  }
75
81
 
82
+ function initTheme() {
83
+ if (typeof window === 'undefined') return
84
+ const theme = localStorage.getItem('theme') ||
85
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
86
+ document.documentElement.classList.toggle('dark', theme === 'dark')
87
+ }
88
+
89
+ initTheme()
90
+
76
91
  export function MantiqApp({ pages, initialData }: MantiqAppProps) {
77
92
  const windowData = typeof window !== 'undefined' ? (window as any).__MANTIQ_DATA__ : {}
78
93
  const initial = initialData ?? windowData
@@ -135,49 +150,35 @@ export default function Login({ appName = '${ctx.name}', navigate }: LoginProps)
135
150
  }
136
151
 
137
152
  return (
138
- <div className="min-h-screen bg-gray-950 flex">
139
- <div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-indigo-950 via-gray-950 to-gray-950 items-center justify-center p-16 relative overflow-hidden">
140
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_40%,rgba(99,102,241,0.08),transparent_60%)]" />
141
- <div className="relative space-y-6 max-w-md">
142
- <div className="flex items-center gap-3">
143
- <div className="w-12 h-12 rounded-xl bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
144
- <svg className="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
145
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
146
- </svg>
147
- </div>
148
- <span className="text-2xl font-bold text-white">{appName}</span>
149
- </div>
150
- <h2 className="text-4xl font-bold text-white leading-tight">Build something<br />amazing.</h2>
151
- <p className="text-gray-400 text-lg leading-relaxed">
152
- Session auth, encrypted cookies, CSRF protection — all wired up and ready to go.
153
- </p>
153
+ <div className="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-4">
154
+ <div className="animate-fade-up w-full max-w-sm">
155
+ <div className="text-center mb-8">
156
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white">{appName}</h2>
154
157
  </div>
155
- </div>
156
- <div className="flex-1 flex items-center justify-center p-8">
157
- <div className="bg-gray-900 rounded-xl border border-gray-800 w-full max-w-md p-8 space-y-6">
158
+ <div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-8 space-y-6 shadow-sm">
158
159
  <div>
159
- <h1 className="text-xl font-bold text-white">Welcome back</h1>
160
- <p className="text-sm text-gray-500 mt-1">Sign in to your account</p>
160
+ <h1 className="text-xl font-bold text-gray-900 dark:text-white">Welcome back</h1>
161
+ <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Sign in to your account</p>
161
162
  </div>
162
- {error && <div className="bg-red-500/10 border border-red-500/30 text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{error}</div>}
163
+ {error && <div className="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{error}</div>}
163
164
  <form onSubmit={handleSubmit} className="space-y-4">
164
- <div className="space-y-1">
165
- <label className="block text-sm font-medium text-gray-400">Email</label>
165
+ <div className="space-y-1.5">
166
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
166
167
  <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
167
- className="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
168
+ className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
168
169
  </div>
169
- <div className="space-y-1">
170
- <label className="block text-sm font-medium text-gray-400">Password</label>
170
+ <div className="space-y-1.5">
171
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
171
172
  <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
172
- className="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
173
+ className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
173
174
  </div>
174
175
  <button type="submit" disabled={loading}
175
- className="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold text-sm py-2.5 rounded-lg transition-colors">
176
- Sign in
176
+ className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold text-sm py-2.5 rounded-lg transition-colors">
177
+ {loading ? 'Signing in...' : 'Sign in'}
177
178
  </button>
178
179
  </form>
179
- <p className="text-sm text-gray-500 text-center">
180
- Don't have an account? <a href="/register" className="text-indigo-400 hover:text-indigo-300">Register</a>
180
+ <p className="text-sm text-gray-500 dark:text-gray-400 text-center">
181
+ Don't have an account? <a href="/register" className="text-emerald-600 dark:text-emerald-400 hover:text-emerald-500 dark:hover:text-emerald-300 font-medium">Register</a>
181
182
  </p>
182
183
  </div>
183
184
  </div>
@@ -211,54 +212,40 @@ export default function Register({ appName = '${ctx.name}', navigate }: Register
211
212
  }
212
213
 
213
214
  return (
214
- <div className="min-h-screen bg-gray-950 flex">
215
- <div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-indigo-950 via-gray-950 to-gray-950 items-center justify-center p-16 relative overflow-hidden">
216
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_40%,rgba(99,102,241,0.08),transparent_60%)]" />
217
- <div className="relative space-y-6 max-w-md">
218
- <div className="flex items-center gap-3">
219
- <div className="w-12 h-12 rounded-xl bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
220
- <svg className="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
221
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
222
- </svg>
223
- </div>
224
- <span className="text-2xl font-bold text-white">{appName}</span>
225
- </div>
226
- <h2 className="text-4xl font-bold text-white leading-tight">Build something<br />amazing.</h2>
227
- <p className="text-gray-400 text-lg leading-relaxed">
228
- Session auth, encrypted cookies, CSRF protection — all wired up and ready to go.
229
- </p>
215
+ <div className="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-4">
216
+ <div className="animate-fade-up w-full max-w-sm">
217
+ <div className="text-center mb-8">
218
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white">{appName}</h2>
230
219
  </div>
231
- </div>
232
- <div className="flex-1 flex items-center justify-center p-8">
233
- <div className="bg-gray-900 rounded-xl border border-gray-800 w-full max-w-md p-8 space-y-6">
220
+ <div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-8 space-y-6 shadow-sm">
234
221
  <div>
235
- <h1 className="text-xl font-bold text-white">Create an account</h1>
236
- <p className="text-sm text-gray-500 mt-1">Get started with {appName}</p>
222
+ <h1 className="text-xl font-bold text-gray-900 dark:text-white">Create an account</h1>
223
+ <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Get started with {appName}</p>
237
224
  </div>
238
- {error && <div className="bg-red-500/10 border border-red-500/30 text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{error}</div>}
225
+ {error && <div className="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{error}</div>}
239
226
  <form onSubmit={handleSubmit} className="space-y-4">
240
- <div className="space-y-1">
241
- <label className="block text-sm font-medium text-gray-400">Name</label>
227
+ <div className="space-y-1.5">
228
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
242
229
  <input value={name} onChange={(e) => setName(e.target.value)} required
243
- className="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
230
+ className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
244
231
  </div>
245
- <div className="space-y-1">
246
- <label className="block text-sm font-medium text-gray-400">Email</label>
232
+ <div className="space-y-1.5">
233
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
247
234
  <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
248
- className="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
235
+ className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
249
236
  </div>
250
- <div className="space-y-1">
251
- <label className="block text-sm font-medium text-gray-400">Password</label>
237
+ <div className="space-y-1.5">
238
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
252
239
  <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
253
- className="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
240
+ className="w-full bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-3.5 py-2.5 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/25 transition-all" />
254
241
  </div>
255
242
  <button type="submit" disabled={loading}
256
- className="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold text-sm py-2.5 rounded-lg transition-colors">
257
- Create account
243
+ className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold text-sm py-2.5 rounded-lg transition-colors">
244
+ {loading ? 'Creating account...' : 'Create account'}
258
245
  </button>
259
246
  </form>
260
- <p className="text-sm text-gray-500 text-center">
261
- Already have an account? <a href="/login" className="text-indigo-400 hover:text-indigo-300">Sign in</a>
247
+ <p className="text-sm text-gray-500 dark:text-gray-400 text-center">
248
+ Already have an account? <a href="/login" className="text-emerald-600 dark:text-emerald-400 hover:text-emerald-500 dark:hover:text-emerald-300 font-medium">Sign in</a>
262
249
  </p>
263
250
  </div>
264
251
  </div>
@@ -283,6 +270,15 @@ interface DashboardProps {
283
270
  export default function Dashboard({ appName = '${ctx.name}', currentUser, users: initialUsers, navigate }: DashboardProps) {
284
271
  const [users, setUsers] = useState<User[]>(initialUsers ?? [])
285
272
  const [loading, setLoading] = useState(!initialUsers?.length)
273
+ const [isDark, setIsDark] = useState(() =>
274
+ typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : true
275
+ )
276
+
277
+ const toggleTheme = () => {
278
+ const dark = document.documentElement.classList.toggle('dark')
279
+ localStorage.setItem('theme', dark ? 'dark' : 'light')
280
+ setIsDark(dark)
281
+ }
286
282
 
287
283
  const fetchUsers = useCallback(async () => {
288
284
  setLoading(true)
@@ -301,60 +297,85 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
301
297
  }
302
298
 
303
299
  return (
304
- <div className="min-h-screen bg-gray-950 text-gray-100">
305
- <nav className="border-b border-gray-800/80 bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
300
+ <div className="min-h-screen bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100">
301
+ <nav className="border-b border-gray-200 dark:border-gray-800/80 bg-white/90 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
306
302
  <div className="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
307
- <div className="flex items-center gap-2.5">
308
- <div className="w-7 h-7 rounded-lg bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
309
- <svg className="w-3.5 h-3.5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
310
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
311
- </svg>
312
- </div>
313
- <span className="text-sm font-bold text-white">{appName}</span>
314
- </div>
303
+ <span className="text-sm font-bold text-gray-900 dark:text-white">{appName}</span>
315
304
  <div className="flex items-center gap-3">
316
- <span className="text-xs text-gray-400">{currentUser?.name}</span>
305
+ <button onClick={toggleTheme} className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" title="Toggle theme">
306
+ {isDark ? (
307
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
308
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
309
+ </svg>
310
+ ) : (
311
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
312
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
313
+ </svg>
314
+ )}
315
+ </button>
316
+ <span className="text-xs text-gray-500 dark:text-gray-400">{currentUser?.name}</span>
317
317
  <button onClick={handleLogout}
318
- className="text-xs text-gray-500 hover:text-white bg-gray-900 hover:bg-gray-800 border border-gray-800 rounded-lg px-3 py-1.5 transition-colors">
318
+ className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white bg-gray-100 dark:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-800 rounded-lg px-3 py-1.5 transition-colors">
319
319
  Logout
320
320
  </button>
321
321
  </div>
322
322
  </div>
323
323
  </nav>
324
324
 
325
- <main className="max-w-5xl mx-auto px-6 py-8 space-y-6">
325
+ <main className="max-w-5xl mx-auto px-6 py-8 space-y-6 animate-fade-up">
326
326
  <div>
327
- <h1 className="text-xl font-bold text-white">Dashboard</h1>
328
- <p className="text-sm text-gray-500 mt-1">Welcome back, {currentUser?.name}.</p>
327
+ <h1 className="text-xl font-bold text-gray-900 dark:text-white">Welcome back, {currentUser?.name}</h1>
328
+ <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
329
+ </div>
330
+
331
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
332
+ <a href="/heartbeat" className="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-500/40 transition-colors">
333
+ <div className="flex items-center justify-between">
334
+ <div>
335
+ <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-200">Heartbeat Dashboard</h3>
336
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Monitor application health</p>
337
+ </div>
338
+ <span className="text-emerald-600 dark:text-emerald-400 text-sm group-hover:translate-x-0.5 transition-transform">&rarr;</span>
339
+ </div>
340
+ </a>
341
+ <a href="/api/ping" className="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-500/40 transition-colors">
342
+ <div className="flex items-center justify-between">
343
+ <div>
344
+ <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-200">API Ping</h3>
345
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Test API connectivity</p>
346
+ </div>
347
+ <span className="text-emerald-600 dark:text-emerald-400 text-sm group-hover:translate-x-0.5 transition-transform">&rarr;</span>
348
+ </div>
349
+ </a>
329
350
  </div>
330
351
 
331
- <div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
332
- <div className="px-5 py-4 border-b border-gray-800 flex items-center justify-between">
333
- <h2 className="text-sm font-bold text-gray-200">Users</h2>
334
- <span className="text-xs text-gray-500">{loading ? 'Loading...' : \`\${users.length} total\`}</span>
352
+ <div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">
353
+ <div className="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
354
+ <h2 className="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
355
+ <span className="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : \`\${users.length} total\`}</span>
335
356
  </div>
336
357
  <table className="w-full text-sm">
337
358
  <thead>
338
- <tr className="border-b border-gray-800 text-left text-xs text-gray-500 uppercase tracking-wider">
359
+ <tr className="border-b border-gray-100 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
339
360
  <th className="px-5 py-3 font-medium">Name</th>
340
361
  <th className="px-5 py-3 font-medium">Email</th>
341
362
  <th className="px-5 py-3 font-medium">Role</th>
342
363
  </tr>
343
364
  </thead>
344
- <tbody className="divide-y divide-gray-800/60">
365
+ <tbody className="divide-y divide-gray-100 dark:divide-gray-800/60">
345
366
  {users.map((u) => (
346
- <tr key={u.id} className="hover:bg-gray-900/50 transition-colors">
347
- <td className="px-5 py-3 text-gray-200">{u.name}</td>
348
- <td className="px-5 py-3 text-gray-400">{u.email}</td>
367
+ <tr key={u.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
368
+ <td className="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
369
+ <td className="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
349
370
  <td className="px-5 py-3">
350
371
  <span className={\`text-[10px] px-2 py-0.5 rounded-full font-medium \${
351
- u.role === 'admin' ? 'bg-purple-500/15 text-purple-300 border border-purple-500/20' : 'bg-gray-800 text-gray-400 border border-gray-700'
372
+ u.role === 'admin' ? 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-500/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700'
352
373
  }\`}>{u.role}</span>
353
374
  </td>
354
375
  </tr>
355
376
  ))}
356
377
  {users.length === 0 && !loading && (
357
- <tr><td colSpan={3} className="px-5 py-8 text-center text-gray-600">No users found</td></tr>
378
+ <tr><td colSpan={3} className="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
358
379
  )}
359
380
  </tbody>
360
381
  </table>