@gravito/zenith 1.1.3 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +28 -10
  2. package/dist/bin.js +43235 -76691
  3. package/dist/client/index.html +13 -0
  4. package/dist/server/index.js +43235 -76691
  5. package/package.json +16 -7
  6. package/CHANGELOG.md +0 -62
  7. package/Dockerfile +0 -46
  8. package/Dockerfile.demo-worker +0 -29
  9. package/bin/flux-console.ts +0 -2
  10. package/doc/ECOSYSTEM_EXPANSION_RFC.md +0 -130
  11. package/docker-compose.yml +0 -40
  12. package/docs/ALERTING_GUIDE.md +0 -71
  13. package/docs/DEPLOYMENT.md +0 -157
  14. package/docs/DOCS_INTERNAL.md +0 -73
  15. package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
  16. package/docs/QUASAR_MASTER_PLAN.md +0 -140
  17. package/docs/QUICK_TEST_GUIDE.md +0 -72
  18. package/docs/ROADMAP.md +0 -85
  19. package/docs/integrations/LARAVEL.md +0 -207
  20. package/postcss.config.js +0 -6
  21. package/scripts/debug_redis_keys.ts +0 -24
  22. package/scripts/flood-logs.ts +0 -21
  23. package/scripts/seed.ts +0 -213
  24. package/scripts/verify-throttle.ts +0 -49
  25. package/scripts/worker.ts +0 -124
  26. package/specs/PULSE_SPEC.md +0 -86
  27. package/src/bin.ts +0 -6
  28. package/src/client/App.tsx +0 -72
  29. package/src/client/Layout.tsx +0 -669
  30. package/src/client/Sidebar.tsx +0 -112
  31. package/src/client/ThroughputChart.tsx +0 -158
  32. package/src/client/WorkerStatus.tsx +0 -202
  33. package/src/client/components/BrandIcons.tsx +0 -168
  34. package/src/client/components/ConfirmDialog.tsx +0 -134
  35. package/src/client/components/JobInspector.tsx +0 -487
  36. package/src/client/components/LogArchiveModal.tsx +0 -432
  37. package/src/client/components/NotificationBell.tsx +0 -212
  38. package/src/client/components/PageHeader.tsx +0 -47
  39. package/src/client/components/Toaster.tsx +0 -90
  40. package/src/client/components/UserProfileDropdown.tsx +0 -186
  41. package/src/client/contexts/AuthContext.tsx +0 -105
  42. package/src/client/contexts/NotificationContext.tsx +0 -128
  43. package/src/client/index.css +0 -172
  44. package/src/client/main.tsx +0 -15
  45. package/src/client/pages/LoginPage.tsx +0 -164
  46. package/src/client/pages/MetricsPage.tsx +0 -445
  47. package/src/client/pages/OverviewPage.tsx +0 -519
  48. package/src/client/pages/PulsePage.tsx +0 -409
  49. package/src/client/pages/QueuesPage.tsx +0 -378
  50. package/src/client/pages/SchedulesPage.tsx +0 -535
  51. package/src/client/pages/SettingsPage.tsx +0 -1001
  52. package/src/client/pages/WorkersPage.tsx +0 -380
  53. package/src/client/pages/index.ts +0 -8
  54. package/src/client/utils.ts +0 -15
  55. package/src/server/config/ServerConfigManager.ts +0 -90
  56. package/src/server/index.ts +0 -860
  57. package/src/server/middleware/auth.ts +0 -127
  58. package/src/server/services/AlertService.ts +0 -321
  59. package/src/server/services/CommandService.ts +0 -136
  60. package/src/server/services/LogStreamProcessor.ts +0 -93
  61. package/src/server/services/MaintenanceScheduler.ts +0 -78
  62. package/src/server/services/PulseService.ts +0 -148
  63. package/src/server/services/QueueMetricsCollector.ts +0 -138
  64. package/src/server/services/QueueService.ts +0 -924
  65. package/src/shared/types.ts +0 -223
  66. package/tailwind.config.js +0 -80
  67. package/tests/placeholder.test.ts +0 -7
  68. package/tsconfig.json +0 -29
  69. package/tsconfig.node.json +0 -10
  70. package/vite.config.ts +0 -27
