create-mantiq 0.3.0 → 0.4.0

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.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Scaffold a new MantiqJS application",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/kits/react.ts CHANGED
@@ -275,6 +275,8 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
275
275
  const [users, setUsers] = useState<User[]>(initialUsers ?? [])
276
276
  const [loading, setLoading] = useState(!initialUsers?.length)
277
277
  const [sidebarOpen, setSidebarOpen] = useState(false)
278
+ const [collapsed, setCollapsed] = useState(false)
279
+ const [accountOpen, setAccountOpen] = useState(false)
278
280
  const [isDark, setIsDark] = useState(() =>
279
281
  typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : true
280
282
  )
@@ -307,34 +309,39 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
307
309
  {sidebarOpen && <div className="fixed inset-0 bg-black/50 z-30 lg:hidden" onClick={() => setSidebarOpen(false)} />}
308
310
 
309
311
  {/* Sidebar */}
310
- <aside className={\`fixed inset-y-0 left-0 w-60 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col z-40 transition-transform duration-200 lg:translate-x-0 \${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}\`}>
312
+ <aside className={\`fixed inset-y-0 left-0 \${sidebarOpen ? 'w-60 translate-x-0' : '-translate-x-full'} \${collapsed ? 'lg:w-16' : 'lg:w-60'} bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col z-40 transition-all duration-200 lg:translate-x-0\`}>
311
313
  <div className="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
312
- <span className="text-sm font-semibold text-gray-900 dark:text-white">{appName}</span>
314
+ <span className={\`text-sm font-semibold text-gray-900 dark:text-white \${collapsed ? 'lg:hidden' : ''}\`}>{appName}</span>
313
315
  </div>
314
316
  <nav className="flex-1 px-3 py-3 space-y-0.5">
315
317
  <a href="/dashboard" onClick={() => setSidebarOpen(false)} 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">
316
- <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>
317
- Dashboard
318
+ <svg className="w-4 h-4 text-gray-500 dark:text-gray-400 shrink-0" 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>
319
+ <span className={\`\${collapsed ? 'lg:hidden' : ''}\`}>Dashboard</span>
318
320
  </a>
319
321
  <a href="#users-section" onClick={(e) => { e.preventDefault(); setSidebarOpen(false); 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">
320
- <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>
321
- Users
322
+ <svg className="w-4 h-4 shrink-0" 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>
323
+ <span className={\`\${collapsed ? 'lg:hidden' : ''}\`}>Users</span>
322
324
  </a>
323
325
  </nav>
326
+
327
+ {/* Collapse toggle */}
328
+ <div className="px-3 py-2 hidden lg:block">
329
+ <button onClick={() => setCollapsed(!collapsed)} className="flex items-center justify-center w-full px-2.5 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
330
+ <span className="text-xs font-mono">{collapsed ? '>>' : '<<'}</span>
331
+ </button>
332
+ </div>
333
+
334
+ {/* Bottom links */}
324
335
  <div className="px-3 py-3 mt-auto border-t border-gray-200 dark:border-gray-800 space-y-0.5">
325
- <a href="/_heartbeat" onClick={() => setSidebarOpen(false)} 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">
326
- <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>
327
- Heartbeat
328
- </a>
329
- <a href="/api/ping" onClick={() => setSidebarOpen(false)} 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">
330
- <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>
331
- API Ping
336
+ <a href="https://github.com/mantiqjs/mantiq" target="_blank" rel="noopener" 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">
337
+ <svg className="w-4 h-4 shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" /></svg>
338
+ <span className={\`\${collapsed ? 'lg:hidden' : ''}\`}>Documentation</span>
332
339
  </a>
333
340
  </div>
334
341
  </aside>
335
342
 
336
343
  {/* Main */}
337
- <div className="flex-1 lg:ml-60">
344
+ <div className={\`flex-1 \${collapsed ? 'lg:ml-16' : 'lg:ml-60'} transition-all duration-200\`}>
338
345
  {/* Top bar */}
339
346
  <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">
340
347
  <div className="flex items-center">
@@ -355,10 +362,30 @@ export default function Dashboard({ appName = '${ctx.name}', currentUser, users:
355
362
  </svg>
356
363
  )}
357
364
  </button>
358
- <span className="text-xs text-gray-500 dark:text-gray-400">{currentUser?.name}</span>
359
- <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">
360
- Logout
361
- </button>
365
+ {/* Account dropdown */}
366
+ <div className="relative">
367
+ <button onClick={() => setAccountOpen(!accountOpen)} className="flex items-center gap-2 rounded-lg px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
368
+ <div className="w-7 h-7 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center text-xs font-medium text-emerald-700 dark:text-emerald-300">
369
+ {currentUser?.name?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)}
370
+ </div>
371
+ <span className="text-sm text-gray-700 dark:text-gray-300 hidden sm:block">{currentUser?.name}</span>
372
+ <svg className="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
373
+ </button>
374
+ {accountOpen && (
375
+ <>
376
+ <div className="fixed inset-0 z-40" onClick={() => setAccountOpen(false)} />
377
+ <div className="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-lg py-1 z-50">
378
+ <div className="px-3 py-2 border-b border-gray-100 dark:border-gray-800">
379
+ <div className="text-sm font-medium text-gray-900 dark:text-white">{currentUser?.name}</div>
380
+ <div className="text-xs text-gray-500 dark:text-gray-400">{currentUser?.email}</div>
381
+ </div>
382
+ <button onClick={handleLogout} className="w-full text-left px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white transition-colors">
383
+ Sign out
384
+ </button>
385
+ </div>
386
+ </>
387
+ )}
388
+ </div>
362
389
  </div>
363
390
  </header>
364
391
 
@@ -297,15 +297,10 @@ export function render(_url: string, data?: Record<string, any>) {
297
297
  let loading = !users?.length
298
298
  let isDark = true
299
299
  let sidebarOpen = false
300
+ let collapsed = false
301
+ let accountOpen = false
300
302
 
301
- const navItems = [
302
- { label: 'Dashboard', icon: 'grid', href: '/dashboard', active: true },
303
- ]
304
-
305
- const bottomLinks = [
306
- { label: 'Heartbeat', icon: 'heart', href: '/heartbeat' },
307
- { label: 'API Ping', icon: 'zap', href: '/api/ping' },
308
- ]
303
+ $: userInitials = currentUser?.name?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) ?? ''
309
304
 
310
305
  async function fetchUsers() {
311
306
  loading = true
@@ -340,53 +335,48 @@ export function render(_url: string, data?: Record<string, any>) {
340
335
  {/if}
341
336
 
342
337
  <!-- Sidebar -->
343
- <aside class="fixed inset-y-0 left-0 w-60 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col z-40 transition-transform duration-200 lg:translate-x-0 {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}">
338
+ <aside class="fixed inset-y-0 left-0 {sidebarOpen ? 'w-60 translate-x-0' : '-translate-x-full'} {collapsed ? 'lg:w-16' : 'lg:w-60'} bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col z-40 transition-all duration-200 lg:translate-x-0">
344
339
  <!-- App name -->
345
340
  <div class="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
346
341
  <div class="flex items-center gap-2.5">
347
- <div class="w-7 h-7 rounded-lg bg-emerald-600 flex items-center justify-center">
342
+ <div class="w-7 h-7 rounded-lg bg-emerald-600 flex items-center justify-center shrink-0">
348
343
  <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>
349
344
  </div>
350
- <span class="text-sm font-bold text-gray-900 dark:text-white">{appName}</span>
345
+ <span class="text-sm font-bold text-gray-900 dark:text-white {collapsed ? 'lg:hidden' : ''}">{appName}</span>
351
346
  </div>
352
347
  </div>
353
348
 
354
349
  <!-- Nav items -->
355
350
  <nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
356
- {#each navItems as item}
357
- <a href={item.href}
358
- on:click={() => sidebarOpen = false}
359
- class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors
360
- {item.active
361
- ? 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
362
- : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'}">
363
- {#if item.icon === 'grid'}
364
- <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>
365
- {/if}
366
- {item.label}
367
- </a>
368
- {/each}
351
+ <a href="/dashboard"
352
+ on:click={() => sidebarOpen = false}
353
+ class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300">
354
+ <svg class="w-4 h-4 shrink-0" 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>
355
+ <span class="{collapsed ? 'lg:hidden' : ''}">Dashboard</span>
356
+ </a>
369
357
  </nav>
370
358
 
359
+ <!-- Collapse toggle -->
360
+ <div class="px-3 py-2 hidden lg:block">
361
+ <button on:click={() => collapsed = !collapsed} class="flex items-center justify-center w-full px-2.5 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
362
+ <span class="text-xs font-mono">{collapsed ? '>>' : '<<'}</span>
363
+ </button>
364
+ </div>
365
+
371
366
  <!-- Bottom links -->
372
- <div class="px-3 py-4 border-t border-gray-200 dark:border-gray-800 space-y-1">
373
- {#each bottomLinks as link}
374
- <a href={link.href}
375
- on:click={() => sidebarOpen = false}
376
- 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">
377
- {#if link.icon === 'heart'}
378
- <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>
379
- {:else if link.icon === 'zap'}
380
- <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>
381
- {/if}
382
- {link.label}
383
- </a>
384
- {/each}
367
+ <div class="px-3 py-4 mt-auto border-t border-gray-200 dark:border-gray-800 space-y-1">
368
+ <a href="https://github.com/mantiqjs/mantiq"
369
+ target="_blank"
370
+ rel="noopener"
371
+ 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">
372
+ <svg class="w-4 h-4 shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" /></svg>
373
+ <span class="{collapsed ? 'lg:hidden' : ''}">Documentation</span>
374
+ </a>
385
375
  </div>
386
376
  </aside>
387
377
 
388
378
  <!-- Main content -->
389
- <div class="flex-1 lg:ml-60 flex flex-col min-h-screen">
379
+ <div class="flex-1 {collapsed ? 'lg:ml-16' : 'lg:ml-60'} flex flex-col min-h-screen transition-all duration-200">
390
380
  <!-- Header -->
391
381
  <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">
392
382
  <div class="flex items-center">
@@ -403,8 +393,28 @@ export function render(_url: string, data?: Record<string, any>) {
403
393
  <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="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" /></svg>
404
394
  {/if}
405
395
  </button>
406
- <span class="text-xs text-gray-500 dark:text-gray-400">{currentUser?.name}</span>
407
- <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>
396
+ <!-- Account dropdown -->
397
+ <div class="relative">
398
+ <button on:click={() => accountOpen = !accountOpen} class="flex items-center gap-2 rounded-lg px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
399
+ <div class="w-7 h-7 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center text-xs font-medium text-emerald-700 dark:text-emerald-300">
400
+ {userInitials}
401
+ </div>
402
+ <span class="text-sm text-gray-700 dark:text-gray-300 hidden sm:block">{currentUser?.name}</span>
403
+ <svg class="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>
404
+ </button>
405
+ {#if accountOpen}
406
+ <div class="fixed inset-0 z-40" on:click={() => accountOpen = false}></div>
407
+ <div class="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-lg py-1 z-50">
408
+ <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-800">
409
+ <div class="text-sm font-medium text-gray-900 dark:text-white">{currentUser?.name}</div>
410
+ <div class="text-xs text-gray-500 dark:text-gray-400">{currentUser?.email}</div>
411
+ </div>
412
+ <button on:click={handleLogout} class="w-full text-left px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white transition-colors">
413
+ Sign out
414
+ </button>
415
+ </div>
416
+ {/if}
417
+ </div>
408
418
  </div>
409
419
  </header>
410
420
 
package/src/kits/vue.ts CHANGED
@@ -331,7 +331,7 @@ async function handleSubmit() {
331
331
  `,
332
332
 
333
333
  'src/pages/Dashboard.vue': `<script setup lang="ts">
334
- import { ref, onMounted, inject } from 'vue'
334
+ import { ref, computed, onMounted, inject } from 'vue'
335
335
  import { api, post } from '../lib/api.ts'
336
336
 
337
337
  const props = defineProps<{
@@ -346,8 +346,14 @@ const users = ref(props.users ?? [])
346
346
  const loading = ref(!props.users?.length)
347
347
  const isDark = ref(true)
348
348
  const sidebarOpen = ref(false)
349
+ const collapsed = ref(false)
350
+ const accountOpen = ref(false)
349
351
  const nav = inject<(href: string) => void>('navigate', props.navigate)
350
352
 
353
+ const userInitials = computed(() => {
354
+ return props.currentUser?.name?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) ?? ''
355
+ })
356
+
351
357
  function toggleTheme() {
352
358
  const dark = document.documentElement.classList.toggle('dark')
353
359
  isDark.value = dark
@@ -379,12 +385,15 @@ onMounted(() => {
379
385
 
380
386
  <!-- Sidebar -->
381
387
  <aside
382
- :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
383
- class="fixed inset-y-0 left-0 w-60 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col z-40 transition-transform duration-200 lg:translate-x-0"
388
+ :class="[
389
+ sidebarOpen ? 'w-60 translate-x-0' : '-translate-x-full',
390
+ collapsed ? 'lg:w-16' : 'lg:w-60'
391
+ ]"
392
+ class="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-40 transition-all duration-200 lg:translate-x-0"
384
393
  >
385
394
  <!-- App name -->
386
395
  <div class="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
387
- <span class="text-sm font-bold text-gray-900 dark:text-white">{{ appName }}</span>
396
+ <span :class="collapsed ? 'lg:hidden' : ''" class="text-sm font-bold text-gray-900 dark:text-white">{{ appName }}</span>
388
397
  </div>
389
398
 
390
399
  <!-- Navigation -->
@@ -394,43 +403,36 @@ onMounted(() => {
394
403
  @click="sidebarOpen = false"
395
404
  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"
396
405
  >
397
- <!-- Dashboard icon -->
398
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
406
+ <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
399
407
  <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" />
400
408
  </svg>
401
- Dashboard
409
+ <span :class="collapsed ? 'lg:hidden' : ''">Dashboard</span>
402
410
  </a>
403
411
  </nav>
404
412
 
413
+ <!-- Collapse toggle -->
414
+ <div class="px-3 py-2 hidden lg:block">
415
+ <button @click="collapsed = !collapsed" class="flex items-center justify-center w-full px-2.5 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
416
+ <span class="text-xs font-mono">{{ collapsed ? '>>' : '<<' }}</span>
417
+ </button>
418
+ </div>
419
+
405
420
  <!-- Bottom links -->
406
- <div class="px-3 py-4 border-t border-gray-200 dark:border-gray-800 space-y-1">
407
- <a
408
- href="/heartbeat"
409
- @click="sidebarOpen = false"
410
- 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"
411
- >
412
- <!-- Heart/pulse icon -->
413
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
414
- <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" />
415
- </svg>
416
- Heartbeat
417
- </a>
421
+ <div class="px-3 py-4 mt-auto border-t border-gray-200 dark:border-gray-800 space-y-1">
418
422
  <a
419
- href="/api/ping"
420
- @click="sidebarOpen = false"
423
+ href="https://github.com/mantiqjs/mantiq"
424
+ target="_blank"
425
+ rel="noopener"
421
426
  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"
422
427
  >
423
- <!-- Signal/wifi icon -->
424
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
425
- <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" />
426
- </svg>
427
- API Ping
428
+ <svg class="w-4 h-4 shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" /></svg>
429
+ <span :class="collapsed ? 'lg:hidden' : ''">Documentation</span>
428
430
  </a>
429
431
  </div>
430
432
  </aside>
431
433
 
432
434
  <!-- Main area -->
433
- <div class="flex-1 lg:ml-60 flex flex-col min-h-screen">
435
+ <div :class="collapsed ? 'lg:ml-16' : 'lg:ml-60'" class="flex-1 flex flex-col min-h-screen transition-all duration-200">
434
436
  <!-- Header -->
435
437
  <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">
436
438
  <div class="flex items-center">
@@ -446,22 +448,35 @@ onMounted(() => {
446
448
  class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
447
449
  :title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
448
450
  >
449
- <!-- Sun icon (shown in dark mode) -->
450
451
  <svg v-if="isDark" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
451
452
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="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" />
452
453
  </svg>
453
- <!-- Moon icon (shown in light mode) -->
454
454
  <svg v-else class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
455
455
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="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" />
456
456
  </svg>
457
457
  </button>
458
- <span class="text-xs text-gray-500 dark:text-gray-400">{{ currentUser?.name }}</span>
459
- <button
460
- @click="handleLogout"
461
- 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"
462
- >
463
- Logout
464
- </button>
458
+ <!-- Account dropdown -->
459
+ <div class="relative">
460
+ <button @click="accountOpen = !accountOpen" class="flex items-center gap-2 rounded-lg px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
461
+ <div class="w-7 h-7 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center text-xs font-medium text-emerald-700 dark:text-emerald-300">
462
+ {{ userInitials }}
463
+ </div>
464
+ <span class="text-sm text-gray-700 dark:text-gray-300 hidden sm:block">{{ currentUser?.name }}</span>
465
+ <svg class="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>
466
+ </button>
467
+ <template v-if="accountOpen">
468
+ <div class="fixed inset-0 z-40" @click="accountOpen = false" />
469
+ <div class="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-lg py-1 z-50">
470
+ <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-800">
471
+ <div class="text-sm font-medium text-gray-900 dark:text-white">{{ currentUser?.name }}</div>
472
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ currentUser?.email }}</div>
473
+ </div>
474
+ <button @click="handleLogout" class="w-full text-left px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white transition-colors">
475
+ Sign out
476
+ </button>
477
+ </div>
478
+ </template>
479
+ </div>
465
480
  </div>
466
481
  </header>
467
482
 
package/src/ui/shadcn.ts CHANGED
@@ -6,6 +6,31 @@ export function getShadcnTemplates(ctx: TemplateContext): {
6
6
  } {
7
7
  return {
8
8
  files: {
9
+ // ── vite config override with @/ resolve alias ────────────────────────
10
+ 'vite.config.ts': `import { defineConfig } from 'vite'
11
+ import react from '@vitejs/plugin-react'
12
+ import tailwindcss from '@tailwindcss/vite'
13
+ import path from 'path'
14
+
15
+ export default defineConfig({
16
+ plugins: [react(), tailwindcss()],
17
+ publicDir: false,
18
+ resolve: {
19
+ alias: {
20
+ '@': path.resolve(__dirname, './src'),
21
+ },
22
+ },
23
+ build: {
24
+ outDir: 'public/build',
25
+ manifest: true,
26
+ emptyOutDir: true,
27
+ rollupOptions: {
28
+ input: ['src/main.tsx', 'src/style.css'],
29
+ },
30
+ },
31
+ })
32
+ `,
33
+
9
34
  // ── tsconfig override with @/ path alias ──────────────────────────────
10
35
  'tsconfig.json': JSON.stringify({
11
36
  compilerOptions: {