@gravito/zenith 1.1.2 → 1.1.3

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 (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +77 -22
  3. package/README.zh-TW.md +88 -0
  4. package/dist/bin.js +64681 -15842
  5. package/dist/client/assets/index-C80c1frR.css +1 -0
  6. package/dist/client/assets/index-CrWem9u3.js +434 -0
  7. package/dist/server/index.js +64681 -15842
  8. package/package.json +9 -7
  9. package/postcss.config.js +4 -4
  10. package/src/client/Layout.tsx +36 -39
  11. package/src/client/Sidebar.tsx +7 -7
  12. package/src/client/ThroughputChart.tsx +31 -17
  13. package/src/client/WorkerStatus.tsx +56 -80
  14. package/src/client/components/ConfirmDialog.tsx +22 -14
  15. package/src/client/components/JobInspector.tsx +95 -162
  16. package/src/client/index.css +29 -31
  17. package/src/client/pages/LoginPage.tsx +33 -31
  18. package/src/client/pages/MetricsPage.tsx +65 -37
  19. package/src/client/pages/OverviewPage.tsx +30 -28
  20. package/src/client/pages/PulsePage.tsx +111 -190
  21. package/src/client/pages/QueuesPage.tsx +82 -83
  22. package/src/client/pages/SchedulesPage.tsx +56 -61
  23. package/src/client/pages/SettingsPage.tsx +118 -137
  24. package/src/client/pages/WorkersPage.tsx +101 -115
  25. package/src/server/services/CommandService.ts +8 -9
  26. package/src/server/services/PulseService.ts +61 -4
  27. package/src/server/services/QueueService.ts +293 -0
  28. package/src/shared/types.ts +38 -13
  29. package/tailwind.config.js +75 -68
  30. package/tsconfig.json +28 -37
  31. package/tsconfig.node.json +9 -11
  32. package/dist/client/assets/index-BSMp8oq_.js +0 -436
  33. package/dist/client/assets/index-BwxlHx-_.css +0 -1
  34. package/dist/client/index.html +0 -13
  35. package/src/client/index.html +0 -12
  36. /package/{ECOSYSTEM_EXPANSION_RFC.md → doc/ECOSYSTEM_EXPANSION_RFC.md} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { useQuery, useQueryClient } from '@tanstack/react-query'
2
2
  import { motion } from 'framer-motion'
3
- import { AlertCircle, ArrowRight, CheckCircle2, Clock, Search } from 'lucide-react'
3
+ import { AlertCircle, CheckCircle2, Clock, RefreshCcw, Search } from 'lucide-react'
4
4
  import React from 'react'
5
5
  import { createPortal } from 'react-dom'
6
6
  import { cn } from '../utils'
@@ -48,7 +48,6 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
48
48
  const [view, setView] = React.useState<'waiting' | 'delayed' | 'failed' | 'archive'>('waiting')
49
49
  const [page, setPage] = React.useState(1)
50
50
  const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set())
51
- const [totalCount, setTotalCount] = React.useState<number>(0)
52
51
  const [isProcessing, setIsProcessing] = React.useState(false)
53
52
  const [confirmDialog, setConfirmDialog] = React.useState<{
54
53
  open: boolean
@@ -71,18 +70,6 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
71
70
  },
72
71
  })
73
72
 
74
- // Fetch total count for non-archive views
75
- React.useEffect(() => {
76
- if (view !== 'archive') {
77
- fetch(`/api/queues/${queueName}/jobs/count?type=${view}`)
78
- .then((res) => res.json())
79
- .then((data) => setTotalCount(data.count))
80
- .catch(() => setTotalCount(0))
81
- } else {
82
- setTotalCount(data?.total || 0)
83
- }
84
- }, [queueName, view, data?.total])
85
-
86
73
  // Reset selection when view changes
87
74
  // biome-ignore lint/correctness/useExhaustiveDependencies: We want to reset when view changes
88
75
  React.useEffect(() => {
@@ -213,39 +200,6 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
213
200
  })
214
201
  }
215
202
 
