@gravito/zenith 0.1.0-beta.1
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/ARCHITECTURE.md +88 -0
- package/BATCH_OPERATIONS_IMPLEMENTATION.md +159 -0
- package/DEMO.md +156 -0
- package/DEPLOYMENT.md +157 -0
- package/DOCS_INTERNAL.md +73 -0
- package/Dockerfile +46 -0
- package/Dockerfile.demo-worker +29 -0
- package/EVOLUTION_BLUEPRINT.md +112 -0
- package/JOBINSPECTOR_SCROLL_FIX.md +152 -0
- package/PULSE_IMPLEMENTATION_PLAN.md +111 -0
- package/QUICK_TEST_GUIDE.md +72 -0
- package/README.md +33 -0
- package/ROADMAP.md +85 -0
- package/TESTING_BATCH_OPERATIONS.md +252 -0
- package/bin/flux-console.ts +2 -0
- package/dist/bin.js +108196 -0
- package/dist/client/assets/index-DGYEwTDL.css +1 -0
- package/dist/client/assets/index-oyTdySX0.js +421 -0
- package/dist/client/index.html +13 -0
- package/dist/server/index.js +108191 -0
- package/docker-compose.yml +40 -0
- package/docs/integrations/LARAVEL.md +207 -0
- package/package.json +50 -0
- package/postcss.config.js +6 -0
- package/scripts/flood-logs.ts +21 -0
- package/scripts/seed.ts +213 -0
- package/scripts/verify-throttle.ts +45 -0
- package/scripts/worker.ts +123 -0
- package/src/bin.ts +6 -0
- package/src/client/App.tsx +70 -0
- package/src/client/Layout.tsx +644 -0
- package/src/client/Sidebar.tsx +102 -0
- package/src/client/ThroughputChart.tsx +135 -0
- package/src/client/WorkerStatus.tsx +170 -0
- package/src/client/components/ConfirmDialog.tsx +103 -0
- package/src/client/components/JobInspector.tsx +524 -0
- package/src/client/components/LogArchiveModal.tsx +383 -0
- package/src/client/components/NotificationBell.tsx +203 -0
- package/src/client/components/Toaster.tsx +80 -0
- package/src/client/components/UserProfileDropdown.tsx +177 -0
- package/src/client/contexts/AuthContext.tsx +93 -0
- package/src/client/contexts/NotificationContext.tsx +103 -0
- package/src/client/index.css +174 -0
- package/src/client/index.html +12 -0
- package/src/client/main.tsx +15 -0
- package/src/client/pages/LoginPage.tsx +153 -0
- package/src/client/pages/MetricsPage.tsx +408 -0
- package/src/client/pages/OverviewPage.tsx +511 -0
- package/src/client/pages/QueuesPage.tsx +372 -0
- package/src/client/pages/SchedulesPage.tsx +531 -0
- package/src/client/pages/SettingsPage.tsx +449 -0
- package/src/client/pages/WorkersPage.tsx +316 -0
- package/src/client/pages/index.ts +7 -0
- package/src/client/utils.ts +6 -0
- package/src/server/index.ts +556 -0
- package/src/server/middleware/auth.ts +127 -0
- package/src/server/services/AlertService.ts +160 -0
- package/src/server/services/QueueService.ts +828 -0
- package/tailwind.config.js +73 -0
- package/tests/placeholder.test.ts +7 -0
- package/tsconfig.json +38 -0
- package/tsconfig.node.json +12 -0
- package/vite.config.ts +27 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { AnimatePresence, motion } from 'framer-motion'
|
|
3
|
+
import { Activity, ChevronRight, Clock, LogOut, Server, Settings, Shield, User } from 'lucide-react'
|
|
4
|
+
import { useEffect, useRef, useState } from 'react'
|
|
5
|
+
import { useNavigate } from 'react-router-dom'
|
|
6
|
+
import { useAuth } from '../contexts/AuthContext'
|
|
7
|
+
|
|
8
|
+
export function UserProfileDropdown() {
|
|
9
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
10
|
+
const { isAuthEnabled, logout } = useAuth()
|
|
11
|
+
const navigate = useNavigate()
|
|
12
|
+
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
13
|
+
|
|
14
|
+
const { data: systemStatus } = useQuery<any>({
|
|
15
|
+
queryKey: ['system-status'],
|
|
16
|
+
queryFn: () => fetch('/api/system/status').then((res) => res.json()),
|
|
17
|
+
refetchInterval: 30000,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Close dropdown when clicking outside
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
23
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
24
|
+
setIsOpen(false)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isOpen) {
|
|
29
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
34
|
+
}
|
|
35
|
+
}, [isOpen])
|
|
36
|
+
|
|
37
|
+
const handleLogout = async () => {
|
|
38
|
+
await logout()
|
|
39
|
+
setIsOpen(false)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleNavigate = (path: string) => {
|
|
43
|
+
navigate(path)
|
|
44
|
+
setIsOpen(false)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const formatUptime = (seconds: number) => {
|
|
48
|
+
const hours = Math.floor(seconds / 3600)
|
|
49
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
50
|
+
if (hours > 0) {
|
|
51
|
+
return `${hours}h ${minutes}m`
|
|
52
|
+
}
|
|
53
|
+
return `${minutes}m`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="relative" ref={dropdownRef}>
|
|
58
|
+
{/* Trigger */}
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="flex items-center gap-3 cursor-pointer group outline-none"
|
|
62
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
63
|
+
>
|
|
64
|
+
<div className="text-right hidden sm:block">
|
|
65
|
+
<p className="text-sm font-black tracking-tight group-hover:text-primary transition-colors">
|
|
66
|
+
Admin User
|
|
67
|
+
</p>
|
|
68
|
+
<p className="text-[10px] font-black text-muted-foreground/60 uppercase tracking-widest">
|
|
69
|
+
Architect
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="w-10 h-10 rounded-2xl bg-gradient-to-br from-primary to-indigo-600 flex items-center justify-center text-primary-foreground font-black shadow-lg shadow-primary/20 group-hover:scale-105 transition-transform">
|
|
73
|
+
<User size={20} />
|
|
74
|
+
</div>
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
{/* Dropdown */}
|
|
78
|
+
<AnimatePresence>
|
|
79
|
+
{isOpen && (
|
|
80
|
+
<motion.div
|
|
81
|
+
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
82
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
83
|
+
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
84
|
+
transition={{ duration: 0.2 }}
|
|
85
|
+
className="absolute right-0 top-full mt-2 w-72 bg-card border rounded-2xl shadow-2xl overflow-hidden z-50"
|
|
86
|
+
>
|
|
87
|
+
{/* User Info Header */}
|
|
88
|
+
<div className="p-5 bg-gradient-to-br from-primary/10 to-indigo-500/10 border-b">
|
|
89
|
+
<div className="flex items-center gap-4">
|
|
90
|
+
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-primary to-indigo-600 flex items-center justify-center text-primary-foreground shadow-lg">
|
|
91
|
+
<User size={28} />
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<h3 className="font-bold text-lg">Admin User</h3>
|
|
95
|
+
<p className="text-xs text-muted-foreground">System Administrator</p>
|
|
96
|
+
<div className="flex items-center gap-1 mt-1">
|
|
97
|
+
<Shield size={10} className="text-green-500" />
|
|
98
|
+
<span className="text-[9px] font-bold text-green-500 uppercase tracking-widest">
|
|
99
|
+
Full Access
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* System Status */}
|
|
107
|
+
<div className="p-4 bg-muted/20 border-b">
|
|
108
|
+
<p className="text-[9px] font-black text-muted-foreground/50 uppercase tracking-widest mb-3">
|
|
109
|
+
System Status
|
|
110
|
+
</p>
|
|
111
|
+
<div className="grid grid-cols-2 gap-3">
|
|
112
|
+
<div className="flex items-center gap-2">
|
|
113
|
+
<Activity size={12} className="text-green-500" />
|
|
114
|
+
<span className="text-[10px] font-bold">Online</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="flex items-center gap-2">
|
|
117
|
+
<Clock size={12} className="text-muted-foreground" />
|
|
118
|
+
<span className="text-[10px] font-bold">
|
|
119
|
+
{systemStatus?.uptime ? formatUptime(systemStatus.uptime) : '...'}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="flex items-center gap-2">
|
|
123
|
+
<Server size={12} className="text-muted-foreground" />
|
|
124
|
+
<span className="text-[10px] font-bold">{systemStatus?.node || '...'}</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="flex items-center gap-2">
|
|
127
|
+
<span className="w-3 h-3 text-[10px] font-black">🔥</span>
|
|
128
|
+
<span className="text-[10px] font-bold">
|
|
129
|
+
{systemStatus?.memory?.rss || '...'}
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Menu Items */}
|
|
136
|
+
<div className="p-2">
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
onClick={() => handleNavigate('/settings')}
|
|
140
|
+
className="w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-muted/50 transition-colors group"
|
|
141
|
+
>
|
|
142
|
+
<div className="flex items-center gap-3">
|
|
143
|
+
<Settings
|
|
144
|
+
size={18}
|
|
145
|
+
className="text-muted-foreground group-hover:text-foreground transition-colors"
|
|
146
|
+
/>
|
|
147
|
+
<span className="text-sm font-semibold">Settings</span>
|
|
148
|
+
</div>
|
|
149
|
+
<ChevronRight size={16} className="text-muted-foreground/50" />
|
|
150
|
+
</button>
|
|
151
|
+
|
|
152
|
+
{isAuthEnabled && (
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={handleLogout}
|
|
156
|
+
className="w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-red-500/10 transition-colors group"
|
|
157
|
+
>
|
|
158
|
+
<div className="flex items-center gap-3">
|
|
159
|
+
<LogOut size={18} className="text-red-500" />
|
|
160
|
+
<span className="text-sm font-semibold text-red-500">Logout</span>
|
|
161
|
+
</div>
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Footer */}
|
|
167
|
+
<div className="px-4 py-3 bg-muted/10 border-t">
|
|
168
|
+
<p className="text-[9px] text-center text-muted-foreground/50">
|
|
169
|
+
Flux Console v0.1.0-alpha.1
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
</motion.div>
|
|
173
|
+
)}
|
|
174
|
+
</AnimatePresence>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface AuthContextType {
|
|
4
|
+
isAuthenticated: boolean
|
|
5
|
+
isAuthEnabled: boolean
|
|
6
|
+
isLoading: boolean
|
|
7
|
+
login: (password: string) => Promise<{ success: boolean; error?: string }>
|
|
8
|
+
logout: () => Promise<void>
|
|
9
|
+
checkAuth: () => Promise<void>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AuthContext = createContext<AuthContextType | null>(null)
|
|
13
|
+
|
|
14
|
+
export function useAuth() {
|
|
15
|
+
const context = useContext(AuthContext)
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw new Error('useAuth must be used within an AuthProvider')
|
|
18
|
+
}
|
|
19
|
+
return context
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AuthProviderProps {
|
|
23
|
+
children: ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AuthProvider({ children }: AuthProviderProps) {
|
|
27
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
|
28
|
+
const [isAuthEnabled, setIsAuthEnabled] = useState(false)
|
|
29
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
30
|
+
|
|
31
|
+
const checkAuth = useCallback(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch('/api/auth/status')
|
|
34
|
+
const data = await res.json()
|
|
35
|
+
setIsAuthEnabled(data.enabled)
|
|
36
|
+
setIsAuthenticated(data.authenticated)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('Failed to check auth status:', err)
|
|
39
|
+
setIsAuthenticated(false)
|
|
40
|
+
} finally {
|
|
41
|
+
setIsLoading(false)
|
|
42
|
+
}
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
const login = async (password: string): Promise<{ success: boolean; error?: string }> => {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch('/api/auth/login', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ password }),
|
|
51
|
+
})
|
|
52
|
+
const data = await res.json()
|
|
53
|
+
|
|
54
|
+
if (data.success) {
|
|
55
|
+
setIsAuthenticated(true)
|
|
56
|
+
return { success: true }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { success: false, error: data.error || 'Login failed' }
|
|
60
|
+
} catch (_err) {
|
|
61
|
+
return { success: false, error: 'Network error' }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const logout = async () => {
|
|
66
|
+
try {
|
|
67
|
+
await fetch('/api/auth/logout', { method: 'POST' })
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('Logout error:', err)
|
|
70
|
+
} finally {
|
|
71
|
+
setIsAuthenticated(false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
checkAuth()
|
|
77
|
+
}, [checkAuth])
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<AuthContext.Provider
|
|
81
|
+
value={{
|
|
82
|
+
isAuthenticated,
|
|
83
|
+
isAuthEnabled,
|
|
84
|
+
isLoading,
|
|
85
|
+
login,
|
|
86
|
+
logout,
|
|
87
|
+
checkAuth,
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</AuthContext.Provider>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface Notification {
|
|
4
|
+
id: string
|
|
5
|
+
type: 'info' | 'success' | 'warning' | 'error'
|
|
6
|
+
title: string
|
|
7
|
+
message: string
|
|
8
|
+
timestamp: number
|
|
9
|
+
read: boolean
|
|
10
|
+
source?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface NotificationContextType {
|
|
14
|
+
notifications: Notification[]
|
|
15
|
+
unreadCount: number
|
|
16
|
+
addNotification: (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void
|
|
17
|
+
markAsRead: (id: string) => void
|
|
18
|
+
markAllAsRead: () => void
|
|
19
|
+
clearAll: () => void
|
|
20
|
+
removeNotification: (id: string) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const NotificationContext = createContext<NotificationContextType | null>(null)
|
|
24
|
+
|
|
25
|
+
export function useNotifications() {
|
|
26
|
+
const context = useContext(NotificationContext)
|
|
27
|
+
if (!context) {
|
|
28
|
+
throw new Error('useNotifications must be used within a NotificationProvider')
|
|
29
|
+
}
|
|
30
|
+
return context
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface NotificationProviderProps {
|
|
34
|
+
children: ReactNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function NotificationProvider({ children }: NotificationProviderProps) {
|
|
38
|
+
const [notifications, setNotifications] = useState<Notification[]>([])
|
|
39
|
+
|
|
40
|
+
const unreadCount = notifications.filter((n) => !n.read).length
|
|
41
|
+
|
|
42
|
+
const addNotification = useCallback(
|
|
43
|
+
(notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => {
|
|
44
|
+
const newNotification: Notification = {
|
|
45
|
+
...notification,
|
|
46
|
+
id: crypto.randomUUID(),
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
read: false,
|
|
49
|
+
}
|
|
50
|
+
setNotifications((prev) => [newNotification, ...prev].slice(0, 50)) // Keep max 50 notifications
|
|
51
|
+
},
|
|
52
|
+
[]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const markAsRead = useCallback((id: string) => {
|
|
56
|
+
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)))
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
const markAllAsRead = useCallback(() => {
|
|
60
|
+
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const clearAll = useCallback(() => {
|
|
64
|
+
setNotifications([])
|
|
65
|
+
}, [])
|
|
66
|
+
|
|
67
|
+
const removeNotification = useCallback((id: string) => {
|
|
68
|
+
setNotifications((prev) => prev.filter((n) => n.id !== id))
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
// Listen to the global log update event dispatched by Layout's SSE stream
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const handler = (e: any) => {
|
|
74
|
+
const log = e.detail
|
|
75
|
+
if (log.level === 'error' || log.level === 'warn') {
|
|
76
|
+
addNotification({
|
|
77
|
+
type: log.level === 'error' ? 'error' : 'warning',
|
|
78
|
+
title: log.level === 'error' ? 'Job Failed' : 'Warning',
|
|
79
|
+
message: log.message || 'An event occurred',
|
|
80
|
+
source: log.queue || log.source,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
window.addEventListener('flux-log-update', handler)
|
|
85
|
+
return () => window.removeEventListener('flux-log-update', handler)
|
|
86
|
+
}, [addNotification])
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<NotificationContext.Provider
|
|
90
|
+
value={{
|
|
91
|
+
notifications,
|
|
92
|
+
unreadCount,
|
|
93
|
+
addNotification,
|
|
94
|
+
markAsRead,
|
|
95
|
+
markAllAsRead,
|
|
96
|
+
clearAll,
|
|
97
|
+
removeNotification,
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</NotificationContext.Provider>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 224 71.4% 4.1%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 224 71.4% 4.1%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 224 71.4% 4.1%;
|
|
13
|
+
--primary: 238.7 83.5% 66.7%;
|
|
14
|
+
/* Indigo 500 */
|
|
15
|
+
--primary-foreground: 210 20% 98%;
|
|
16
|
+
--secondary: 220 14.3% 95.9%;
|
|
17
|
+
--secondary-foreground: 238.7 83.5% 66.7%;
|
|
18
|
+
--muted: 220 14.3% 95.9%;
|
|
19
|
+
--muted-foreground: 220 8.9% 46.1%;
|
|
20
|
+
--accent: 220 14.3% 95.9%;
|
|
21
|
+
--accent-foreground: 238.7 83.5% 66.7%;
|
|
22
|
+
--destructive: 0 84.2% 60.2%;
|
|
23
|
+
--destructive-foreground: 210 20% 98%;
|
|
24
|
+
--border: 220 13% 91%;
|
|
25
|
+
--input: 220 13% 91%;
|
|
26
|
+
--ring: 238.7 83.5% 66.7%;
|
|
27
|
+
--radius: 1rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.dark {
|
|
31
|
+
--background: 222 47% 2%;
|
|
32
|
+
--foreground: 213 31% 91%;
|
|
33
|
+
--card: 222 47% 6%;
|
|
34
|
+
--card-foreground: 213 31% 91%;
|
|
35
|
+
--popover: 222 47% 4%;
|
|
36
|
+
--popover-foreground: 213 31% 91%;
|
|
37
|
+
--primary: 238.7 83.5% 66.7%;
|
|
38
|
+
--primary-foreground: 222 47% 4%;
|
|
39
|
+
--secondary: 222 47% 10%;
|
|
40
|
+
--secondary-foreground: 213 31% 91%;
|
|
41
|
+
--muted: 222 47% 8%;
|
|
42
|
+
--muted-foreground: 215.4 16.3% 56.9%;
|
|
43
|
+
--accent: 222 47% 12%;
|
|
44
|
+
--accent-foreground: 213 31% 91%;
|
|
45
|
+
--destructive: 0 62.8% 30.6%;
|
|
46
|
+
--destructive-foreground: 210 20% 98%;
|
|
47
|
+
--border: 222 47% 14%;
|
|
48
|
+
--input: 222 47% 12%;
|
|
49
|
+
--ring: 238.7 83.5% 66.7%;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@layer base {
|
|
54
|
+
* {
|
|
55
|
+
@apply border-border;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
body {
|
|
59
|
+
@apply bg-background text-foreground antialiased;
|
|
60
|
+
font-feature-settings: "ss01", "ss02", "cv01", "cv02", "cv03";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dark body {
|
|
64
|
+
background-image: radial-gradient(at 50% 0%, hsla(238, 83%, 66%, 0.05) 0%, transparent 50%),
|
|
65
|
+
radial-gradient(at 100% 100%, hsla(238, 83%, 66%, 0.02) 0%, transparent 50%);
|
|
66
|
+
background-attachment: fixed;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@layer utilities {
|
|
71
|
+
.scrollbar-thin::-webkit-scrollbar {
|
|
72
|
+
width: 6px;
|
|
73
|
+
height: 6px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.scrollbar-thin::-webkit-scrollbar-track {
|
|
77
|
+
background: transparent;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
81
|
+
@apply bg-muted-foreground/20 rounded-full hover:bg-muted-foreground/40 transition-colors;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.glass {
|
|
85
|
+
@apply bg-card/60 backdrop-blur-xl border-white/10;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.scanline {
|
|
89
|
+
position: relative;
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.scanline::after {
|
|
94
|
+
content: " ";
|
|
95
|
+
display: block;
|
|
96
|
+
position: absolute;
|
|
97
|
+
top: 0;
|
|
98
|
+
left: 0;
|
|
99
|
+
right: 0;
|
|
100
|
+
bottom: 0;
|
|
101
|
+
background: linear-gradient(to bottom,
|
|
102
|
+
transparent 50%,
|
|
103
|
+
rgba(var(--primary), 0.02) 50%);
|
|
104
|
+
background-size: 100% 4px;
|
|
105
|
+
z-index: 2;
|
|
106
|
+
pointer-events: none;
|
|
107
|
+
animation: scanline-move 10s linear infinite;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@keyframes scanline-move {
|
|
111
|
+
0% {
|
|
112
|
+
background-position: 0 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
100% {
|
|
116
|
+
background-position: 0 100%;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.glow-pulse {
|
|
121
|
+
animation: glow-pulse 2.5s ease-in-out infinite;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes glow-pulse {
|
|
125
|
+
|
|
126
|
+
0%,
|
|
127
|
+
100% {
|
|
128
|
+
opacity: 0.3;
|
|
129
|
+
transform: scale(1);
|
|
130
|
+
filter: blur(2px);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
50% {
|
|
134
|
+
opacity: 0.7;
|
|
135
|
+
transform: scale(1.5);
|
|
136
|
+
filter: blur(4px);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.card-premium {
|
|
141
|
+
@apply bg-card border border-border/50 shadow-sm transition-all duration-300;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.dark .card-premium {
|
|
145
|
+
background-color: hsla(222, 47%, 6%, 0.6);
|
|
146
|
+
backdrop-filter: blur(12px);
|
|
147
|
+
border-color: rgba(255, 255, 255, 0.05);
|
|
148
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.dark .card-premium:hover {
|
|
152
|
+
border-color: hsla(238, 83%, 66%, 0.2);
|
|
153
|
+
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.05);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.animate-in {
|
|
158
|
+
animation-duration: 400ms;
|
|
159
|
+
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
|
160
|
+
fill-mode: forwards;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@keyframes toast-progress {
|
|
164
|
+
from {
|
|
165
|
+
transform: scaleX(1);
|
|
166
|
+
}
|
|
167
|
+
to {
|
|
168
|
+
transform: scaleX(0);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.animate-toast-progress {
|
|
173
|
+
animation: toast-progress 5s linear forwards;
|
|
174
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Gravito Zenith</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import ReactDOM from 'react-dom/client'
|
|
4
|
+
import App from './App'
|
|
5
|
+
import './index.css'
|
|
6
|
+
|
|
7
|
+
const queryClient = new QueryClient()
|
|
8
|
+
|
|
9
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
10
|
+
<React.StrictMode>
|
|
11
|
+
<QueryClientProvider client={queryClient}>
|
|
12
|
+
<App />
|
|
13
|
+
</QueryClientProvider>
|
|
14
|
+
</React.StrictMode>
|
|
15
|
+
)
|