create-mantiq 0.5.0 → 0.6.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.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Scaffold a new MantiqJS application",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/templates.ts CHANGED
@@ -21,12 +21,12 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
21
21
  },
22
22
  dependencies: {
23
23
  '@mantiq/auth': '^0.1.2',
24
- '@mantiq/cli': '^0.1.2',
25
- '@mantiq/core': '^0.1.2',
26
- '@mantiq/database': '^0.1.2',
24
+ '@mantiq/cli': '^0.1.6',
25
+ '@mantiq/core': '^0.1.4',
26
+ '@mantiq/database': '^0.1.4',
27
27
  '@mantiq/events': '^0.1.2',
28
28
  '@mantiq/filesystem': '^0.1.2',
29
- '@mantiq/heartbeat': '^0.1.2',
29
+ '@mantiq/heartbeat': '^0.3.0',
30
30
  '@mantiq/helpers': '^0.1.2',
31
31
  '@mantiq/logging': '^0.1.2',
32
32
  '@mantiq/queue': '^0.1.2',
@@ -34,6 +34,8 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
34
34
  '@mantiq/validation': '^0.1.2',
35
35
  '@mantiq/mail': '^0.2.0',
36
36
  '@mantiq/notify': '^0.1.0',
37
+ '@mantiq/search': '^0.1.0',
38
+ '@mantiq/health': '^0.1.0',
37
39
  },
