create-mantiq 0.1.6 → 0.1.7

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.6",
3
+ "version": "0.1.7",
4
4
  "description": "Scaffold a new MantiqJS application",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/kits/react.ts CHANGED
@@ -297,12 +297,41 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
297
297
  }
298
298
 
299
299
  return (
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">
302
- <div className="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
303
- <span className="text-sm font-bold text-gray-900 dark:text-white">{appName}</span>
300
+ <div className="min-h-screen flex bg-gray-50 dark:bg-gray-950">
301
+ {/* Sidebar */}
302
+ <aside className="w-60 fixed inset-y-0 left-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col z-30">
303
+ <div className="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
304
+ <span className="text-sm font-semibold text-gray-900 dark:text-white">{appName}</span>
305
+ </div>
306
+ <nav className="flex-1 px-3 py-3 space-y-0.5">
307
+ <a href="/dashboard" className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
308
+ <svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
309
+ Dashboard
310
+ </a>
311
+ <a href="#users-section" onClick={(e) => { e.preventDefault(); document.getElementById('users-section')?.scrollIntoView({ behavior: 'smooth' }) }} className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
312
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
313
+ Users
314
+ </a>
315
+ </nav>
316
+ <div className="px-3 py-3 mt-auto border-t border-gray-200 dark:border-gray-800 space-y-0.5">
317
+ <a href="/_heartbeat" className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
318
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064" /></svg>
319
+ Heartbeat
320
+ </a>
321
+ <a href="/api/ping" className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
322
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
323
+ API Ping
324
+ </a>
325
+ </div>
326
+ </aside>
327
+
328
+ {/* Main */}
329
+ <div className="flex-1 ml-60">
330
+ {/* Top bar */}
331
+ <header className="h-14 border-b border-gray-200 dark:border-gray-800 bg-white/90 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20 flex items-center justify-between px-6">
332
+ <h1 className="text-sm font-medium text-gray-900 dark:text-gray-200">Dashboard</h1>
304
333
  <div className="flex items-center gap-3">
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">
334
+ <button onClick={toggleTheme} className="p-1.5 rounded-lg text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" title="Toggle theme">
306
335
  {isDark ? (
307
336
  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
308
337
  <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" />
@@ -314,73 +343,52 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
314
343
  )}
315
344
  </button>
316
345
  <span className="text-xs text-gray-500 dark:text-gray-400">{currentUser?.name}</span>
317
- <button onClick={handleLogout}
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">
346
+ <button onClick={handleLogout} className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-1.5 transition-colors">
319
347
  Logout
320
348
  </button>
321
349
  </div>
322
- </div>
323
- </nav>
350
+ </header>
324
351
 
325
- <main className="max-w-5xl mx-auto px-6 py-8 space-y-6 animate-fade-up">
326
- <div>
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>
352
+ {/* Content */}
353
+ <main className="p-6 space-y-6 animate-fade-up">
354
+ <div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-6">
355
+ <h2 className="text-lg font-bold text-gray-900 dark:text-white">Welcome back, {currentUser?.name}</h2>
356
+ <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
357
+ </div>
330
358
 
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>
359
+ <div id="users-section" className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">
360
+ <div className="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
361
+ <h2 className="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
362
+ <span className="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : \`\${users.length} total\`}</span>
339
363
  </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>
350
- </div>
351
-
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>
356
- </div>
357
- <table className="w-full text-sm">
358
- <thead>
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">
360
- <th className="px-5 py-3 font-medium">Name</th>
361
- <th className="px-5 py-3 font-medium">Email</th>
362
- <th className="px-5 py-3 font-medium">Role</th>
363
- </tr>
364
- </thead>
365
- <tbody className="divide-y divide-gray-100 dark:divide-gray-800/60">
366
- {users.map((u) => (
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>
370
- <td className="px-5 py-3">
371
- <span className={\`text-[10px] px-2 py-0.5 rounded-full font-medium \${
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'
373
- }\`}>{u.role}</span>
374
- </td>
364
+ <table className="w-full text-sm">
365
+ <thead>
366
+ <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">
367
+ <th className="px-5 py-3 font-medium">Name</th>
368
+ <th className="px-5 py-3 font-medium">Email</th>
369
+ <th className="px-5 py-3 font-medium">Role</th>
375
370
  </tr>
376
- ))}
377
- {users.length === 0 && !loading && (
378
- <tr><td colSpan={3} className="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
379
- )}
380
- </tbody>
381
- </table>
382
- </div>
383
- </main>
371
+ </thead>
372
+ <tbody className="divide-y divide-gray-100 dark:divide-gray-800/60">
373
+ {users.map((u) => (
374
+ <tr key={u.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
375
+ <td className="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
376
+ <td className="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
377
+ <td className="px-5 py-3">
378
+ <span className={\`text-[10px] px-2 py-0.5 rounded-full font-medium \${
379
+ 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'
380
+ }\`}>{u.role}</span>
381
+ </td>
382
+ </tr>
383
+ ))}
384
+ {users.length === 0 && !loading && (
385
+ <tr><td colSpan={3} className="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
386
+ )}
387
+ </tbody>
388
+ </table>
389
+ </div>
390
+ </main>
391
+ </div>
384
392
  </div>
385
393
  )
386
394
  }
@@ -96,6 +96,13 @@ export function render(_url: string, data?: Record<string, any>) {
96
96
 
97
97
  $: PageComponent = pages[currentPage] ?? null
98
98
 
99
+ // Initialize theme immediately to prevent flash
100
+ if (typeof window !== 'undefined') {
101
+ const theme = localStorage.getItem('theme') ||
102
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
103
+ document.documentElement.classList.toggle('dark', theme === 'dark')
104
+ }
105
+
99
106
  async function navigate(href: string) {
100
107
  const res = await fetch(href, {
101
108
  headers: { 'X-Mantiq': 'true', Accept: 'application/json' },
@@ -272,7 +279,7 @@ export function render(_url: string, data?: Record<string, any>) {
272
279
  `,
273
280
 
274
281
  'src/pages/Dashboard.svelte': `<script lang="ts">
275
- import { onMount } from 'svelte'
282
+ import { onMount, getContext } from 'svelte'
276
283
  import { api, post } from '../lib/api.ts'
277
284
 
278
285
  export let appName: string = '${ctx.name}'
@@ -280,10 +287,21 @@ export function render(_url: string, data?: Record<string, any>) {
280
287
  export let users: any[] = []
281
288
  export let navigate: (href: string) => void
282
289
 
290
+ const ctxNavigate = getContext<(href: string) => void>('navigate')
291
+
283
292
  let localUsers: any[] = users ?? []
284
293
  let loading = !users?.length
285
294
  let isDark = true
286
295
 
296
+ const navItems = [
297
+ { label: 'Dashboard', icon: 'grid', href: '/dashboard', active: true },
298
+ ]
299
+
300
+ const bottomLinks = [
301
+ { label: 'Heartbeat', icon: 'heart', href: '/heartbeat' },
302
+ { label: 'API Ping', icon: 'zap', href: '/api/ping' },
303
+ ]
304
+
287
305
  async function fetchUsers() {
288
306
  loading = true
289
307
  const { ok, data } = await api('/api/users')
@@ -293,7 +311,7 @@ export function render(_url: string, data?: Record<string, any>) {
293
311
 
294
312
  async function handleLogout() {
295
313
  await post('/logout', {})
296
- navigate('/login')
314
+ ;(navigate ?? ctxNavigate)('/login')
297
315
  }
298
316
 
299
317
  function toggleTheme() {
@@ -310,10 +328,56 @@ export function render(_url: string, data?: Record<string, any>) {
310
328
  })
311
329
  </script>
312
330
 
313
- <div class="min-h-screen bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors">
314
- <nav class="border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
315
- <div class="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
316
- <span class="text-sm font-bold text-gray-900 dark:text-white">{appName}</span>
331
+ <div class="min-h-screen flex bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors">
332
+ <!-- Sidebar -->
333
+ <aside class="w-60 flex-shrink-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col fixed inset-y-0 left-0 z-30">
334
+ <!-- App name -->
335
+ <div class="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
336
+ <div class="flex items-center gap-2.5">
337
+ <div class="w-7 h-7 rounded-lg bg-emerald-600 flex items-center justify-center">
338
+ <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
339
+ </div>
340
+ <span class="text-sm font-bold text-gray-900 dark:text-white">{appName}</span>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- Nav items -->
345
+ <nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
346
+ {#each navItems as item}
347
+ <a href={item.href}
348
+ class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors
349
+ {item.active
350
+ ? 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
351
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'}">
352
+ {#if item.icon === 'grid'}
353
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
354
+ {/if}
355
+ {item.label}
356
+ </a>
357
+ {/each}
358
+ </nav>
359
+
360
+ <!-- Bottom links -->
361
+ <div class="px-3 py-4 border-t border-gray-200 dark:border-gray-800 space-y-1">
362
+ {#each bottomLinks as link}
363
+ <a href={link.href}
364
+ class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
365
+ {#if link.icon === 'heart'}
366
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /></svg>
367
+ {:else if link.icon === 'zap'}
368
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
369
+ {/if}
370
+ {link.label}
371
+ </a>
372
+ {/each}
373
+ </div>
374
+ </aside>
375
+
376
+ <!-- Main content -->
377
+ <div class="flex-1 ml-60 flex flex-col min-h-screen">
378
+ <!-- Header -->
379
+ <header class="h-14 flex items-center justify-between px-6 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
380
+ <h1 class="text-sm font-semibold text-gray-900 dark:text-white">Dashboard</h1>
317
381
  <div class="flex items-center gap-3">
318
382
  <button on:click={toggleTheme} class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" aria-label="Toggle theme">
319
383
  {#if isDark}
@@ -325,66 +389,48 @@ export function render(_url: string, data?: Record<string, any>) {
325
389
  <span class="text-xs text-gray-500 dark:text-gray-400">{currentUser?.name}</span>
326
390
  <button on:click={handleLogout} class="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">Logout</button>
327
391
  </div>
328
- </div>
329
- </nav>
330
-
331
- <main class="max-w-5xl mx-auto px-6 py-8 space-y-6 animate-fade-up">
332
- <div>
333
- <h1 class="text-xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
334
- <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Welcome back, {currentUser?.name}.</p>
335
- </div>
392
+ </header>
393
+
394
+ <!-- Page content -->
395
+ <main class="flex-1 px-6 py-8 space-y-6 animate-fade-up">
396
+ <!-- Welcome card -->
397
+ <div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-6">
398
+ <h2 class="text-lg font-bold text-gray-900 dark:text-white">Welcome back, {currentUser?.name}</h2>
399
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
400
+ </div>
336
401
 
337
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
338
- <a href="/heartbeat" class="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-300 dark:hover:border-emerald-500/40 transition-colors">
339
- <div class="flex items-center justify-between">
340
- <div>
341
- <h3 class="text-sm font-semibold text-gray-900 dark:text-white">Heartbeat Dashboard</h3>
342
- <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Monitor application health</p>
343
- </div>
344
- <span class="text-emerald-600 dark:text-emerald-400 group-hover:translate-x-0.5 transition-transform">&rarr;</span>
345
- </div>
346
- </a>
347
- <a href="/api/ping" class="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-300 dark:hover:border-emerald-500/40 transition-colors">
348
- <div class="flex items-center justify-between">
349
- <div>
350
- <h3 class="text-sm font-semibold text-gray-900 dark:text-white">API Ping</h3>
351
- <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Test API connectivity</p>
352
- </div>
353
- <span class="text-emerald-600 dark:text-emerald-400 group-hover:translate-x-0.5 transition-transform">&rarr;</span>
402
+ <!-- Users table -->
403
+ <div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">
404
+ <div class="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
405
+ <h2 class="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
406
+ <span class="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : localUsers.length + ' total'}</span>
354
407
  </div>
355
- </a>
356
- </div>
357
-
358
- <div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">
359
- <div class="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
360
- <h2 class="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
361
- <span class="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : localUsers.length + ' total'}</span>
362
- </div>
363
- <table class="w-full text-sm">
364
- <thead>
365
- <tr class="border-b border-gray-200 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
366
- <th class="px-5 py-3 font-medium">Name</th>
367
- <th class="px-5 py-3 font-medium">Email</th>
368
- <th class="px-5 py-3 font-medium">Role</th>
369
- </tr>
370
- </thead>
371
- <tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
372
- {#each localUsers as u (u.id)}
373
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors">
374
- <td class="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
375
- <td class="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
376
- <td class="px-5 py-3">
377
- <span class="text-[10px] px-2 py-0.5 rounded-full font-medium border {u.role === 'admin' ? 'bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'}">{u.role}</span>
378
- </td>
408
+ <table class="w-full text-sm">
409
+ <thead>
410
+ <tr class="border-b border-gray-200 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
411
+ <th class="px-5 py-3 font-medium">Name</th>
412
+ <th class="px-5 py-3 font-medium">Email</th>
413
+ <th class="px-5 py-3 font-medium">Role</th>
379
414
  </tr>
380
- {/each}
381
- {#if localUsers.length === 0 && !loading}
382
- <tr><td colspan="3" class="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
383
- {/if}
384
- </tbody>
385
- </table>
386
- </div>
387
- </main>
415
+ </thead>
416
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
417
+ {#each localUsers as u (u.id)}
418
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors">
419
+ <td class="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
420
+ <td class="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
421
+ <td class="px-5 py-3">
422
+ <span class="text-[10px] px-2 py-0.5 rounded-full font-medium border {u.role === 'admin' ? 'bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/20' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'}">{u.role}</span>
423
+ </td>
424
+ </tr>
425
+ {/each}
426
+ {#if localUsers.length === 0 && !loading}
427
+ <tr><td colspan="3" class="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
428
+ {/if}
429
+ </tbody>
430
+ </table>
431
+ </div>
432
+ </main>
433
+ </div>
388
434
  </div>
389
435
  `,
390
436
  }
package/src/kits/vue.ts CHANGED
@@ -120,7 +120,7 @@ onMounted(() => {
120
120
 
121
121
  // Initialize theme: localStorage > system preference > dark default
122
122
  const theme = localStorage.getItem('theme') ||
123
- (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'dark')
123
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
124
124
  document.documentElement.classList.toggle('dark', theme === 'dark')
125
125
  })
126
126
 
@@ -327,7 +327,7 @@ async function handleSubmit() {
327
327
  `,
328
328
 
329
329
  'src/pages/Dashboard.vue': `<script setup lang="ts">
330
- import { ref, onMounted } from 'vue'
330
+ import { ref, onMounted, inject } from 'vue'
331
331
  import { api, post } from '../lib/api.ts'
332
332
 
333
333
  const props = defineProps<{
@@ -341,6 +341,7 @@ const appName = props.appName ?? '${ctx.name}'
341
341
  const users = ref(props.users ?? [])
342
342
  const loading = ref(!props.users?.length)
343
343
  const isDark = ref(true)
344
+ const nav = inject<(href: string) => void>('navigate', props.navigate)
344
345
 
345
346
  function toggleTheme() {
346
347
  const dark = document.documentElement.classList.toggle('dark')
@@ -361,21 +362,64 @@ async function handleLogout() {
361
362
  }
362
363
 
363
364
  onMounted(() => {
364
- const theme = localStorage.getItem('theme') ||
365
- (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'dark')
366
- document.documentElement.classList.toggle('dark', theme === 'dark')
367
- isDark.value = theme === 'dark'
368
-
365
+ isDark.value = document.documentElement.classList.contains('dark')
369
366
  if (!props.users?.length) fetchUsers()
370
367
  })
371
368
  </script>
372
369
 
373
370
  <template>
374
- <div class="min-h-screen bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors">
375
- <!-- Navbar -->
376
- <nav class="border-b border-gray-200 dark:border-gray-800/80 bg-white/80 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
377
- <div class="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
371
+ <div class="min-h-screen flex bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors">
372
+ <!-- Sidebar -->
373
+ <aside class="w-60 flex-shrink-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col fixed inset-y-0 left-0 z-30">
374
+ <!-- App name -->
375
+ <div class="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
378
376
  <span class="text-sm font-bold text-gray-900 dark:text-white">{{ appName }}</span>
377
+ </div>
378
+
379
+ <!-- Navigation -->
380
+ <nav class="flex-1 px-3 py-4 space-y-1">
381
+ <a
382
+ href="/dashboard"
383
+ class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
384
+ >
385
+ <!-- Dashboard icon -->
386
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
387
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
388
+ </svg>
389
+ Dashboard
390
+ </a>
391
+ </nav>
392
+
393
+ <!-- Bottom links -->
394
+ <div class="px-3 py-4 border-t border-gray-200 dark:border-gray-800 space-y-1">
395
+ <a
396
+ href="/heartbeat"
397
+ class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
398
+ >
399
+ <!-- Heart/pulse icon -->
400
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
401
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
402
+ </svg>
403
+ Heartbeat
404
+ </a>
405
+ <a
406
+ href="/api/ping"
407
+ class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
408
+ >
409
+ <!-- Signal/wifi icon -->
410
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
411
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.858 15.355-5.858 21.213 0" />
412
+ </svg>
413
+ API Ping
414
+ </a>
415
+ </div>
416
+ </aside>
417
+
418
+ <!-- Main area -->
419
+ <div class="flex-1 ml-60 flex flex-col min-h-screen">
420
+ <!-- Header -->
421
+ <header class="h-14 flex items-center justify-between px-6 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
422
+ <h1 class="text-sm font-semibold text-gray-900 dark:text-white">Dashboard</h1>
379
423
  <div class="flex items-center gap-3">
380
424
  <!-- Dark/Light toggle -->
381
425
  <button
@@ -400,80 +444,53 @@ onMounted(() => {
400
444
  Logout
401
445
  </button>
402
446
  </div>
403
- </div>
404
- </nav>
405
-
406
- <main class="max-w-5xl mx-auto px-6 py-8 space-y-6">
407
- <!-- Welcome section -->
408
- <div class="animate-fade-up">
409
- <h1 class="text-xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
410
- <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Welcome back, {{ currentUser?.name }}.</p>
411
- </div>
447
+ </header>
448
+
449
+ <!-- Content -->
450
+ <main class="flex-1 px-6 py-8 space-y-6">
451
+ <!-- Welcome card -->
452
+ <div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-6 animate-fade-up">
453
+ <h2 class="text-lg font-bold text-gray-900 dark:text-white">Welcome back, {{ currentUser?.name }}</h2>
454
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
455
+ </div>
412
456
 
413
- <!-- Quick links -->
414
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 animate-fade-up">
415
- <a
416
- href="/heartbeat"
417
- class="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-300 dark:hover:border-emerald-800 transition-colors"
418
- >
419
- <div class="flex items-center justify-between">
420
- <div>
421
- <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Heartbeat Dashboard</h3>
422
- <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Monitor application health</p>
423
- </div>
424
- <span class="text-emerald-600 dark:text-emerald-400 group-hover:translate-x-0.5 transition-transform">&rarr;</span>
457
+ <!-- Users table -->
458
+ <div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden animate-fade-up">
459
+ <div class="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
460
+ <h2 class="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
461
+ <span class="text-xs text-gray-500 dark:text-gray-400">{{ loading ? 'Loading...' : users.length + ' total' }}</span>
425
462
  </div>
426
- </a>
427
- <a
428
- href="/api/ping"
429
- class="group bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-5 hover:border-emerald-300 dark:hover:border-emerald-800 transition-colors"
430
- >
431
- <div class="flex items-center justify-between">
432
- <div>
433
- <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">API Ping</h3>
434
- <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Test API connectivity</p>
435
- </div>
436
- <span class="text-emerald-600 dark:text-emerald-400 group-hover:translate-x-0.5 transition-transform">&rarr;</span>
437
- </div>
438
- </a>
439
- </div>
440
-
441
- <!-- Users table -->
442
- <div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden animate-fade-up">
443
- <div class="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
444
- <h2 class="text-sm font-bold text-gray-800 dark:text-gray-200">Users</h2>
445
- <span class="text-xs text-gray-500 dark:text-gray-400">{{ loading ? 'Loading...' : users.length + ' total' }}</span>
463
+ <table class="w-full text-sm">
464
+ <thead>
465
+ <tr class="border-b border-gray-200 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
466
+ <th class="px-5 py-3 font-medium">Name</th>
467
+ <th class="px-5 py-3 font-medium">Email</th>
468
+ <th class="px-5 py-3 font-medium">Role</th>
469
+ </tr>
470
+ </thead>
471
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
472
+ <tr v-for="u in users" :key="u.id" class="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
473
+ <td class="px-5 py-3 text-gray-900 dark:text-gray-200">{{ u.name }}</td>
474
+ <td class="px-5 py-3 text-gray-500 dark:text-gray-400">{{ u.email }}</td>
475
+ <td class="px-5 py-3">
476
+ <span
477
+ :class="u.role === 'admin'
478
+ ? 'bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/20'
479
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'"
480
+ class="text-[10px] px-2 py-0.5 rounded-full font-medium border"
481
+ >
482
+ {{ u.role }}
483
+ </span>
484
+ </td>
485
+ </tr>
486
+ <tr v-if="users.length === 0 && !loading">
487
+ <td colspan="3" class="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td>
488
+ </tr>
489
+ </tbody>
490
+ </table>
446
491
  </div>
447
- <table class="w-full text-sm">
448
- <thead>
449
- <tr class="border-b border-gray-200 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
450
- <th class="px-5 py-3 font-medium">Name</th>
451
- <th class="px-5 py-3 font-medium">Email</th>
452
- <th class="px-5 py-3 font-medium">Role</th>
453
- </tr>
454
- </thead>
455
- <tbody class="divide-y divide-gray-100 dark:divide-gray-800/60">
456
- <tr v-for="u in users" :key="u.id" class="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
457
- <td class="px-5 py-3 text-gray-800 dark:text-gray-200">{{ u.name }}</td>
458
- <td class="px-5 py-3 text-gray-500 dark:text-gray-400">{{ u.email }}</td>
459
- <td class="px-5 py-3">
460
- <span
461
- :class="u.role === 'admin'
462
- ? 'bg-emerald-50 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/20'
463
- : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'"
464
- class="text-[10px] px-2 py-0.5 rounded-full font-medium border"
465
- >
466
- {{ u.role }}
467
- </span>
468
- </td>
469
- </tr>
470
- <tr v-if="users.length === 0 && !loading">
471
- <td colspan="3" class="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td>
472
- </tr>
473
- </tbody>
474
- </table>
475
- </div>
476
- </main>
492
+ </main>
493
+ </div>
477
494
  </div>
478
495
  </template>
479
496
  `,
package/src/templates.ts CHANGED
@@ -869,13 +869,13 @@ export default {
869
869
 
870
870
  // ── routes/web.ts ───────────────────────────────────────────────────────
871
871
  templates['routes/web.ts'] = `import type { Router } from '@mantiq/core'
872
- import { MantiqResponse } from '@mantiq/core'
872
+ import { HomeController } from '../app/Http/Controllers/HomeController.ts'
873
873
  import { PageController } from '../app/Http/Controllers/PageController.ts'
874
874
  import { AuthController } from '../app/Http/Controllers/AuthController.ts'
875
875
 
876
876
  export default function (router: Router) {
877
- // Redirect root to dashboard
878
- router.get('/', () => MantiqResponse.redirect('/dashboard'))
877
+ // Welcome page shows auth-aware buttons
878
+ router.get('/', [HomeController, 'index'])
879
879
 
880
880
  // Page routes — each returns HTML (first load) or JSON (client navigation)
881
881
  router.get('/dashboard', [PageController, 'dashboard']).middleware('auth')
@@ -904,6 +904,124 @@ export default function (router: Router) {
904
904
  return MantiqResponse.json({ data: users.map((u: any) => u.toObject()) })
905
905
  }).middleware('auth')
906
906
  }
907
+ `
908
+
909
+ // ── HomeController (welcome page, auth-aware) ─────────────────────────
910
+ templates['app/Http/Controllers/HomeController.ts'] = `import type { MantiqRequest } from '@mantiq/core'
911
+ import { config } from '@mantiq/core'
912
+ import { auth } from '@mantiq/auth'
913
+
914
+ export class HomeController {
915
+ async index(request: MantiqRequest): Promise<Response> {
916
+ const appName = config('app.name') ?? 'MantiqJS'
917
+ const appEnv = config('app.env') ?? 'production'
918
+ const debug = config('app.debug') ? 'Enabled' : 'Disabled'
919
+ const bunVersion = typeof Bun !== 'undefined' ? Bun.version : 'unknown'
920
+
921
+ let mantiqVersion = '0.0.0'
922
+ try {
923
+ const pkg = await Bun.file(require.resolve('@mantiq/core/package.json')).json()
924
+ mantiqVersion = pkg.version
925
+ } catch { /* fallback */ }
926
+
927
+ // Check auth state
928
+ let isLoggedIn = false
929
+ let userName = ''
930
+ try {
931
+ const manager = auth()
932
+ manager.setRequest(request)
933
+ const user = await manager.user()
934
+ if (user) {
935
+ isLoggedIn = true
936
+ userName = (user as any).getAttribute?.('name') ?? ''
937
+ }
938
+ } catch { /* not authenticated or migration not run */ }
939
+
940
+ // Check if migration has been run (users table exists)
941
+ let migrated = false
942
+ try {
943
+ const { User } = await import('../../Models/User.ts')
944
+ await User.count()
945
+ migrated = true
946
+ } catch { /* table doesn't exist yet */ }
947
+
948
+ const authButtons = isLoggedIn
949
+ ? \\\`<a class="l l-accent" href="/dashboard">Dashboard<span class="a">&rarr;</span></a>
950
+ <a class="l" href="/_heartbeat">Heartbeat<span class="a">&rarr;</span></a>\\\`
951
+ : migrated
952
+ ? \\\`<a class="l l-accent" href="/login">Sign in<span class="a">&rarr;</span></a>
953
+ <a class="l" href="/register">Register<span class="a">&rarr;</span></a>\\\`
954
+ : \\\`<a class="l" href="/_heartbeat">Heartbeat<span class="a">&rarr;</span></a>
955
+ <a class="l" href="/api/ping">API Ping<span class="a">&rarr;</span></a>\\\`
956
+
957
+ const html = \\\`<!DOCTYPE html>
958
+ <html lang="en">
959
+ <head>
960
+ <meta charset="UTF-8">
961
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
962
+ <title>\\\${appName}</title>
963
+ <style>
964
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
965
+ body{
966
+ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
967
+ background:#0a0a0b;color:#fafafa;min-height:100vh;
968
+ display:flex;align-items:center;justify-content:center;
969
+ -webkit-font-smoothing:antialiased;
970
+ }
971
+ .c{width:100%;max-width:460px;padding:32px;animation:up .5s ease}
972
+ @keyframes up{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
973
+ .w{font-size:28px;font-weight:600;letter-spacing:-0.04em;color:#fafafa}
974
+ .w .d{color:#10b981}
975
+ .v{font-family:'SF Mono',ui-monospace,monospace;font-size:12px;color:#52525b;margin-top:6px}
976
+ hr{border:none;border-top:1px solid #1e1e1e;margin:24px 0}
977
+ .g{display:grid;grid-template-columns:1fr 1fr;gap:8px}
978
+ .l{
979
+ background:#111113;border:1px solid #1e1e1e;border-radius:8px;
980
+ padding:14px 16px;text-decoration:none;color:#a1a1aa;font-size:13px;
981
+ display:flex;align-items:center;justify-content:space-between;
982
+ transition:border-color .15s,color .15s;
983
+ }
984
+ .l:hover{border-color:#27272a;color:#34d399}
985
+ .l-accent{border-color:#10b981;color:#fafafa}
986
+ .l-accent:hover{background:#10b981;color:#0a0a0b}
987
+ .l .a{color:#52525b;font-size:11px;transition:color .15s}
988
+ .l:hover .a{color:#34d399}
989
+ .l-accent .a{color:#10b981}
990
+ .l-accent:hover .a{color:#0a0a0b}
991
+ .e{
992
+ margin-top:24px;font-family:'SF Mono',ui-monospace,monospace;
993
+ font-size:11px;color:#3f3f46;line-height:2;
994
+ }
995
+ .e span{color:#52525b}
996
+ .g2{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}
997
+ </style>
998
+ </head>
999
+ <body>
1000
+ <div class="c">
1001
+ <div class="w"><span class="d">.</span>mantiq</div>
1002
+ <div class="v">v\\\${mantiqVersion} — \\\${appName}</div>
1003
+ <hr>
1004
+ <div class="g">
1005
+ \\\${authButtons}
1006
+ </div>
1007
+ <div class="g2">
1008
+ <a class="l" href="https://github.com/mantiqjs/mantiq" target="_blank" rel="noopener">GitHub<span class="a">&nearr;</span></a>
1009
+ <a class="l" href="https://www.npmjs.com/org/mantiq" target="_blank" rel="noopener">npm<span class="a">&nearr;</span></a>
1010
+ </div>
1011
+ <div class="e">
1012
+ <span>Runtime</span> Bun \\\${bunVersion}<br>
1013
+ <span>Environment</span> \\\${appEnv}<br>
1014
+ <span>Debug</span> \\\${debug}
1015
+ </div>
1016
+ </div>
1017
+ </body>
1018
+ </html>\\\`
1019
+
1020
+ return new Response(html, {
1021
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
1022
+ })
1023
+ }
1024
+ }
907
1025
  `
908
1026
 
909
1027
  // ── PageController (SSR + universal routing) ────────────────────────────