216
- const handleBulkActionAll = async (action: 'delete' | 'retry') => {
217
- if (view === 'archive') {
218
- return
219
- }
220
-
221
- setConfirmDialog({
222
- open: true,
223
- title: `${action === 'delete' ? 'Delete' : 'Retry'} ALL ${totalCount} Jobs?`,
224
- message: `⚠️ WARNING: This will ${action} ALL ${totalCount} ${view} jobs in "${queueName}".\n\nThis is a destructive operation that cannot be undone.`,
225
- variant: 'danger',
226
- action: async () => {
227
- setIsProcessing(true)
228
- try {
229
- const endpoint = action === 'delete' ? 'bulk-delete-all' : 'bulk-retry-all'
230
- await fetch(`/api/queues/${queueName}/jobs/${endpoint}`, {
231
- method: 'POST',
232
- headers: { 'Content-Type': 'application/json' },
233
- body: JSON.stringify({ type: view }),
234
- })
235
-
236
- setSelectedIndices(new Set())
237
- queryClient.invalidateQueries({ queryKey: ['jobs', queueName] })
238
- queryClient.invalidateQueries({ queryKey: ['queues'] })
239
- setConfirmDialog(null)
240
- } catch (err) {
241
- console.error(`Failed to ${action} all jobs:`, err)
242
- } finally {
243
- setIsProcessing(false)
244
- }
245
- },
246
- })
247
- }
248
-
249
203
  return createPortal(
250
204
  <div className="fixed inset-0 z-[1001] flex items-center justify-end p-4 sm:p-6 outline-none pointer-events-none">
251
205
  <motion.div
@@ -261,32 +215,32 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
261
215
  animate={{ x: 0, opacity: 1 }}
262
216
  exit={{ x: '100%', opacity: 0 }}
263
217
  transition={{ type: 'spring', damping: 25, stiffness: 200 }}
264
- className="bg-card border border-border/50 h-[85vh] w-full max-w-2xl shadow-2xl flex flex-col overflow-hidden rounded-2xl relative z-[1002] pointer-events-auto"
218
+ className="bg-zinc-950 border-l border-white/10 h-screen w-full max-w-2xl shadow-2xl flex flex-col overflow-hidden relative z-[1002] pointer-events-auto"
265
219
  onClick={(e) => e.stopPropagation()}
266
220
  >
267
- <div className="p-6 border-b flex justify-between items-center bg-muted/20 flex-shrink-0">
221
+ <div className="p-8 border-b border-white/5 bg-black/40 flex justify-between items-center flex-shrink-0">
268
222
  <div>
269
- <h2 className="text-xl font-bold flex items-center gap-2">
270
- <Search className="text-primary" size={20} />
271
- Queue Insight: <span className="text-primary">{queueName}</span>
223
+ <h2 className="text-2xl font-black flex items-center gap-3 font-heading tracking-tight italic uppercase">
224
+ <Search className="text-primary" size={24} />
225
+ Inspector <span className="text-primary/60">/</span> {queueName}
272
226
  </h2>
273
- <div className="flex items-center gap-4 mt-2">
227
+ <div className="flex items-center gap-3 mt-4">
274
228
  {(['waiting', 'delayed', 'failed', 'archive'] as const).map((v) => (
275
229
  <button
276
230
  type="button"
277
231
  key={v}
278
232
  onClick={() => setView(v)}
279
233
  className={cn(
280
- 'text-[9px] font-black px-3 py-1 rounded-sm transition-all border shrink-0 uppercase tracking-widest',
234
+ 'text-[9px] font-black px-3 py-1.5 rounded-lg transition-all border shrink-0 uppercase tracking-[0.2em] font-mono',
281
235
  view === v
282
236
  ? v === 'failed'
283
- ? 'bg-red-500 text-white border-red-500 shadow-lg shadow-red-500/20'
237
+ ? 'bg-red-500 text-black border-red-500 shadow-[0_0_20px_rgba(239,68,68,0.2)]'
284
238
  : v === 'delayed'
285
- ? 'bg-amber-500 text-white border-amber-500 shadow-lg shadow-amber-500/20'
239
+ ? 'bg-amber-500 text-black border-amber-500 shadow-[0_0_20px_rgba(245,158,11,0.2)]'
286
240
  : v === 'archive'
287
- ? 'bg-indigo-500 text-white border-indigo-500 shadow-lg shadow-indigo-500/20'
288
- : 'bg-primary text-primary-foreground border-primary shadow-lg shadow-primary/20'
289
- : 'bg-muted text-muted-foreground border-transparent hover:bg-muted/80'
241
+ ? 'bg-indigo-500 text-black border-indigo-500 shadow-[0_0_20px_rgba(99,102,241,0.2)]'
242
+ : 'bg-primary text-black border-primary shadow-[0_0_20px_rgba(0,240,255,0.2)]'
243
+ : 'bg-zinc-900 text-muted-foreground border-white/5 hover:bg-zinc-800'
290
244
  )}
291
245
  >
292
246
  {v}
@@ -297,158 +251,135 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
297
251
  <button
298
252
  type="button"
299
253
  onClick={onClose}
300
- className="w-10 h-10 rounded-full hover:bg-muted flex items-center justify-center transition-colors"
254
+ aria-label="Close"
255
+ className="w-12 h-12 rounded-2xl bg-white/5 border border-white/5 hover:bg-white/10 flex items-center justify-center transition-all text-white/40 hover:text-white"
301
256
  >
302
257
 
303
258
  </button>
304
259
  </div>
305
260
 
306
- <div className="flex-1 overflow-y-auto bg-muted/5 min-h-0">
261
+ <div className="flex-1 overflow-y-auto bg-black/20 min-h-0 scrollbar-thin">
307
262
  {isPending && (
308
- <div className="p-12 text-center text-muted-foreground font-medium animate-pulse">
309
- Loading jobs...
263
+ <div className="p-20 text-center flex flex-col items-center gap-4">
264
+ <RefreshCcw className="animate-spin text-primary opacity-40" size={32} />
265
+ <p className="text-[10px] font-black uppercase tracking-[0.3em] text-muted-foreground animate-pulse">
266
+ Syncing jobs...
267
+ </p>
310
268
  </div>
311
269
  )}
312
270
  {error && (
313
- <div className="p-12 text-center text-red-500 font-bold">Error loading jobs</div>
271
+ <div className="p-20 text-center">
272
+ <div className="bg-red-500/10 text-red-500 p-8 rounded-2xl border border-red-500/20 font-black uppercase text-xs tracking-widest italic">
273
+ Connection Fault: {error.message}
274
+ </div>
275
+ </div>
314
276
  )}
315
277
 
316
278
  {data?.jobs && data.jobs.length > 0 && (
317
- <>
318
- <div className="px-6 py-3 border-b bg-muted/5 flex items-center gap-3">
319
- <input
320
- type="checkbox"
321
- className="w-4 h-4 rounded border-border"
322
- checked={
323
- selectedIndices.size ===
324
- data.jobs.filter((j) => j._raw && !j._archived).length &&
325
- selectedIndices.size > 0
326
- }
327
- onChange={toggleSelectAll}
328
- />
329
- <span className="text-[10px] font-black uppercase tracking-widest text-muted-foreground">
330
- Select All (Page)
331
- </span>
332
- {view !== 'archive' && totalCount > 0 && (
333
- <span className="text-[9px] text-muted-foreground/60 ml-2">
334
- {data.jobs.filter((j) => !j._archived).length} of {totalCount} total
279
+ <div className="px-8 py-4 border-b border-white/5 bg-white/[0.02] flex items-center gap-4">
280
+ <input
281
+ type="checkbox"
282
+ aria-label="Select all jobs"
283
+ className="w-4 h-4 rounded border-white/10 bg-black/40 text-primary focus:ring-primary/20"
284
+ checked={
285
+ selectedIndices.size === data.jobs.filter((j) => j._raw && !j._archived).length &&
286
+ selectedIndices.size > 0
287
+ }
288
+ onChange={toggleSelectAll}
289
+ />
290
+ <span className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 font-heading">
291
+ Batch Operations
292
+ </span>
293
+ {selectedIndices.size > 0 && (
294
+ <div className="ml-auto flex items-center gap-3">
295
+ <span className="text-[10px] font-black uppercase text-primary font-mono bg-primary/10 px-2 py-0.5 rounded border border-primary/20">
296
+ {selectedIndices.size} Selected
335
297
  </span>
336
- )}
337
- {selectedIndices.size > 0 && (
338
- <div className="ml-auto flex items-center gap-2">
339
- <span className="text-[10px] font-black uppercase text-primary mr-2">
340
- {selectedIndices.size} items selected
341
- </span>
342
- <button
343
- type="button"
344
- onClick={() => handleBulkAction('delete')}
345
- className="px-3 py-1 bg-red-500/10 text-red-500 rounded-md text-[10px] font-black uppercase hover:bg-red-500 hover:text-white transition-all"
346
- >
347
- Delete Selected
348
- </button>
349
- {(view === 'delayed' || view === 'failed') && (
350
- <button
351
- type="button"
352
- onClick={() => handleBulkAction('retry')}
353
- className="px-3 py-1 bg-primary/10 text-primary rounded-md text-[10px] font-black uppercase hover:bg-primary hover:text-primary-foreground transition-all"
354
- >
355
- Retry Selected
356
- </button>
357
- )}
358
- </div>
359
- )}
360
- </div>
361
- {view !== 'archive' && totalCount > data.jobs.filter((j) => !j._archived).length && (
362
- <div className="px-6 py-3 border-b bg-amber-500/5 flex items-center justify-between">
363
- <span className="text-xs font-bold text-amber-600 flex items-center gap-2">
364
- <AlertCircle size={14} />
365
- Showing {data.jobs.filter((j) => !j._archived).length} of {totalCount} total{' '}
366
- {view} jobs
367
- </span>
368
- <div className="flex items-center gap-2">
298
+ <button
299
+ type="button"
300
+ onClick={() => handleBulkAction('delete')}
301
+ className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-lg text-[10px] font-black uppercase hover:bg-red-500 hover:text-white transition-all border border-red-500/20"
302
+ >
303
+ Delete
304
+ </button>
305
+ {(view === 'delayed' || view === 'failed') && (
369
306
  <button
370
307
  type="button"
371
- onClick={() => handleBulkActionAll('delete')}
372
- className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-md text-[10px] font-black uppercase hover:bg-red-500 hover:text-white transition-all"
308
+ onClick={() => handleBulkAction('retry')}
309
+ className="px-3 py-1.5 bg-primary/10 text-primary rounded-lg text-[10px] font-black uppercase hover:bg-primary hover:text-black transition-all border border-primary/20"
373
310
  >
374
- Delete All {totalCount}
311
+ Retry
375
312
  </button>
376
- {(view === 'delayed' || view === 'failed') && (
377
- <button
378
- type="button"
379
- onClick={() => handleBulkActionAll('retry')}
380
- className="px-3 py-1.5 bg-amber-500/10 text-amber-600 rounded-md text-[10px] font-black uppercase hover:bg-amber-500 hover:text-white transition-all"
381
- >
382
- Retry All {totalCount}
383
- </button>
384
- )}
385
- </div>
313
+ )}
386
314
  </div>
387
315
  )}
388
- </>
316
+ </div>
389
317
  )}
390
318
 
391
319
  {data?.jobs && data.jobs.length === 0 && (
392
- <div className="p-12 text-center text-muted-foreground flex flex-col items-center gap-4">
393
- <div className="w-16 h-16 bg-muted/50 rounded-full flex items-center justify-center text-muted-foreground/30">
394
- <CheckCircle2 size={32} />
320
+ <div className="p-20 text-center text-muted-foreground flex flex-col items-center gap-6 opacity-40">
321
+ <div className="w-20 h-20 bg-white/5 rounded-full flex items-center justify-center text-primary/40 border border-white/5">
322
+ <CheckCircle2 size={40} />
323
+ </div>
324
+ <div className="space-y-2">
325
+ <p className="text-xl font-black font-heading uppercase italic tracking-widest">
326
+ Pipeline Clear
327
+ </p>
328
+ <p className="text-[10px] font-black uppercase tracking-[0.2em]">
329
+ Zero incidents detected in spectrum
330
+ </p>
395
331
  </div>
396
- <p className="text-lg font-bold">Clear Sky!</p>
397
- <p className="text-sm opacity-60">No jobs found in this queue.</p>
398
332
  </div>
399
333
  )}
334
+
400
335
  {data?.jobs && (
401
- <div className="p-6 space-y-4">
336
+ <div className="p-8 space-y-6">
402
337
  {data.jobs.map((job, i) => (
403
338
  <div
404
339
  key={i}
405
340
  className={cn(
406
- 'bg-card border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all group border-border/50',
407
- selectedIndices.has(i) && 'ring-2 ring-primary border-primary'
341
+ 'bg-zinc-900/40 border rounded-2xl overflow-hidden transition-all group border-white/5',
342
+ selectedIndices.has(i) && 'ring-2 ring-primary border-primary bg-primary/5'
408
343
  )}
409
344
  >
410
- <div className="p-4 border-b bg-muted/10 flex justify-between items-center text-[10px]">
411
- <div className="flex items-center gap-3">
345
+ <div className="p-4 border-b border-white/5 bg-black/40 flex justify-between items-center text-[10px] font-mono">
346
+ <div className="flex items-center gap-4">
412
347
  {job._raw && !job._archived && (
413
348
  <input
414
349
  type="checkbox"
415
- className="w-4 h-4 rounded border-border"
350
+ aria-label={`Select job ${job.id || i}`}
351
+ className="w-4 h-4 rounded border-white/10 bg-black/40 text-primary focus:ring-primary/20"
416
352
  checked={selectedIndices.has(i)}
417
353
  onChange={() => toggleSelection(i)}
418
354
  />
419
355
  )}
420
- <span className="font-mono bg-primary/10 text-primary px-2 py-1 rounded-md font-bold uppercase tracking-wider flex items-center gap-2">
421
- ID: {job.id || 'N/A'}
356
+ <span className="bg-primary/10 text-primary px-2 py-1 rounded-md font-black uppercase tracking-widest flex items-center gap-2 border border-primary/20 shadow-[0_0_15px_rgba(0,240,255,0.05)]">
357
+ ID:{job.id || 'N/A'}
422
358
  {job._archived && (
423
359
  <span
424
360
  className={cn(
425
- 'px-1.5 py-0.5 rounded text-[8px] border',
361
+ 'px-1.5 py-0.5 rounded text-[8px] border ml-1',
426
362
  job._status === 'completed'
427
363
  ? 'bg-green-500/20 text-green-500 border-green-500/20'
428
364
  : 'bg-red-500/20 text-red-500 border-red-500/20'
429
365
  )}
430
366
  >
431
- Archive: {job._status}
367
+ {job._status}
432
368
  </span>
433
369
  )}
434
370
  </span>
435
371
  </div>
436
- <span className="text-muted-foreground font-semibold flex items-center gap-3">
372
+ <span className="text-white/20 font-black flex items-center gap-4 uppercase tracking-tighter">
437
373
  {view === 'delayed' && job.scheduledAt && (
438
- <span className="text-amber-500 flex items-center gap-1 font-bold">
374
+ <span className="text-amber-500 flex items-center gap-1.5">
439
375
  <Clock size={12} /> {new Date(job.scheduledAt).toLocaleString()}
440
376
  </span>
441
377
  )}
442
378
  {view === 'failed' && job.failedAt && (
443
- <span className="text-red-500 flex items-center gap-1 font-bold">
379
+ <span className="text-red-500 flex items-center gap-1.5">
444
380
  <AlertCircle size={12} /> {new Date(job.failedAt).toLocaleString()}
445
381
  </span>
446
382
  )}
447
- {job._archivedAt && (
448
- <span className="text-indigo-400 flex items-center gap-1 font-bold">
449
- <ArrowRight size={12} /> {new Date(job._archivedAt).toLocaleString()}
450
- </span>
451
- )}
452
383
  {job.timestamp &&
453
384
  !job._archivedAt &&
454
385
  new Date(job.timestamp).toLocaleString()}
@@ -457,24 +388,24 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
457
388
  <button
458
389
  type="button"
459
390
  onClick={() => job._raw && !job._archived && toggleSelection(i)}
460
- className="w-full text-left cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:ring-inset"
391
+ className="w-full text-left cursor-pointer focus:outline-none focus:ring-inset"
461
392
  >
462
393
  {job.error && (
463
- <div className="p-4 bg-red-500/10 text-red-500 text-xs font-semibold border-b border-red-500/10 flex items-start gap-2">
464
- <AlertCircle size={14} className="mt-0.5 shrink-0" />
394
+ <div className="p-5 bg-red-500/10 text-red-500 text-xs font-black border-b border-red-500/10 flex items-start gap-3 uppercase tracking-tight">
395
+ <AlertCircle size={16} className="mt-0.5 shrink-0" />
465
396
  <p>{job.error}</p>
466
397
  </div>
467
398
  )}
468
- <pre className="text-[11px] font-mono p-4 overflow-x-auto text-foreground/80 leading-relaxed bg-muted/5">
399
+ <pre className="text-[11px] font-mono p-6 overflow-x-auto text-white/60 leading-relaxed bg-black/40">
469
400
  {JSON.stringify(job, null, 2)}
470
401
  </pre>
471
402
  </button>
472
- <div className="p-3 bg-muted/5 border-t border-border/50 flex justify-end gap-2">
403
+ <div className="p-4 bg-black/20 border-t border-white/5 flex justify-end gap-3">
473
404
  {!job._archived && (
474
405
  <button
475
406
  type="button"
476
407
  onClick={() => handleAction('delete', job)}
477
- className="text-[10px] font-bold uppercase tracking-widest px-4 py-2 rounded-lg hover:bg-red-500/10 text-red-500 transition-colors"
408
+ className="text-[10px] font-black uppercase tracking-[0.2em] px-5 py-2.5 rounded-xl hover:bg-red-500/10 text-red-500/60 hover:text-red-500 transition-all font-heading border border-transparent hover:border-red-500/20"
478
409
  >
479
410
  Terminate
480
411
  </button>
@@ -484,13 +415,13 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
484
415
  type="button"
485
416
  onClick={() => handleAction('retry', job)}
486
417
  className={cn(
487
- 'text-[10px] font-bold uppercase tracking-widest px-4 py-2 rounded-lg text-white shadow-sm transition-all',
418
+ 'text-[10px] font-black uppercase tracking-[0.2em] px-5 py-2.5 rounded-xl text-black shadow-lg transition-all font-heading',
488
419
  view === 'delayed'
489
- ? 'bg-amber-500 hover:bg-amber-600'
490
- : 'bg-blue-500 hover:bg-blue-600'
420
+ ? 'bg-amber-500 shadow-amber-500/20 hover:bg-amber-400'
421
+ : 'bg-primary shadow-primary/20 hover:bg-primary/80'
491
422
  )}
492
423
  >
493
- {view === 'delayed' ? 'Process Now' : 'Retry Job'}
424
+ {view === 'delayed' ? 'Execute Now' : 'Re-Run Cycle'}
494
425
  </button>
495
426
  )}
496
427
  </div>
@@ -507,6 +438,7 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
507
438
  type="button"
508
439
  onClick={() => setPage((p) => Math.max(1, p - 1))}
509
440
  disabled={page === 1}
441
+ aria-label="Previous page"
510
442
  className="p-2 rounded-lg bg-muted text-muted-foreground disabled:opacity-30 hover:bg-primary hover:text-white transition-all"
511
443
  >
512
444
 
@@ -516,6 +448,7 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
516
448
  type="button"
517
449
  onClick={() => setPage((p) => p + 1)}
518
450
  disabled={page * 50 >= (data.total || 0)}
451
+ aria-label="Next page"
519
452
  className="p-2 rounded-lg bg-muted text-muted-foreground disabled:opacity-30 hover:bg-primary hover:text-white transition-all"
520
453
  >
521
454
 
@@ -2,6 +2,8 @@
2
2
  @tailwind components;
3
3
  @tailwind utilities;
4
4
 
5
+ @import url("https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Poppins:wght@400;500;600;700;800;900&display=swap");
6
+
5
7
  @layer base {
6
8
  :root {
7
9
  --background: 0 0% 100%;
@@ -24,29 +26,29 @@
24
26
  --border: 220 13% 91%;
25
27
  --input: 220 13% 91%;
26
28
  --ring: 238.7 83.5% 66.7%;
27
- --radius: 1rem;
29
+ --radius: 1.25rem;
28
30
  }
29
31
 
30
32
  .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%;
33
+ --background: 240 10% 3.9%;
34
+ --foreground: 0 0% 98%;
35
+ --card: 240 10% 3.9%;
36
+ --card-foreground: 0 0% 98%;
37
+ --popover: 240 10% 3.9%;
38
+ --popover-foreground: 0 0% 98%;
39
+ --primary: 180 100% 50%; /* Electric Cyan */
40
+ --primary-foreground: 240 5.9% 10%;
41
+ --secondary: 240 3.7% 15.9%;
42
+ --secondary-foreground: 0 0% 98%;
43
+ --muted: 240 3.7% 10%;
44
+ --muted-foreground: 240 5% 64.9%;
45
+ --accent: 240 3.7% 15.9%;
46
+ --accent-foreground: 0 0% 98%;
45
47
  --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%;
48
+ --destructive-foreground: 0 0% 98%;
49
+ --border: 240 3.7% 15.9%;
50
+ --input: 240 3.7% 15.9%;
51
+ --ring: 180 100% 50%;
50
52
  }
51
53
  }
52
54
 
@@ -61,7 +63,8 @@
61
63
  }
62
64
 
63
65
  .dark body {
64
- background-image: radial-gradient(at 50% 0%, hsla(238, 83%, 66%, 0.05) 0%, transparent 50%),
66
+ background-image:
67
+ radial-gradient(at 50% 0%, hsla(238, 83%, 66%, 0.05) 0%, transparent 50%),
65
68
  radial-gradient(at 100% 100%, hsla(238, 83%, 66%, 0.02) 0%, transparent 50%);
66
69
  background-attachment: fixed;
67
70
  }
@@ -98,9 +101,7 @@
98
101
  left: 0;
99
102
  right: 0;
100
103
  bottom: 0;
101
- background: linear-gradient(to bottom,
102
- transparent 50%,
103
- rgba(var(--primary), 0.02) 50%);
104
+ background: linear-gradient(to bottom, transparent 50%, rgba(var(--primary), 0.02) 50%);
104
105
  background-size: 100% 4px;
105
106
  z-index: 2;
106
107
  pointer-events: none;
@@ -122,7 +123,6 @@
122
123
  }
123
124
 
124
125
  @keyframes glow-pulse {
125
-
126
126
  0%,
127
127
  100% {
128
128
  opacity: 0.3;
@@ -142,15 +142,13 @@
142
142
  }
143
143
 
144
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);
145
+ @apply bg-zinc-900/40 backdrop-blur-xl border-white/5;
146
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.8);
149
147
  }
150
148
 
151
149
  .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);
150
+ @apply border-primary/20 bg-zinc-900/60;
151
+ box-shadow: 0 8px 40px rgba(0, 240, 255, 0.05);
154
152
  }
155
153
  }
156
154
 
@@ -171,4 +169,4 @@
171
169
 
172
170
  .animate-toast-progress {
173
171
  animation: toast-progress 5s linear forwards;
174
- }
172
+ }