38
40
  devDependencies: {
39
41
  'bun-types': 'latest',
@@ -117,6 +119,7 @@ import { HeartbeatServiceProvider, HeartbeatMiddleware } from '@mantiq/heartbeat
117
119
  import { RealtimeServiceProvider } from '@mantiq/realtime'
118
120
  import { MailServiceProvider } from '@mantiq/mail'
119
121
  import { NotificationServiceProvider } from '@mantiq/notify'
122
+ import { SearchServiceProvider } from '@mantiq/search'
120
123
  import { DatabaseServiceProvider } from './app/Providers/DatabaseServiceProvider.ts'
121
124
 
122
125
  // ── Load .env ─────────────────────────────────────────────────────────────────
@@ -150,6 +153,7 @@ await app.registerProviders([
150
153
  RealtimeServiceProvider,
151
154
  MailServiceProvider,
152
155
  NotificationServiceProvider,
156
+ SearchServiceProvider,
153
157
  ])
154
158
  await app.bootProviders()
155
159
 
@@ -456,6 +460,33 @@ export default {
456
460
  'config/notify.ts': `export default {
457
461
  channels: {},
458
462
  }
463
+ `,
464
+
465
+ 'config/search.ts': `export default {
466
+ default: 'collection',
467
+ prefix: '',
468
+ queue: false,
469
+ softDelete: false,
470
+
471
+ engines: {
472
+ collection: {
473
+ driver: 'collection' as const,
474
+ },
475
+ database: {
476
+ driver: 'database' as const,
477
+ },
478
+ // algolia: {
479
+ // driver: 'algolia' as const,
480
+ // applicationId: env('ALGOLIA_APP_ID', ''),
481
+ // apiKey: env('ALGOLIA_SECRET', ''),
482
+ // },
483
+ // meilisearch: {
484
+ // driver: 'meilisearch' as const,
485
+ // host: env('MEILISEARCH_HOST', 'http://127.0.0.1:7700'),
486
+ // apiKey: env('MEILISEARCH_KEY', ''),
487
+ // },
488
+ },
489
+ }
459
490
  `,
460
491
 
461
492
  'config/heartbeat.ts': `export default {
@@ -736,12 +767,12 @@ function applyKitOverrides(templates: Record<string, string>, ctx: TemplateConte
736
767
  },
737
768
  dependencies: {
738
769
  '@mantiq/auth': '^0.1.2',
739
- '@mantiq/cli': '^0.1.2',
740
- '@mantiq/core': '^0.1.2',
741
- '@mantiq/database': '^0.1.2',
770
+ '@mantiq/cli': '^0.1.6',
771
+ '@mantiq/core': '^0.1.4',
772
+ '@mantiq/database': '^0.1.4',
742
773
  '@mantiq/events': '^0.1.2',
743
774
  '@mantiq/filesystem': '^0.1.2',
744
- '@mantiq/heartbeat': '^0.1.2',
775
+ '@mantiq/heartbeat': '^0.3.0',
745
776
  '@mantiq/helpers': '^0.1.2',
746
777
  '@mantiq/logging': '^0.1.2',
747
778
  '@mantiq/queue': '^0.1.2',
@@ -749,7 +780,9 @@ function applyKitOverrides(templates: Record<string, string>, ctx: TemplateConte
749
780
  '@mantiq/validation': '^0.1.2',
750
781
  '@mantiq/mail': '^0.2.0',
751
782
  '@mantiq/notify': '^0.1.0',
752
- '@mantiq/vite': '^0.1.2',
783
+ '@mantiq/search': '^0.1.0',
784
+ '@mantiq/health': '^0.1.0',
785
+ '@mantiq/vite': '^0.1.3',
753
786
  ...uiDeps,
754
787
  },
755
788
  devDependencies: {
@@ -826,6 +859,7 @@ import { HeartbeatServiceProvider, HeartbeatMiddleware } from '@mantiq/heartbeat
826
859
  import { RealtimeServiceProvider } from '@mantiq/realtime'
827
860
  import { MailServiceProvider } from '@mantiq/mail'
828
861
  import { NotificationServiceProvider } from '@mantiq/notify'
862
+ import { SearchServiceProvider } from '@mantiq/search'
829
863
  import { DatabaseServiceProvider } from './app/Providers/DatabaseServiceProvider.ts'
830
864
 
831
865
  // ── Load .env ─────────────────────────────────────────────────────────────────
@@ -860,6 +894,7 @@ await app.registerProviders([
860
894
  RealtimeServiceProvider,
861
895
  MailServiceProvider,
862
896
  NotificationServiceProvider,
897
+ SearchServiceProvider,
863
898
  ])
864
899
  await app.bootProviders()
865
900
 
@@ -87,7 +87,7 @@ export function NavUser({ user, navigate, onLogout }: NavUserProps) {
87
87
  </div>
88
88
  </DropdownMenuLabel>
89
89
  <DropdownMenuSeparator />
90
- <DropdownMenuItem onClick={() => navigate('/account')}>
90
+ <DropdownMenuItem onClick={() => navigate('/account/profile')}>
91
91
  <User className="mr-2 h-4 w-4" />
92
92
  Account
93
93
  </DropdownMenuItem>
@@ -44,7 +44,7 @@ export const sidebarData: NavGroup[] = [
44
44
  items: [
45
45
  {
46
46
  title: 'Settings',
47
- url: '/account',
47
+ url: '/account/profile',
48
48
  icon: Settings,
49
49
  items: [
50
50
  { title: 'Profile', url: '/account/profile', icon: User },
@@ -44,7 +44,7 @@ export const sidebarData: NavGroup[] = [
44
44
  items: [
45
45
  {
46
46
  title: 'Settings',
47
- url: '/account',
47
+ url: '/account/profile',
48
48
  icon: Settings,
49
49
  items: [
50
50
  { title: 'Profile', url: '/account/profile', icon: User },
@@ -44,7 +44,7 @@ export const sidebarData: NavGroup[] = [
44
44
  items: [
45
45
  {
46
46
  title: 'Settings',
47
- url: '/account',
47
+ url: '/account/profile',
48
48
  icon: Settings,
49
49
  items: [
50
50
  { title: 'Profile', url: '/account/profile', icon: User },
package/src/kits/react.ts DELETED
@@ -1,437 +0,0 @@
1
- import type { TemplateContext } from '../templates.ts'
2
-
3
- export function getReactTemplates(ctx: TemplateContext): Record<string, string> {
4
- return {
5
- 'vite.config.ts': `import { defineConfig } from 'vite'
6
- import react from '@vitejs/plugin-react'
7
- import tailwindcss from '@tailwindcss/vite'
8
-
9
- export default defineConfig({
10
- plugins: [react(), tailwindcss()],
11
- publicDir: false,
12
- build: {
13
- outDir: 'public/build',
14
- manifest: true,
15
- emptyOutDir: true,
16
- rollupOptions: {
17
- input: ['src/main.tsx', 'src/style.css'],
18
- },
19
- },
20
- })
21
- `,
22
-
23
- 'src/style.css': `@import "tailwindcss";
24
- @custom-variant dark (&:where(.dark, .dark *));
25
-
26
- @keyframes fadeUp {
27
- from { opacity: 0; transform: translateY(8px); }
28
- to { opacity: 1; transform: translateY(0); }
29
- }
30
- .animate-fade-up { animation: fadeUp 0.4s ease-out; }
31
- `,
32
-
33
- 'src/pages.ts': `import Login from './pages/Login.tsx'
34
- import Register from './pages/Register.tsx'
35
- import Dashboard from './pages/Dashboard.tsx'
36
-
37
- export const pages: Record<string, React.ComponentType<any>> = {
38
- Login,
39
- Register,
40
- Dashboard,
41
- }
42
- `,
43
-
44
- 'src/lib/api.ts': `export async function api<T = any>(url: string, opts: RequestInit = {}): Promise<{ ok: boolean; status: number; data: T }> {
45
- const res = await fetch(url, { ...opts, headers: { Accept: 'application/json', ...opts.headers } })
46
- const data = res.headers.get('content-type')?.includes('json') ? await res.json() : null
47
- return { ok: res.ok, status: res.status, data }
48
- }
49
-
50
- export function post(url: string, body: object) {
51
- return api(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
52
- }
53
- `,
54
-
55
- 'src/main.tsx': `import './style.css'
56
- import { hydrateRoot, createRoot } from 'react-dom/client'
57
- import { MantiqApp } from './App.tsx'
58
- import { pages } from './pages.ts'
59
-
60
- const root = document.getElementById('app')!
61
- const app = <MantiqApp pages={pages} />
62
-
63
- // Hydrate if SSR content exists, otherwise CSR mount
64
- root.innerHTML.trim() ? hydrateRoot(root, app) : createRoot(root).render(app)
65
- `,
66
-
67
- 'src/ssr.tsx': `import { renderToString } from 'react-dom/server'
68
- import { MantiqApp } from './App.tsx'
69
- import { pages } from './pages.ts'
70
-
71
- export function render(_url: string, data?: Record<string, any>) {
72
- return { html: renderToString(<MantiqApp pages={pages} initialData={data} />) }
73
- }
74
- `,
75
-
76
- 'src/App.tsx': `import { useState, useCallback, useEffect } from 'react'
77
-
78
- interface MantiqAppProps {
79
- pages: Record<string, React.ComponentType<any>>
80
- initialData?: Record<string, any>
81
- }
82
-
83
- function initTheme() {
84
- if (typeof window === 'undefined') return
85
- const theme = localStorage.getItem('theme') ||
86
- (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
87
- document.documentElement.classList.toggle('dark', theme === 'dark')
88
- }
89
-
90
- initTheme()
91
-
92
- export function MantiqApp({ pages, initialData }: MantiqAppProps) {
93
- const windowData = typeof window !== 'undefined' ? (window as any).__MANTIQ_DATA__ : {}
94
- const initial = initialData ?? windowData
95
- const [page, setPage] = useState<string>(initial._page ?? 'Login')
96
- const [data, setData] = useState<Record<string, any>>(initial)
97
-
98
- const navigate = useCallback(async (href: string) => {
99
- const res = await fetch(href, {
100
- headers: { 'X-Mantiq': 'true', Accept: 'application/json' },
101
- })
102
- const newData = await res.json()
103
- setPage(newData._page)
104
- setData(newData)
105
- history.pushState(null, '', newData._url)
106
- }, [])
107
-
108
- useEffect(() => {
109
- const handleClick = (e: MouseEvent) => {
110
- const anchor = (e.target as HTMLElement).closest('a')
111
- const href = anchor?.getAttribute('href')
112
- if (!href?.startsWith('/') || anchor?.target || e.ctrlKey || e.metaKey) return
113
- // Only intercept known SPA routes — let other links navigate normally
114
- const spaRoutes = ['/login', '/register', '/dashboard']
115
- if (!spaRoutes.some(r => href === r || href.startsWith(r + '?'))) return
116
- e.preventDefault()
117
- navigate(href)
118
- }
119
- const handlePop = () => navigate(location.pathname)
120
- document.addEventListener('click', handleClick)
121
- window.addEventListener('popstate', handlePop)
122
- return () => {
123
- document.removeEventListener('click', handleClick)
124
- window.removeEventListener('popstate', handlePop)
125
- }
126
- }, [navigate])
127
-
128
- const Page = pages[page]
129
- return Page ? <Page {...data} navigate={navigate} /> : null
130
- }
131
- `,
132
-
133
- 'src/pages/Login.tsx': `import { useState } from 'react'
134
- import { post } from '../lib/api.ts'
135
-
136
- interface LoginProps {
137
- appName?: string
138
- navigate: (href: string) => void
139
- [key: string]: any
140
- }
141
-
142
- export default function Login({ appName = '${ctx.name}', navigate }: LoginProps) {
143
- const [email, setEmail] = useState('admin@example.com')
144
- const [password, setPassword] = useState('password')
145
- const [error, setError] = useState('')
146
- const [loading, setLoading] = useState(false)
147
-
148
- const handleSubmit = async (e: React.FormEvent) => {
149
- e.preventDefault(); setError(''); setLoading(true)
150
- const { ok, data } = await post('/login', { email, password })
151
- if (ok) navigate('/dashboard')
152
- else setError(data?.error ?? 'Login failed')
153
- setLoading(false)
154
- }
155
-
156
- return (
157
- <div className="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-4">
158
- <div className="animate-fade-up w-full max-w-sm">
159
- <div className="text-center mb-8">
160
- <h2 className="text-lg font-semibold text-gray-900 dark:text-white">{appName}</h2>
161
- </div>
162
- <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">
163
- <div>
164
- <h1 className="text-xl font-bold text-gray-900 dark:text-white">Welcome back</h1>
165
- <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Sign in to your account</p>
166
- </div>
167
- {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>}
168
- <form onSubmit={handleSubmit} className="space-y-4">
169
- <div className="space-y-1.5">
170
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
171
- <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
172
- 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
- </div>
174
- <div className="space-y-1.5">
175
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
176
- <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
177
- 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" />
178
- </div>
179
- <button type="submit" disabled={loading}
180
- 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">
181
- {loading ? 'Signing in...' : 'Sign in'}
182
- </button>
183
- </form>
184
- <p className="text-sm text-gray-500 dark:text-gray-400 text-center">
185
- 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>
186
- </p>
187
- </div>
188
- </div>
189
- </div>
190
- )
191
- }
192
- `,
193
-
194
- 'src/pages/Register.tsx': `import { useState } from 'react'
195
- import { post } from '../lib/api.ts'
196
-
197
- interface RegisterProps {
198
- appName?: string
199
- navigate: (href: string) => void
200
- [key: string]: any
201
- }
202
-
203
- export default function Register({ appName = '${ctx.name}', navigate }: RegisterProps) {
204
- const [name, setName] = useState('')
205
- const [email, setEmail] = useState('')
206
- const [password, setPassword] = useState('')
207
- const [error, setError] = useState('')
208
- const [loading, setLoading] = useState(false)
209
-
210
- const handleSubmit = async (e: React.FormEvent) => {
211
- e.preventDefault(); setError(''); setLoading(true)
212
- const { ok, data } = await post('/register', { name, email, password })
213
- if (ok) navigate('/dashboard')
214
- else setError(data?.error?.message ?? data?.error ?? 'Registration failed')
215
- setLoading(false)
216
- }
217
-
218
- return (
219
- <div className="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-4">
220
- <div className="animate-fade-up w-full max-w-sm">
221
- <div className="text-center mb-8">
222
- <h2 className="text-lg font-semibold text-gray-900 dark:text-white">{appName}</h2>
223
- </div>
224
- <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">
225
- <div>
226
- <h1 className="text-xl font-bold text-gray-900 dark:text-white">Create an account</h1>
227
- <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Get started with {appName}</p>
228
- </div>
229
- {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>}
230
- <form onSubmit={handleSubmit} className="space-y-4">
231
- <div className="space-y-1.5">
232
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
233
- <input value={name} onChange={(e) => setName(e.target.value)} required
234
- 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" />
235
- </div>
236
- <div className="space-y-1.5">
237
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
238
- <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
239
- 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" />
240
- </div>
241
- <div className="space-y-1.5">
242
- <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
243
- <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
244
- 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" />
245
- </div>
246
- <button type="submit" disabled={loading}
247
- 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">
248
- {loading ? 'Creating account...' : 'Create account'}
249
- </button>
250
- </form>
251
- <p className="text-sm text-gray-500 dark:text-gray-400 text-center">
252
- 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>
253
- </p>
254
- </div>
255
- </div>
256
- </div>
257
- )
258
- }
259
- `,
260
-
261
- 'src/pages/Dashboard.tsx': `import { useState, useEffect, useCallback } from 'react'
262
- import { api, post } from '../lib/api.ts'
263
-
264
- interface User { id: number; name: string; email: string; role: string }
265
-
266
- interface DashboardProps {
267
- appName?: string
268
- currentUser?: User | null
269
- users?: User[]
270
- navigate: (href: string) => void
271
- [key: string]: any
272
- }
273
-
274
- export default function Dashboard({ appName = '${ctx.name}', currentUser, users: initialUsers, navigate }: DashboardProps) {
275
- const [users, setUsers] = useState<User[]>(initialUsers ?? [])
276
- const [loading, setLoading] = useState(!initialUsers?.length)
277
- const [sidebarOpen, setSidebarOpen] = useState(false)
278
- const [collapsed, setCollapsed] = useState(false)
279
- const [accountOpen, setAccountOpen] = useState(false)
280
- const [isDark, setIsDark] = useState(() =>
281
- typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : true
282
- )
283
-
284
- const toggleTheme = () => {
285
- const dark = document.documentElement.classList.toggle('dark')
286
- localStorage.setItem('theme', dark ? 'dark' : 'light')
287
- setIsDark(dark)
288
- }
289
-
290
- const fetchUsers = useCallback(async () => {
291
- setLoading(true)
292
- const { ok, data } = await api('/api/users')
293
- if (ok) setUsers(data.data ?? [])
294
- setLoading(false)
295
- }, [])
296
-
297
- useEffect(() => {
298
- if (!initialUsers?.length) fetchUsers()
299
- }, [fetchUsers, initialUsers])
300
-
301
- const handleLogout = async () => {
302
- await post('/logout', {})
303
- navigate('/login')
304
- }
305
-
306
- return (
307
- <div className="min-h-screen flex bg-gray-50 dark:bg-gray-950">
308
- {/* Mobile overlay */}
309
- {sidebarOpen && <div className="fixed inset-0 bg-black/50 z-30 lg:hidden" onClick={() => setSidebarOpen(false)} />}
310
-
311
- {/* Sidebar */}
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\`}>
313
- <div className="h-14 flex items-center px-5 border-b border-gray-200 dark:border-gray-800">
314
- <span className={\`text-sm font-semibold text-gray-900 dark:text-white \${collapsed ? 'lg:hidden' : ''}\`}>{appName}</span>
315
- </div>
316
- <nav className="flex-1 px-3 py-3 space-y-0.5">
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">
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>
320
- </a>
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">
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>
324
- </a>
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 */}
335
- <div className="px-3 py-3 mt-auto border-t border-gray-200 dark:border-gray-800 space-y-0.5">
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>
339
- </a>
340
- </div>
341
- </aside>
342
-
343
- {/* Main */}
344
- <div className={\`flex-1 \${collapsed ? 'lg:ml-16' : 'lg:ml-60'} transition-all duration-200\`}>
345
- {/* Top bar */}
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">
347
- <div className="flex items-center">
348
- <button onClick={() => setSidebarOpen(!sidebarOpen)} 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 lg:hidden mr-2">
349
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /></svg>
350
- </button>
351
- <h1 className="text-sm font-medium text-gray-900 dark:text-gray-200">Dashboard</h1>
352
- </div>
353
- <div className="flex items-center gap-3">
354
- <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">
355
- {isDark ? (
356
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
357
- <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" />
358
- </svg>
359
- ) : (
360
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
361
- <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" />
362
- </svg>
363
- )}
364
- </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>
389
- </div>
390
- </header>
391
-
392
- {/* Content */}
393
- <main className="p-6 space-y-6 animate-fade-up">
394
- <div className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 p-6">
395
- <h2 className="text-lg font-bold text-gray-900 dark:text-white">Welcome back, {currentUser?.name}</h2>
396
- <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Here's what's happening with your application.</p>
397
- </div>
398
-
399
- <div id="users-section" className="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">
400
- <div className="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
401
- <h2 className="text-sm font-bold text-gray-900 dark:text-gray-200">Users</h2>
402
- <span className="text-xs text-gray-500 dark:text-gray-400">{loading ? 'Loading...' : \`\${users.length} total\`}</span>
403
- </div>
404
- <table className="w-full text-sm">
405
- <thead>
406
- <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">
407
- <th className="px-5 py-3 font-medium">Name</th>
408
- <th className="px-5 py-3 font-medium">Email</th>
409
- <th className="px-5 py-3 font-medium">Role</th>
410
- </tr>
411
- </thead>
412
- <tbody className="divide-y divide-gray-100 dark:divide-gray-800/60">
413
- {users.map((u) => (
414
- <tr key={u.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors">
415
- <td className="px-5 py-3 text-gray-900 dark:text-gray-200">{u.name}</td>
416
- <td className="px-5 py-3 text-gray-500 dark:text-gray-400">{u.email}</td>
417
- <td className="px-5 py-3">
418
- <span className={\`text-[10px] px-2 py-0.5 rounded-full font-medium \${
419
- 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'
420
- }\`}>{u.role}</span>
421
- </td>
422
- </tr>
423
- ))}
424
- {users.length === 0 && !loading && (
425
- <tr><td colSpan={3} className="px-5 py-8 text-center text-gray-400 dark:text-gray-600">No users found</td></tr>
426
- )}
427
- </tbody>
428
- </table>
429
- </div>
430
- </main>
431
- </div>
432
- </div>
433
- )
434
- }
435
- `,
436
- }
437
- }