@@ -1,669 +0,0 @@
1
- // ... imports ...
2
- import { useQuery, useQueryClient } from '@tanstack/react-query'
3
- import { AnimatePresence, motion } from 'framer-motion'
4
- import {
5
- Activity,
6
- BarChart3,
7
- Briefcase,
8
- Command,
9
- HardDrive,
10
- LayoutDashboard,
11
- ListTree,
12
- LogOut,
13
- Moon,
14
- RefreshCcw,
15
- Search,
16
- Settings,
17
- ShieldCheck,
18
- Sun,
19
- Trash2,
20
- Zap,
21
- } from 'lucide-react'
22
- import type * as React from 'react'
23
- import { useEffect, useMemo, useState } from 'react'
24
- import { useNavigate } from 'react-router-dom'
25
- import { NotificationBell } from './components/NotificationBell'
26
- import { Toaster } from './components/Toaster'
27
- import { UserProfileDropdown } from './components/UserProfileDropdown'
28
- import { useAuth } from './contexts/AuthContext'
29
- import { Sidebar } from './Sidebar'
30
- import { cn } from './utils'
31
-
32
- interface LayoutProps {
33
- children: React.ReactNode
34
- }
35
-
36
- interface CommandItem {
37
- id: string
38
- title: string
39
- description: string
40
- icon: React.ReactNode
41
- action: () => void
42
- category: 'Navigation' | 'System' | 'Action'
43
- }
44
-
45
- /**
46
- * The main layout wrapper for the Zenith dashboard.
47
- *
48
- * It manages global state for the dashboard, including theme switching,
49
- * the command palette (Ctrl+K), real-time event streaming (SSE),
50
- * and the overall responsive structure.
51
- *
52
- * @public
53
- * @since 3.0.0
54
- */
55
- export function Layout({ children }: LayoutProps) {
56
- const navigate = useNavigate()
57
- const queryClient = useQueryClient()
58
- const { isAuthEnabled, logout } = useAuth()
59
- const [theme, setTheme] = useState<'light' | 'dark'>(() => {
60
- if (typeof window !== 'undefined') {
61
- return (localStorage.getItem('theme') as 'light' | 'dark') || 'light'
62
- }
63
- return 'light'
64
- })
65
- const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false)
66
- const [searchQuery, setSearchQuery] = useState('')
67
- const [selectedIndex, setSelectedIndex] = useState(0)
68
- const [health, setHealth] = useState(99.9)
69
- const [systemStatus, setSystemStatus] = useState<Record<string, any>>({})
70
- const [isSidebarOpen, setIsSidebarOpen] = useState(true)
71
-
72
- // Initial System Status Fetch
73
- useEffect(() => {
74
- fetch('/api/system/status')
75
- .then((res) => res.json())
76
- .then(setSystemStatus)
77
- .catch(() => {})
78
- }, [])
79
-
80
- // Global SSE Stream Manager
81
- useEffect(() => {
82
- console.log('[Zenith] Establishing Global Event Stream...')
83
- const ev = new EventSource('/api/logs/stream')
84
-
85
- ev.addEventListener('log', (e) => {
86
- try {
87
- const data = JSON.parse(e.data)
88
- window.dispatchEvent(new CustomEvent('flux-log-update', { detail: data }))
89
- } catch (err) {
90
- console.error('SSE Log Error', err)
91
- }
92
- })
93
-
94
- ev.addEventListener('stats', (e) => {
95
- try {
96
- const data = JSON.parse(e.data)
97
- window.dispatchEvent(new CustomEvent('flux-stats-update', { detail: data }))
98
- } catch (err) {
99
- console.error('SSE Stats Error', err)
100
- }
101
- })
102
-
103
- ev.addEventListener('pulse', (e) => {
104
- try {
105
- const data = JSON.parse(e.data)
106
- window.dispatchEvent(new CustomEvent('flux-pulse-update', { detail: data }))
107
- } catch (err) {
108
- console.error('SSE Pulse Error', err)
109
- }
110
- })
111
-
112
- ev.onerror = (err) => {
113
- console.error('[Zenith] SSE Connection Error', err)
114
- ev.close()
115
- }
116
-
117
- return () => {
118
- console.log('[Zenith] Closing Global Event Stream')
119
- ev.close()
120
- }
121
- }, [])
122
-
123
- // Fetch Queues for search (once)
124
- const [queueData, setQueueData] = useState<{ queues: any[] }>({ queues: [] })
125
- useEffect(() => {
126
- fetch('/api/queues')
127
- .then((res) => res.json())
128
- .then(setQueueData)
129
- .catch(() => {})
130
-
131
- // Optional: Listen to global stats if available (from OverviewPage) to keep queue stats fresh in command palette
132
- const handler = (e: Event) => {
133
- const customEvent = e as CustomEvent
134
- if (customEvent.detail?.queues) {
135
- setQueueData({ queues: customEvent.detail.queues })
136
- }
137
- }
138
- window.addEventListener('flux-stats-update', handler)
139
- return () => window.removeEventListener('flux-stats-update', handler)
140
- }, [])
141
-
142
- // Debounced job search
143
- const [debouncedQuery, setDebouncedQuery] = useState('')
144
-
145
- useEffect(() => {
146
- const timer = setTimeout(() => {
147
- if (searchQuery.length >= 2) {
148
- setDebouncedQuery(searchQuery)
149
- } else {
150
- setDebouncedQuery('')
151
- }
152
- }, 300)
153
- return () => clearTimeout(timer)
154
- }, [searchQuery])
155
-
156
- // Search jobs (Real-time and Archive)
157
- const { data: searchResults } = useQuery<{ results: any[]; archiveResults?: any[] }>({
158
- queryKey: ['job-search', debouncedQuery],
159
- queryFn: async () => {
160
- const [realtime, archive] = await Promise.all([
161
- fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}&limit=10`).then((res) =>
162
- res.json()
163
- ),
164
- fetch(`/api/archive/search?q=${encodeURIComponent(debouncedQuery)}&limit=10`).then((res) =>
165
- res.json()
166
- ),
167
- ])
168
- return {
169
- results: realtime.results || [],
170
- archiveResults: archive.results || [],
171
- }
172
- },
173
- enabled: debouncedQuery.length >= 2,
174
- staleTime: 5000,
175
- })
176
-
177
- useEffect(() => {
178
- const root = window.document.documentElement
179
- if (theme === 'dark') {
180
- root.classList.add('dark')
181
- } else {
182
- root.classList.remove('dark')
183
- }
184
- localStorage.setItem('theme', theme)
185
- }, [theme])
186
-
187
- useEffect(() => {
188
- const interval = setInterval(() => {
189
- setHealth((prev) => {
190
- const jitter = (Math.random() - 0.5) * 0.1
191
- return Math.min(100, Math.max(98.5, prev + jitter))
192
- })
193
- }, 3000)
194
- return () => clearInterval(interval)
195
- }, [])
196
-
197
- const toggleTheme = () => setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
198
-
199
- const retryAllFailed = async () => {
200
- const queues = queueData?.queues || []
201
- for (const q of queues) {
202
- if (q.failed > 0) {
203
- await fetch(`/api/queues/${q.name}/retry-all-failed`, { method: 'POST' })
204
- }
205
- }
206
- queryClient.invalidateQueries({ queryKey: ['queues'] })
207
- }
208
-
209
- const baseCommands: CommandItem[] = [
210
- {
211
- id: 'nav-overview',
212
- title: 'Go to Overview',
213
- description: 'Navigate to system dashboard',
214
- icon: <LayoutDashboard size={18} />,
215
- category: 'Navigation',
216
- action: () => navigate('/'),
217
- },
218
- {
219
- id: 'nav-queues',
220
- title: 'Go to Queues',
221
- description: 'Manage processing queues',
222
- icon: <ListTree size={18} />,
223
- category: 'Navigation',
224
- action: () => navigate('/queues'),
225
- },
226
- {
227
- id: 'nav-workers',
228
- title: 'Go to Workers',
229
- description: 'Monitor worker nodes',
230
- icon: <HardDrive size={18} />,
231
- category: 'Navigation',
232
- action: () => navigate('/workers'),
233
- },
234
- {
235
- id: 'nav-metrics',
236
- title: 'Go to Metrics',
237
- description: 'View system analytics',
238
- icon: <BarChart3 size={18} />,
239
- category: 'Navigation',
240
- action: () => navigate('/metrics'),
241
- },
242
- {
243
- id: 'nav-settings',
244
- title: 'Go to Settings',
245
- description: 'Configure console preferences',
246
- icon: <Settings size={18} />,
247
- category: 'Navigation',
248
- action: () => navigate('/settings'),
249
- },
250
- {
251
- id: 'act-retry-all',
252
- title: 'Retry All Failed Jobs',
253
- description: 'Recover all critical failures across all queues',
254
- icon: <RefreshCcw size={18} />,
255
- category: 'Action',
256
- action: retryAllFailed,
257
- },
258
- {
259
- id: 'sys-theme',
260
- title: `Switch to ${theme === 'dark' ? 'Light' : 'Dark'} Mode`,
261
- description: 'Toggle system visual appearance',
262
- icon: theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />,
263
- category: 'System',
264
- action: toggleTheme,
265
- },
266
- ...(isAuthEnabled
267
- ? [
268
- {
269
- id: 'sys-logout',
270
- title: 'Logout',
271
- description: 'Sign out from the console',
272
- icon: <LogOut size={18} />,
273
- category: 'System' as const,
274
- action: logout,
275
- },
276
- ]
277
- : []),
278
- ]
279
-
280
- const queueCommands: CommandItem[] = (queueData?.queues || []).map((q: any) => ({
281
- id: `queue-${q.name}`,
282
- title: `Queue: ${q.name}`,
283
- description: `${q.waiting} waiting, ${q.failed} failed`,
284
- icon: <ListTree size={18} />,
285
- category: 'Navigation',
286
- action: () => {
287
- navigate('/queues')
288
- setTimeout(() => {
289
- window.dispatchEvent(new CustomEvent('select-queue', { detail: q.name }))
290
- }, 100)
291
- },
292
- }))
293
-
294
- const actionCommands: CommandItem[] = [
295
- {
296
- id: 'act-clear-logs',
297
- title: 'Clear All Logs',
298
- description: 'Flush temporary log buffer in UI',
299
- icon: <Trash2 size={18} />,
300
- category: 'Action',
301
- action: () => {
302
- window.dispatchEvent(new CustomEvent('clear-logs'))
303
- },
304
- },
305
- ]
306
-
307
- // Dynamic job search results
308
- const jobCommands: CommandItem[] = useMemo(() => {
309
- const combined = [
310
- ...(searchResults?.results || []),
311
- ...(searchResults?.archiveResults || []).map((j: any) => ({ ...j, _archived: true })),
312
- ]
313
-
314
- if (!combined.length) {
315
- return []
316
- }
317
-
318
- return combined.slice(0, 15).map((job: any) => ({
319
- id: `job-${job._queue}-${job.id}-${job._archived ? 'arch' : 'live'}`,
320
- title: `Job: ${job.id || 'Unknown'}`,
321
- description: `${job._queue} • ${job.status || job._type}${job._archived ? ' • ARCHIVED' : ''} • ${job.name || 'No name'}`,
322
- icon: job._archived ? <HardDrive size={18} /> : <Briefcase size={18} />,
323
- category: 'Action' as const,
324
- action: () => {
325
- navigate('/queues')
326
- setTimeout(() => {
327
- window.dispatchEvent(new CustomEvent('select-queue', { detail: job._queue }))
328
- if (job._archived) {
329
- // Future-proof: trigger archive view opening
330
- window.dispatchEvent(
331
- new CustomEvent('inspect-job', { detail: { queue: job._queue, job } })
332
- )
333
- }
334
- }, 100)
335
- },
336
- }))
337
- }, [searchResults, navigate])
338
-
339
- const commands = [...baseCommands, ...actionCommands, ...queueCommands, ...jobCommands]
340
-
341
- const filteredCommands = commands.filter(
342
- (cmd) =>
343
- cmd.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
344
- cmd.description.toLowerCase().includes(searchQuery.toLowerCase())
345
- )
346
-
347
- useEffect(() => {
348
- const handleKeyDown = (e: KeyboardEvent) => {
349
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
350
- e.preventDefault()
351
- setIsCommandPaletteOpen((prev) => !prev)
352
- }
353
- if (e.key === 'Escape') {
354
- setIsCommandPaletteOpen(false)
355
- }
356
- }
357
- window.addEventListener('keydown', handleKeyDown)
358
- return () => window.removeEventListener('keydown', handleKeyDown)
359
- }, [])
360
-
361
- // Auto-scroll to selected item
362
- useEffect(() => {
363
- const el = document.getElementById(`command-item-${selectedIndex}`)
364
- if (el) {
365
- el.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
366
- }
367
- }, [selectedIndex])
368
-
369
- const handleSelect = (cmd: CommandItem) => {
370
- cmd.action()
371
- setIsCommandPaletteOpen(false)
372
- setSearchQuery('')
373
- }
374
-
375
- return (
376
- <div className="flex h-screen bg-background text-foreground overflow-hidden transition-colors duration-300">
377
- <motion.aside
378
- initial={false}
379
- animate={{ width: isSidebarOpen ? 260 : 80 }}
380
- className="border-r border-border/40 bg-card/50 backdrop-blur-xl flex flex-col z-50 transition-all duration-300 ease-[0.22, 1, 0.36, 1]"
381
- >
382
- <div className="h-16 flex items-center px-6 border-b border-white/5 bg-black/40">
383
- <div className="flex items-center gap-3 overflow-hidden">
384
- <div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center shrink-0 shadow-[0_0_20px_rgba(0,240,255,0.3)]">
385
- <Zap className="text-black fill-current" size={20} />
386
- </div>
387
- <motion.div
388
- animate={{ opacity: isSidebarOpen ? 1 : 0 }}
389
- className="flex flex-col min-w-[140px]"
390
- >
391
- <span className="font-black text-xl tracking-tighter leading-none text-white font-heading italic uppercase">
392
- Zenith
393
- </span>
394
- <span className="text-[8px] font-black text-primary uppercase tracking-[0.3em] mt-0.5">
395
- Quantum Control
396
- </span>
397
- </motion.div>
398
- </div>
399
- </div>
400
- <Sidebar
401
- collapsed={!isSidebarOpen}
402
- toggleCollapse={() => setIsSidebarOpen(!isSidebarOpen)}
403
- />
404
- </motion.aside>
405
-
406
- <main className="flex-1 flex flex-col relative overflow-hidden scanline">
407
- {/* Top Header */}
408
- <header className="h-16 border-b bg-card/50 backdrop-blur-md flex items-center justify-between px-8 sticky top-0 z-10 transition-colors">
409
- <div className="flex items-center gap-6 flex-1">
410
- <button
411
- type="button"
412
- className="relative max-w-md w-full group cursor-pointer outline-none block text-left"
413
- onClick={() => setIsCommandPaletteOpen(true)}
414
- >
415
- <Search
416
- className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground group-hover:text-primary transition-colors"
417
- size={18}
418
- />
419
- <div className="w-full bg-muted/40 border border-border/50 rounded-xl py-2 pl-10 pr-4 text-sm text-muted-foreground/60 font-medium flex justify-between items-center transition-all hover:bg-muted/60 hover:border-primary/20">
420
- <span>Search or command...</span>
421
- <div className="flex gap-1 group-hover:scale-105 transition-transform">
422
- <kbd className="bg-muted px-1.5 py-0.5 rounded border text-[10px] font-black opacity-60">
423
-
424
- </kbd>
425
- <kbd className="bg-muted px-1.5 py-0.5 rounded border text-[10px] font-black opacity-60">
426
- K
427
- </kbd>
428
- </div>
429
- </div>
430
- </button>
431
-
432
- {/* System Integrity Indicator */}
433
- <div className="hidden lg:flex items-center gap-3 px-4 py-2 rounded-xl bg-primary/5 border border-primary/20 transition-all hover:bg-primary/10 cursor-default group shadow-[inset_0_0_20px_rgba(0,240,255,0.02)]">
434
- <div className="relative flex items-center justify-center">
435
- <ShieldCheck size={16} className="text-primary z-10" />
436
- <div className="absolute w-4 h-4 bg-primary/20 rounded-full glow-pulse"></div>
437
- </div>
438
- <div className="flex flex-col">
439
- <span className="text-[8px] font-black uppercase tracking-[0.2em] text-primary/60 leading-none">
440
- Quantum Core
441
- </span>
442
- <span className="text-[11px] font-black tracking-tight leading-none text-white mt-0.5 font-mono">
443
- {health.toFixed(2)}% NOMINAL
444
- </span>
445
- </div>
446
- </div>
447
- </div>
448
-
449
- <div className="flex items-center gap-6">
450
- <button
451
- type="button"
452
- onClick={toggleTheme}
453
- className="p-2.5 hover:bg-muted rounded-xl text-muted-foreground hover:text-primary transition-all duration-300 active:scale-95 group relative"
454
- title={theme === 'light' ? 'Switch to Dark Mode' : 'Switch to Light Mode'}
455
- aria-label={theme === 'light' ? 'Switch to Dark Mode' : 'Switch to Light Mode'}
456
- >
457
- {theme === 'light' ? (
458
- <Moon size={20} className="group-hover:rotate-[15deg] transition-transform" />
459
- ) : (
460
- <Sun
461
- size={20}
462
- className="group-hover:rotate-90 transition-transform text-yellow-500"
463
- />
464
- )}
465
- </button>
466
-
467
- <NotificationBell />
468
-
469
- <div className="h-8 w-[1px] bg-border/50"></div>
470
-
471
- <UserProfileDropdown />
472
- </div>
473
- </header>
474
-
475
- {/* Content Area */}
476
- <div className="flex-1 overflow-y-auto p-8 scrollbar-thin">
477
- <motion.div
478
- initial={{ opacity: 0, scale: 0.98, filter: 'blur(10px)' }}
479
- animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
480
- transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
481
- >
482
- {children}
483
- </motion.div>
484
- </div>
485
-
486
- {/* Dynamic Status Bar (Ambient) */}
487
- <footer className="h-8 border-t border-white/5 bg-black/60 backdrop-blur-xl flex items-center justify-between px-6 z-10 transition-colors">
488
- <div className="flex items-center gap-6 overflow-hidden">
489
- <div className="flex items-center gap-2 border-r border-white/10 pr-4">
490
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_#10B981]"></span>
491
- <span className="text-[9px] font-black text-white/40 uppercase tracking-[0.2em] whitespace-nowrap font-mono">
492
- {systemStatus?.env || 'QUANTUM-1'}
493
- </span>
494
- </div>
495
- <div className="flex items-center gap-4 text-[9px] font-black text-white/20 uppercase tracking-[0.3em] animate-in fade-in slide-in-from-left duration-1000">
496
- <span className="flex items-center gap-1.5 text-primary/60">
497
- <Activity size={10} className="text-primary" /> 4MS
498
- </span>
499
- <span className="hidden sm:inline border-l border-white/5 pl-4 text-white/40">
500
- MEM: {systemStatus?.memory?.rss || '...'}
501
- </span>
502
- <span className="hidden md:inline border-l border-white/5 pl-4 text-white/40">
503
- ENGINE: {systemStatus?.engine || 'V2.4.1'}
504
- </span>
505
- </div>
506
- </div>
507
- <div className="flex items-center gap-4 pl-4 bg-gradient-to-l from-black via-black to-transparent text-right">
508
- <div className="flex items-center gap-3">
509
- <div className="w-12 h-1 bg-white/5 rounded-full overflow-hidden">
510
- <motion.div
511
- animate={{ x: [-48, 48] }}
512
- transition={{ duration: 3, repeat: Infinity, ease: 'linear' }}
513
- className="w-6 h-full bg-primary/40 shadow-[0_0_10px_#00F0FF]"
514
- />
515
- </div>
516
- <span className="text-[8px] font-black text-primary uppercase tracking-[0.3em]">
517
- SYNCING
518
- </span>
519
- </div>
520
- <span className="font-mono text-[9px] text-white/30 tabular-nums uppercase">
521
- {new Date().toISOString().split('T')[1]?.split('.')[0] || ''} Z
522
- </span>
523
- </div>
524
- </footer>
525
- </main>
526
-
527
- {/* Command Palette Modal */}
528
- <AnimatePresence>
529
- {isCommandPaletteOpen && (
530
- <div className="fixed inset-0 z-[100] flex items-start justify-center pt-24 px-4">
531
- <motion.div
532
- initial={{ opacity: 0 }}
533
- animate={{ opacity: 1 }}
534
- exit={{ opacity: 0 }}
535
- className="absolute inset-0 bg-black/60 backdrop-blur-md"
536
- onClick={() => setIsCommandPaletteOpen(false)}
537
- />
538
- <motion.div
539
- initial={{ opacity: 0, scale: 0.95, y: -20 }}
540
- animate={{ opacity: 1, scale: 1, y: 0 }}
541
- exit={{ opacity: 0, scale: 0.95, y: -20 }}
542
- className="relative w-full max-w-2xl bg-card border-border/50 border rounded-3xl shadow-2xl overflow-hidden scanline"
543
- >
544
- <div className="p-6 border-b flex items-center gap-4 bg-muted/5">
545
- <Command className="text-primary animate-pulse" size={24} />
546
- <input
547
- type="text"
548
- placeholder="Execute command or navigate..."
549
- className="flex-1 bg-transparent border-none outline-none text-lg font-bold placeholder:text-muted-foreground/30"
550
- value={searchQuery}
551
- onChange={(e) => {
552
- setSearchQuery(e.target.value)
553
- setSelectedIndex(0)
554
- }}
555
- onKeyDown={(e) => {
556
- if (e.key === 'ArrowDown') {
557
- e.preventDefault()
558
- setSelectedIndex((prev) => (prev + 1) % filteredCommands.length)
559
- } else if (e.key === 'ArrowUp') {
560
- e.preventDefault()
561
- setSelectedIndex(
562
- (prev) => (prev - 1 + filteredCommands.length) % filteredCommands.length
563
- )
564
- } else if (e.key === 'Enter' && filteredCommands[selectedIndex]) {
565
- handleSelect(filteredCommands[selectedIndex]!)
566
- }
567
- }}
568
- />
569
- <div className="flex items-center gap-2 px-2 py-1 rounded-lg bg-muted border text-[9px] font-black text-muted-foreground/60 uppercase">
570
- ESC to close
571
- </div>
572
- </div>
573
- <div className="max-h-[400px] overflow-y-auto p-2 custom-scrollbar">
574
- {filteredCommands.length === 0 ? (
575
- <div className="py-12 text-center text-muted-foreground/40 space-y-2">
576
- <Activity size={32} className="mx-auto opacity-20" />
577
- <p className="text-xs font-black uppercase tracking-widest">
578
- No matching commands found
579
- </p>
580
- </div>
581
- ) : (
582
- <div className="space-y-1">
583
- {filteredCommands.map((cmd, i) => (
584
- <button
585
- type="button"
586
- id={`command-item-${i}`}
587
- key={cmd.id}
588
- className={cn(
589
- 'w-full flex items-center justify-between p-4 rounded-2xl transition-all cursor-pointer group/cmd outline-none',
590
- i === selectedIndex
591
- ? 'bg-primary shadow-lg shadow-primary/20 -translate-x-1'
592
- : 'hover:bg-muted'
593
- )}
594
- onClick={() => handleSelect(cmd)}
595
- onMouseEnter={() => setSelectedIndex(i)}
596
- >
597
- <div className="flex items-center gap-4">
598
- <div
599
- className={cn(
600
- 'w-10 h-10 rounded-xl flex items-center justify-center transition-colors',
601
- i === selectedIndex
602
- ? 'bg-white/20 text-white'
603
- : 'bg-muted text-primary'
604
- )}
605
- >
606
- {cmd.icon}
607
- </div>
608
- <div>
609
- <p
610
- className={cn(
611
- 'text-sm font-black tracking-tight',
612
- i === selectedIndex ? 'text-white' : 'text-foreground'
613
- )}
614
- >
615
- {cmd.title}
616
- </p>
617
- <p
618
- className={cn(
619
- 'text-[10px] font-bold uppercase tracking-widest opacity-60',
620
- i === selectedIndex ? 'text-white/80' : 'text-muted-foreground'
621
- )}
622
- >
623
- {cmd.description}
624
- </p>
625
- </div>
626
- </div>
627
- <div className="flex items-center gap-2">
628
- <span
629
- className={cn(
630
- 'text-[9px] font-black uppercase tracking-widest px-2 py-1 rounded-md',
631
- i === selectedIndex
632
- ? 'bg-white/20 text-white'
633
- : 'bg-muted text-muted-foreground'
634
- )}
635
- >
636
- {cmd.category}
637
- </span>
638
- {i === selectedIndex && (
639
- <kbd className="bg-white/20 px-1.5 py-0.5 rounded text-[10px] text-white">
640
-
641
- </kbd>
642
- )}
643
- </div>
644
- </button>
645
- ))}
646
- </div>
647
- )}
648
- </div>
649
- <div className="p-4 border-t bg-muted/5 flex justify-between items-center px-6">
650
- <div className="flex gap-4">
651
- <div className="flex items-center gap-1.5 text-[9px] font-black text-muted-foreground/40 uppercase">
652
- <kbd className="bg-muted px-1.5 py-0.5 rounded border">↑↓</kbd> to navigate
653
- </div>
654
- <div className="flex items-center gap-1.5 text-[9px] font-black text-muted-foreground/40 uppercase">
655
- <kbd className="bg-muted px-1.5 py-0.5 rounded border">↵</kbd> to select
656
- </div>
657
- </div>
658
- <span className="text-[9px] font-black text-primary/40 uppercase tracking-[0.2em]">
659
- Gravito Zenith v1.0
660
- </span>
661
- </div>
662
- </motion.div>
663
- </div>
664
- )}
665
- </AnimatePresence>
666
- <Toaster />
667
- </div>
668
- )
669
- }