UI Overhaul

This commit is contained in:
rmoren97 2026-02-08 00:05:12 -08:00
parent c3db15827d
commit fdb7b19cbc
31 changed files with 339 additions and 213 deletions

View File

@ -68,7 +68,7 @@ export default function AuditPage() {
value={search}
onChange={e => { setSearch(e.target.value); setPage(1) }}
placeholder="Search audit logs..."
className="w-full bg-gray-800 border border-gray-700 text-gray-100 text-sm rounded-lg pl-10 pr-4 py-2 focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 outline-none"
className="w-full glass-input text-gray-100 text-sm rounded-lg pl-10 pr-4 py-2 focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent outline-none"
/>
</div>
</div>
@ -81,7 +81,7 @@ export default function AuditPage() {
</div>
{/* Table */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="glass rounded-xl overflow-hidden">
{loading ? (
<div className="flex items-center justify-center p-12">
<Spinner size="lg" />
@ -95,7 +95,7 @@ export default function AuditPage() {
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-800/50">
<thead className="bg-white/[0.05]">
<tr>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Timestamp</th>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">User</th>
@ -105,9 +105,9 @@ export default function AuditPage() {
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">IP</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
<tbody className="divide-y divide-white/[0.08]">
{logs.map(log => (
<tr key={log._id} className="hover:bg-gray-800">
<tr key={log._id} className="hover:bg-white/[0.06]">
<td className="px-6 py-3 text-xs text-gray-400 whitespace-nowrap">
{formatDateTime(log.createdAt)}
</td>
@ -139,7 +139,7 @@ export default function AuditPage() {
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-700">
<div className="flex items-center justify-between px-6 py-3 border-t border-white/[0.1]">
<p className="text-sm text-gray-500">
Page {page} of {totalPages}
</p>
@ -147,14 +147,14 @@ export default function AuditPage() {
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="px-3 py-1.5 text-sm bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1.5 text-sm bg-white/[0.08] border border-white/[0.12] hover:border-white/[0.2] text-gray-200 rounded-lg hover:bg-white/[0.14] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
Previous
</button>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-3 py-1.5 text-sm bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1.5 text-sm bg-white/[0.08] border border-white/[0.12] hover:border-white/[0.2] text-gray-200 rounded-lg hover:bg-white/[0.14] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
Next
</button>

View File

@ -93,9 +93,9 @@ export default function DashboardPage() {
</div>
{/* Server List */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-700/50">
<h2 className="text-lg font-semibold text-gray-100">Servers</h2>
<div className="glass rounded-xl overflow-hidden">
<div className="px-6 py-4 border-b border-white/[0.1]">
<h2 className="text-lg font-semibold text-white">Servers</h2>
</div>
{servers.length === 0 ? (
@ -103,12 +103,12 @@ export default function DashboardPage() {
No servers yet. Create your first Minecraft server!
</div>
) : (
<div className="divide-y divide-gray-700">
<div className="divide-y divide-white/[0.08]">
{servers.map(server => (
<a
key={server._id}
href={`/servers/${server._id}`}
className="flex items-center justify-between px-4 sm:px-6 py-4 hover:bg-gray-800 transition-colors gap-3"
className="flex items-center justify-between px-4 sm:px-6 py-4 hover:bg-white/[0.06] transition-colors gap-3"
>
<div className="flex items-center gap-4">
<Server size={20} className="text-gray-500" />
@ -140,21 +140,21 @@ function StatCard({
value: number
color: string
}) {
const colorMap: Record<string, string> = {
cyan: 'bg-cyan-500/20 text-cyan-400',
emerald: 'bg-emerald-500/20 text-emerald-400',
amber: 'bg-amber-500/20 text-amber-400',
gray: 'bg-gray-500/20 text-gray-400',
const colorMap: Record<string, { badge: string; glow: string }> = {
cyan: { badge: 'bg-cyan-400/20 text-cyan-400', glow: 'shadow-cyan-500/15' },
emerald: { badge: 'bg-emerald-400/20 text-emerald-400', glow: 'shadow-emerald-500/15' },
amber: { badge: 'bg-amber-400/20 text-amber-400', glow: 'shadow-amber-500/15' },
gray: { badge: 'bg-white/[0.1] text-gray-400', glow: '' },
}
return (
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-5">
<div className={`glass rounded-xl p-5 ${colorMap[color].glow ? `shadow-lg ${colorMap[color].glow}` : ''}`}>
<div className="flex items-center gap-3">
<div className={`p-2.5 rounded-lg ${colorMap[color]}`}>
<div className={`p-2.5 rounded-lg ${colorMap[color].badge}`}>
<Icon size={20} />
</div>
<div>
<p className="text-2xl font-bold text-gray-100">{value}</p>
<p className="text-2xl font-bold text-white">{value}</p>
<p className="text-xs text-gray-500">{label}</p>
</div>
</div>

View File

@ -9,7 +9,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-950">
<div className="flex items-center justify-center min-h-screen">
<Spinner size="lg" />
</div>
)
@ -18,9 +18,9 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
if (!user) return null // Middleware will redirect
return (
<div className="flex h-screen bg-gray-950">
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 overflow-y-auto p-4 pt-18 lg:p-6 lg:pt-6">
<main className="flex-1 overflow-y-auto overflow-x-hidden p-4 pt-[4.5rem] lg:p-6 lg:pt-6">
{children}
</main>
</div>

View File

@ -233,7 +233,7 @@ export default function RolesPage() {
const PermissionGrid = () => (
<div className="space-y-3 max-h-[400px] overflow-y-auto">
{allResources.map(resource => (
<div key={resource} className="bg-gray-800/50 rounded-lg p-3">
<div key={resource} className="bg-white/[0.06] rounded-lg p-3 border border-white/[0.1]">
<p className="text-xs font-semibold text-gray-300 uppercase tracking-wider mb-2">{resource}</p>
<div className="flex flex-wrap gap-2">
{allActions[resource].map(action => (
@ -241,10 +241,10 @@ export default function RolesPage() {
key={`${resource}:${action}`}
type="button"
onClick={() => togglePermission(resource, action)}
className={`px-2.5 py-1 rounded text-xs font-medium transition-colors ${
className={`px-2.5 py-1 rounded text-xs font-medium transition-all ${
hasPermissionToggle(resource, action)
? 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/50'
: 'bg-gray-700 text-gray-400 border border-gray-600 hover:bg-gray-600'
? 'bg-cyan-400/20 text-cyan-400 border border-cyan-400/35 shadow-[0_0_10px_rgba(6,182,212,0.2)]'
: 'bg-white/[0.08] text-gray-400 border border-white/[0.12] hover:bg-white/[0.14] hover:text-gray-300'
}`}
>
{action}

View File

@ -152,7 +152,7 @@ export default function BackupsPage() {
}
/>
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="glass rounded-xl overflow-hidden">
{backups.length === 0 ? (
<div className="p-8 text-center text-gray-500">
<HardDrive size={40} className="mx-auto mb-3 text-gray-600" />
@ -161,7 +161,7 @@ export default function BackupsPage() {
</div>
) : (
<table className="w-full">
<thead className="bg-gray-800/50">
<thead className="bg-white/[0.05]">
<tr>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Filename</th>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Type</th>
@ -171,9 +171,9 @@ export default function BackupsPage() {
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
<tbody className="divide-y divide-white/[0.08]">
{backups.map(backup => (
<tr key={backup._id} className="hover:bg-gray-800">
<tr key={backup._id} className="hover:bg-white/[0.06]">
<td className="px-6 py-4 text-sm text-gray-200 font-mono">{backup.filename}</td>
<td className="px-6 py-4">
<Badge variant={backup.type === 'manual' ? 'info' : 'neutral'}>

View File

@ -119,7 +119,7 @@ export default function ConfigurationPage() {
/>
{/* JVM Args */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-6">
<div className="glass rounded-xl p-6">
<h3 className="text-sm font-semibold text-gray-200 mb-3">JVM Arguments</h3>
<Input
value={jvmArgs}
@ -132,11 +132,11 @@ export default function ConfigurationPage() {
</div>
{/* Server Properties */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-700/50">
<div className="glass rounded-xl overflow-hidden">
<div className="px-6 py-4 border-b border-white/[0.1]">
<h3 className="text-sm font-semibold text-gray-200">server.properties</h3>
</div>
<div className="divide-y divide-gray-700">
<div className="divide-y divide-white/[0.08]">
{sortedKeys.map(key => (
<div key={key} className="flex items-center gap-4 px-6 py-3">
<label className="w-1/3 text-sm text-gray-400 font-mono truncate" title={key}>
@ -145,7 +145,7 @@ export default function ConfigurationPage() {
<input
value={properties[key]}
onChange={e => handlePropertyChange(key, e.target.value)}
className="flex-1 bg-gray-800 border border-gray-700 text-gray-100 text-sm rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 outline-none"
className="flex-1 glass-input text-gray-100 text-sm rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent outline-none"
/>
</div>
))}

View File

@ -640,7 +640,7 @@ export default function FilesPage() {
{i > 0 && <ChevronRight size={14} className="text-gray-600" />}
<button
onClick={() => navigateTo(crumb.path)}
className={`px-1.5 py-0.5 rounded hover:bg-gray-800 transition-colors ${
className={`px-1.5 py-0.5 rounded hover:bg-white/[0.06] transition-colors ${
i === breadcrumbs.length - 1
? 'text-cyan-400 font-medium'
: 'text-gray-400 hover:text-gray-200'
@ -655,7 +655,7 @@ export default function FilesPage() {
{/* New Folder Inline Form */}
{showNewFolder && !viewingFile && (
<div className="flex items-center gap-3 bg-gray-900/80 p-3 rounded-lg border border-gray-700/50">
<div className="flex items-center gap-3 glass p-3 rounded-xl">
<FolderPlus size={18} className="text-amber-400 flex-shrink-0" />
<input
type="text"
@ -664,7 +664,7 @@ export default function FilesPage() {
onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()}
placeholder="Folder name…"
autoFocus
className="flex-1 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500"
className="flex-1 glass-input text-gray-100 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent"
/>
<Button size="sm" variant="primary" onClick={handleCreateFolder} disabled={actionLoading === 'new-folder' || !newFolderName.trim()}>
Create
@ -677,8 +677,8 @@ export default function FilesPage() {
{viewingFile ? (
/* ─── File Viewer / Editor ─────────────────────────────── */
<div className="bg-gray-950 rounded-lg border border-gray-700/50 overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700/50 bg-gray-900/50">
<div className="bg-[#050510]/80 backdrop-blur-xl rounded-xl border border-white/[0.08] overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.1] bg-white/[0.04]">
<div className="flex items-center gap-2">
<File size={16} className="text-cyan-400" />
<span className="text-sm font-medium text-gray-200">{fileMeta?.name || viewingFile}</span>
@ -705,7 +705,7 @@ export default function FilesPage() {
<textarea
value={editContent || ''}
onChange={(e) => setEditContent(e.target.value)}
className="w-full h-[calc(100vh-400px)] min-h-[400px] bg-gray-950 text-gray-300 font-mono text-sm p-4 resize-none focus:outline-none"
className="w-full h-[calc(100vh-400px)] min-h-[400px] bg-transparent text-gray-300 font-mono text-sm p-4 resize-none focus:outline-none"
spellCheck={false}
/>
) : (
@ -719,7 +719,7 @@ export default function FilesPage() {
<div>
{/* ─── Bulk Action Bar ───────────────────────────────────── */}
{selectMode && selected.size > 0 && (
<div className="flex items-center gap-3 bg-cyan-500/10 border border-cyan-500/30 rounded-lg px-4 py-2.5 mb-4">
<div className="flex items-center gap-3 bg-cyan-400/5 border border-cyan-400/20 rounded-xl px-4 py-2.5 mb-4 backdrop-blur-sm">
<span className="text-sm font-medium text-cyan-400">
{selected.size} selected
</span>
@ -762,7 +762,7 @@ export default function FilesPage() {
</Button>
<button
onClick={clearSelection}
className="p-1.5 rounded hover:bg-gray-700 text-gray-400 hover:text-gray-200 transition-colors"
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-400 hover:text-gray-200 transition-colors"
title="Clear selection"
>
<X size={16} />
@ -772,7 +772,7 @@ export default function FilesPage() {
)}
{/* ─── Directory Listing ────────────────────────────────── */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="glass rounded-xl overflow-hidden">
{loading ? (
<div className="flex items-center justify-center py-12">
<Spinner size="lg" />
@ -783,10 +783,10 @@ export default function FilesPage() {
<p className="text-gray-400">Empty directory</p>
</div>
) : (
<div className="divide-y divide-gray-700/50">
<div className="divide-y divide-white/[0.08]">
{/* Select all header */}
{selectMode && entries.length > 0 && (
<div className="flex items-center gap-3 px-5 py-2 bg-gray-800/30">
<div className="flex items-center gap-3 px-5 py-2 bg-white/[0.04]">
<button
onClick={toggleSelectAll}
className="flex-shrink-0 text-gray-400 hover:text-cyan-400 transition-colors"
@ -811,7 +811,7 @@ export default function FilesPage() {
parts.pop()
navigateTo(parts.length ? parts.join('/') : '/')
}}
className="w-full flex items-center gap-3 px-5 py-3 hover:bg-gray-800/60 transition-colors text-left group"
className="w-full flex items-center gap-3 px-5 py-3 hover:bg-white/[0.06] transition-colors text-left group"
>
{selectMode && <div className="w-[18px] flex-shrink-0" />}
<Folder size={18} className="text-amber-400/70 flex-shrink-0" />
@ -822,8 +822,8 @@ export default function FilesPage() {
{entries.map((entry) => (
<div
key={entry.path}
className={`flex items-center gap-3 px-5 py-3 hover:bg-gray-800/60 transition-colors group relative ${
selected.has(entry.path) ? 'bg-cyan-500/5' : ''
className={`flex items-center gap-3 px-5 py-3 hover:bg-white/[0.06] transition-colors group relative ${
selected.has(entry.path) ? 'bg-cyan-400/[0.03]' : ''
}`}
>
{/* Checkbox */}
@ -869,7 +869,7 @@ export default function FilesPage() {
<button
onClick={(e) => { e.stopPropagation(); handleUnzip(entry) }}
disabled={actionLoading === entry.path}
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-amber-400 transition-colors"
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-amber-400 transition-colors"
title="Extract archive"
>
<ArchiveRestore size={15} />
@ -879,7 +879,7 @@ export default function FilesPage() {
<button
onClick={(e) => { e.stopPropagation(); handleZip(entry) }}
disabled={actionLoading === entry.path}
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-cyan-400 transition-colors"
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-cyan-400 transition-colors"
title="Compress to zip"
>
<Archive size={15} />
@ -891,7 +891,7 @@ export default function FilesPage() {
setMoveDest(entry.path)
setMoveModal({ source: entry.path, name: entry.name })
}}
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-cyan-400 transition-colors"
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-cyan-400 transition-colors"
title="Move / Rename"
>
<FolderInput size={15} />
@ -904,7 +904,7 @@ export default function FilesPage() {
setCopyDest(`${dir}${entry.name}-copy${entry.isDirectory ? '' : ''}`)
setCopyModal({ source: entry.path, name: entry.name })
}}
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-cyan-400 transition-colors"
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-cyan-400 transition-colors"
title="Copy"
>
<Copy size={15} />
@ -913,7 +913,7 @@ export default function FilesPage() {
<button
onClick={(e) => { e.stopPropagation(); handleDelete(entry) }}
disabled={actionLoading === entry.path}
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-red-400 transition-colors"
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-red-400 transition-colors"
title="Delete"
>
<Trash2 size={15} />
@ -942,9 +942,9 @@ export default function FilesPage() {
{/* ─── Copy Modal ────────────────────────────────────────── */}
{copyModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
<h3 className="text-lg font-semibold text-gray-100 mb-1">Copy &ldquo;{copyModal.name}&rdquo;</h3>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
<div className="glass-lg rounded-xl shadow-2xl shadow-black/40 w-full max-w-md p-6 mx-4 glass-shine">
<h3 className="text-lg font-semibold text-white mb-1">Copy &ldquo;{copyModal.name}&rdquo;</h3>
<p className="text-sm text-gray-400 mb-4">Enter the destination path (relative to server root):</p>
<input
type="text"
@ -952,7 +952,7 @@ export default function FilesPage() {
onChange={(e) => setCopyDest(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCopy()}
autoFocus
className="w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 mb-4"
className="w-full glass-input text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent mb-4"
placeholder="path/to/destination"
/>
<div className="flex justify-end gap-3">
@ -975,9 +975,9 @@ export default function FilesPage() {
{/* ─── Bulk Copy Modal ───────────────────────────────────── */}
{bulkCopyModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
<h3 className="text-lg font-semibold text-gray-100 mb-1">Copy {selected.size} item(s)</h3>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
<div className="glass-lg rounded-xl shadow-2xl shadow-black/40 w-full max-w-md p-6 mx-4 glass-shine">
<h3 className="text-lg font-semibold text-white mb-1">Copy {selected.size} item(s)</h3>
<p className="text-sm text-gray-400 mb-4">Enter the destination folder (relative to server root). Items will be copied into this folder:</p>
<input
type="text"
@ -985,7 +985,7 @@ export default function FilesPage() {
onChange={(e) => setBulkCopyDest(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleBulkCopy()}
autoFocus
className="w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 mb-4"
className="w-full glass-input text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent mb-4"
placeholder="path/to/destination-folder"
/>
<div className="flex justify-end gap-3">
@ -1008,9 +1008,9 @@ export default function FilesPage() {
{/* ─── Move Modal ────────────────────────────────────────── */}
{moveModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
<h3 className="text-lg font-semibold text-gray-100 mb-1">Move &ldquo;{moveModal.name}&rdquo;</h3>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
<div className="glass-lg rounded-xl shadow-2xl shadow-black/40 w-full max-w-md p-6 mx-4 glass-shine">
<h3 className="text-lg font-semibold text-white mb-1">Move &ldquo;{moveModal.name}&rdquo;</h3>
<p className="text-sm text-gray-400 mb-4">Enter the new path (relative to server root). Change the name to rename:</p>
<input
type="text"
@ -1018,7 +1018,7 @@ export default function FilesPage() {
onChange={(e) => setMoveDest(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleMove()}
autoFocus
className="w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 mb-4"
className="w-full glass-input text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent mb-4"
placeholder="path/to/new-location"
/>
<div className="flex justify-end gap-3">
@ -1041,9 +1041,9 @@ export default function FilesPage() {
{/* ─── Bulk Move Modal ───────────────────────────────────── */}
{bulkMoveModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
<h3 className="text-lg font-semibold text-gray-100 mb-1">Move {selected.size} item(s)</h3>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
<div className="glass-lg rounded-xl shadow-2xl shadow-black/40 w-full max-w-md p-6 mx-4 glass-shine">
<h3 className="text-lg font-semibold text-white mb-1">Move {selected.size} item(s)</h3>
<p className="text-sm text-gray-400 mb-4">Enter the destination folder (relative to server root). Items will be moved into this folder:</p>
<input
type="text"
@ -1051,7 +1051,7 @@ export default function FilesPage() {
onChange={(e) => setBulkMoveDest(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleBulkMove()}
autoFocus
className="w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 mb-4"
className="w-full glass-input text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent mb-4"
placeholder="path/to/destination-folder"
/>
<div className="flex justify-end gap-3">

View File

@ -129,8 +129,8 @@ export default function LogsPage() {
{selectedFile ? (
/* Log File Viewer */
<div className="bg-gray-950 rounded-lg border border-gray-700/50 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50 bg-gray-900/50">
<div className="bg-[#050510]/80 backdrop-blur-xl rounded-xl border border-white/[0.08] overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 border-b border-white/[0.1] bg-white/[0.04]">
<FileText size={16} className="text-cyan-400" />
<span className="text-sm font-medium text-gray-200">{selectedFile}</span>
</div>
@ -148,7 +148,7 @@ export default function LogsPage() {
</div>
) : (
/* File List */
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="glass rounded-xl overflow-hidden">
{files.length === 0 ? (
<div className="text-center py-12">
<FileText size={40} className="mx-auto text-gray-600 mb-3" />
@ -156,12 +156,12 @@ export default function LogsPage() {
<p className="text-gray-600 text-sm mt-1">Logs will appear after the server has started</p>
</div>
) : (
<div className="divide-y divide-gray-700/50">
<div className="divide-y divide-white/[0.08]">
{files.map((file) => (
<button
key={file.name}
onClick={() => openFile(file.name)}
className="w-full flex items-center gap-3 px-5 py-4 hover:bg-gray-800/60 transition-colors text-left group"
className="w-full flex items-center gap-3 px-5 py-4 hover:bg-white/[0.06] transition-colors text-left group"
>
<FileText size={18} className="text-gray-500 group-hover:text-cyan-400 transition-colors flex-shrink-0" />
<div className="flex-1 min-w-0">

View File

@ -161,7 +161,7 @@ export default function ModsPage() {
}
/>
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="glass rounded-xl overflow-hidden">
{mods.length === 0 ? (
<div className="p-8 text-center text-gray-500">
<Package size={40} className="mx-auto mb-3 text-gray-600" />
@ -170,7 +170,7 @@ export default function ModsPage() {
</div>
) : (
<table className="w-full">
<thead className="bg-gray-800/50">
<thead className="bg-white/[0.05]">
<tr>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Mod</th>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Size</th>
@ -178,9 +178,9 @@ export default function ModsPage() {
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
<tbody className="divide-y divide-white/[0.08]">
{mods.map(mod => (
<tr key={mod.filename} className="hover:bg-gray-800">
<tr key={mod.filename} className="hover:bg-white/[0.06]">
<td className="px-6 py-4">
<p className="text-sm font-medium text-gray-200">{mod.name}</p>
<p className="text-xs text-gray-500 font-mono">{mod.filename}</p>

View File

@ -266,10 +266,10 @@ export default function ServerDetailPage() {
<Link
key={link.href}
href={link.href}
className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-5 hover:border-cyan-500/50 hover:bg-gray-800/60 transition-all group"
className="glass rounded-xl p-5 hover:border-cyan-500/40 hover:bg-white/[0.1] transition-all duration-200 group hover:shadow-lg hover:shadow-cyan-500/10"
>
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-lg bg-gray-800 group-hover:bg-cyan-500/20 transition-colors">
<div className="p-2.5 rounded-lg bg-white/[0.08] group-hover:bg-cyan-400/20 transition-colors">
<Icon size={20} className="text-gray-400 group-hover:text-cyan-400 transition-colors" />
</div>
<span className="text-sm font-medium text-gray-200">{link.label}</span>
@ -284,9 +284,9 @@ export default function ServerDetailPage() {
function InfoCard({ label, value }: { label: string; value: string }) {
return (
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-4">
<div className="glass rounded-xl p-4">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">{label}</p>
<p className="text-lg font-semibold text-gray-100 mt-1 capitalize">{value}</p>
<p className="text-lg font-semibold text-white mt-1 capitalize">{value}</p>
</div>
)
}

View File

@ -137,14 +137,14 @@ export default function PlayersPage() {
{/* Online Players */}
{onlinePlayers.length > 0 && (
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-700/50">
<div className="glass rounded-xl overflow-hidden">
<div className="px-6 py-4 border-b border-white/[0.1]">
<h3 className="text-sm font-semibold text-gray-200 flex items-center gap-2">
<div className="w-2 h-2 bg-emerald-400 rounded-full" />
Online ({onlinePlayers.length})
</h3>
</div>
<div className="divide-y divide-gray-700">
<div className="divide-y divide-white/[0.08]">
{onlinePlayers.map(player => (
<PlayerRow
key={player.name}
@ -159,8 +159,8 @@ export default function PlayersPage() {
)}
{/* All Known Players */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-700/50">
<div className="glass rounded-xl overflow-hidden">
<div className="px-6 py-4 border-b border-white/[0.1]">
<h3 className="text-sm font-semibold text-gray-200">
All Players ({players.length})
</h3>
@ -172,7 +172,7 @@ export default function PlayersPage() {
<p className="text-sm mt-1">Add players to the whitelist or start the server to see players.</p>
</div>
) : (
<div className="divide-y divide-gray-700">
<div className="divide-y divide-white/[0.08]">
{[...onlinePlayers, ...offlinePlayers].map(player => (
<PlayerRow
key={player.name}
@ -200,10 +200,10 @@ export default function PlayersPage() {
<button
type="button"
onClick={() => setAddAction('whitelist')}
className={`flex-1 p-3 rounded-lg border text-sm font-medium transition-colors ${
className={`flex-1 p-3 rounded-lg border text-sm font-medium transition-all duration-200 ${
addAction === 'whitelist'
? 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400'
: 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700'
? 'bg-cyan-400/20 border-cyan-400/35 text-cyan-400 shadow-lg shadow-cyan-500/10'
: 'bg-white/[0.06] border-white/[0.12] text-gray-400 hover:bg-white/[0.1]'
}`}
>
Add to Whitelist
@ -211,10 +211,10 @@ export default function PlayersPage() {
<button
type="button"
onClick={() => setAddAction('op')}
className={`flex-1 p-3 rounded-lg border text-sm font-medium transition-colors ${
className={`flex-1 p-3 rounded-lg border text-sm font-medium transition-all duration-200 ${
addAction === 'op'
? 'bg-amber-500/20 border-amber-500/50 text-amber-400'
: 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700'
? 'bg-amber-400/20 border-amber-400/35 text-amber-400 shadow-lg shadow-amber-500/10'
: 'bg-white/[0.06] border-white/[0.12] text-gray-400 hover:bg-white/[0.1]'
}`}
>
Make Operator

View File

@ -161,7 +161,7 @@ export default function PluginsPage() {
}
/>
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
<div className="glass rounded-xl overflow-hidden">
{plugins.length === 0 ? (
<div className="p-8 text-center text-gray-500">
<Puzzle size={40} className="mx-auto mb-3 text-gray-600" />
@ -170,7 +170,7 @@ export default function PluginsPage() {
</div>
) : (
<table className="w-full">
<thead className="bg-gray-800/50">
<thead className="bg-white/[0.05]">
<tr>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Plugin</th>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Size</th>
@ -178,9 +178,9 @@ export default function PluginsPage() {
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
<tbody className="divide-y divide-white/[0.08]">
{plugins.map(plugin => (
<tr key={plugin.filename} className="hover:bg-gray-800">
<tr key={plugin.filename} className="hover:bg-white/[0.06]">
<td className="px-6 py-4">
<p className="text-sm font-medium text-gray-200">{plugin.name}</p>
<p className="text-xs text-gray-500 font-mono">{plugin.filename}</p>

View File

@ -6,8 +6,94 @@
body {
font-family: var(--font-sans);
background-color: #030712; /* gray-950 */
color: #f3f4f6; /* gray-100 */
background-color: #050510;
color: #f3f4f6;
}
/* ─── Glassmorphism Utilities ─────────────────────────────────── */
.glass {
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.glass-lg {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(40px) saturate(1.5);
-webkit-backdrop-filter: blur(40px) saturate(1.5);
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow:
0 12px 48px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.glass-surface {
background: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(16px) saturate(1.3);
-webkit-backdrop-filter: blur(16px) saturate(1.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.glass-input {
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 200ms ease;
}
.glass-input:focus {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(6, 182, 212, 0.6);
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.2),
0 0 0 3px rgba(6, 182, 212, 0.15),
0 0 24px rgba(6, 182, 212, 0.12);
}
.glass-input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
/* ─── Glass Shine (top border gradient) ───────────────────────── */
.glass-shine {
position: relative;
}
.glass-shine::before {
content: '';
position: absolute;
top: 0;
left: 5%;
right: 5%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.18), transparent);
border-radius: 1px;
pointer-events: none;
z-index: 1;
}
/* ─── Glow Effects ────────────────────────────────────────────── */
.glow-cyan {
box-shadow: 0 0 30px rgba(6, 182, 212, 0.25), 0 0 80px rgba(6, 182, 212, 0.1);
}
.glow-emerald {
box-shadow: 0 0 30px rgba(16, 185, 129, 0.25), 0 0 80px rgba(16, 185, 129, 0.1);
}
.glow-amber {
box-shadow: 0 0 30px rgba(245, 158, 11, 0.25), 0 0 80px rgba(245, 158, 11, 0.1);
}
.glow-red {
box-shadow: 0 0 30px rgba(239, 68, 68, 0.25), 0 0 80px rgba(239, 68, 68, 0.1);
}
/* ─── Animations ──────────────────────────────────────────────── */
@ -18,8 +104,8 @@ body {
}
@keyframes scale-in {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
from { opacity: 0; transform: scale(0.96) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes slide-in-right {
@ -32,40 +118,57 @@ body {
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-20px) scale(1.02); }
}
.animate-fade-in {
animation: fade-in 200ms ease-out;
}
.animate-scale-in {
animation: scale-in 200ms ease-out;
animation: scale-in 250ms cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-slide-in-right {
animation: slide-in-right 300ms ease-out;
animation: slide-in-right 300ms cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-slide-up {
animation: slide-up 300ms ease-out;
animation: slide-up 300ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* ─── Scrollbar (Dark Theme) ──────────────────────────────────── */
.animate-float {
animation: float 8s ease-in-out infinite;
}
.animate-float-delayed {
animation: float 10s ease-in-out 2s infinite;
}
.animate-float-slow {
animation: float 12s ease-in-out 4s infinite;
}
/* ─── Scrollbar (Glassmorphism) ───────────────────────────────── */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #111827; /* gray-900 */
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #374151; /* gray-700 */
border-radius: 4px;
background: rgba(255, 255, 255, 0.14);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #4b5563; /* gray-600 */
background: rgba(255, 255, 255, 0.25);
}
/* ─── Console Font ────────────────────────────────────────────── */
@ -73,3 +176,10 @@ body {
.font-mono {
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
}
/* ─── Selection Color ─────────────────────────────────────────── */
::selection {
background: rgba(6, 182, 212, 0.3);
color: white;
}

View File

@ -16,7 +16,14 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className="min-h-screen bg-gray-950 text-gray-100 antialiased">
<body className="min-h-screen bg-[#050510] text-gray-100 antialiased">
{/* Gradient mesh background — smaller/lighter on mobile for GPU perf */}
<div className="fixed inset-0 -z-10 overflow-hidden" aria-hidden="true">
<div className="absolute top-[-20%] left-[-10%] w-[80%] h-[50%] sm:w-[60%] sm:h-[60%] rounded-full bg-cyan-500/15 sm:bg-cyan-500/20 blur-[80px] sm:blur-[150px] animate-float" />
<div className="absolute top-[10%] right-[-15%] w-[60%] h-[40%] sm:w-[50%] sm:h-[50%] rounded-full bg-violet-500/10 sm:bg-violet-500/15 blur-[80px] sm:blur-[150px] animate-float-delayed" />
<div className="absolute bottom-[-10%] left-[20%] w-[60%] h-[30%] sm:w-[50%] sm:h-[40%] rounded-full bg-emerald-500/[0.08] sm:bg-emerald-500/[0.12] blur-[80px] sm:blur-[150px] animate-float-slow" />
</div>
<AuthProvider>
<ToastProvider>
<ConfirmationProvider>

View File

@ -55,22 +55,23 @@ export default function LoginPage() {
}
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="min-h-screen flex items-center justify-center p-4 relative">
<div className="w-full max-w-md relative z-10">
{/* Logo */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-cyan-500/20 rounded-2xl mb-4">
<Gamepad2 size={32} className="text-cyan-400" />
<div className="inline-flex items-center justify-center w-16 h-16 bg-cyan-400/15 rounded-2xl mb-4 relative">
<Gamepad2 size={32} className="text-cyan-400 relative z-10" />
<div className="absolute inset-0 rounded-2xl bg-cyan-400/20 blur-xl" />
</div>
<h1 className="text-2xl font-bold text-gray-100">MC-Manager</h1>
<h1 className="text-2xl font-bold text-white">MC-Manager</h1>
<p className="text-sm text-gray-500 mt-1">Minecraft Server Manager</p>
</div>
{/* Card */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 shadow-xl p-8">
<div className="glass-lg rounded-xl p-8 glass-shine">
{step === 'credentials' ? (
<form onSubmit={handleLogin} className="space-y-5">
<h2 className="text-lg font-semibold text-gray-100 text-center">Sign In</h2>
<h2 className="text-lg font-semibold text-white text-center">Sign In</h2>
{error && (
<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">
@ -103,13 +104,13 @@ export default function LoginPage() {
</form>
) : (
<form onSubmit={handleVerify} className="space-y-5">
<h2 className="text-lg font-semibold text-gray-100 text-center">Verification Code</h2>
<h2 className="text-lg font-semibold text-white text-center">Verification Code</h2>
<p className="text-sm text-gray-400 text-center">
A 6-digit code has been sent to your email.
</p>
{error && (
<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">
<div className="bg-red-500/10 border border-red-500/20 text-red-400 text-sm rounded-lg p-3">
{error}
</div>
)}

View File

@ -20,7 +20,7 @@ export default function HomePage() {
}, [user, loading, router])
return (
<div className="flex items-center justify-center min-h-screen bg-gray-950">
<div className="flex items-center justify-center min-h-screen">
<Spinner size="lg" />
</div>
)

View File

@ -84,7 +84,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
}
return (
<div className="flex flex-col h-full bg-gray-950 rounded-lg border border-gray-700/50 overflow-hidden">
<div className="flex flex-col h-full bg-[#050510]/80 backdrop-blur-xl rounded-xl border border-white/[0.08] overflow-hidden">
{/* Console Output */}
<div
ref={containerRef}
@ -94,7 +94,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
<p className="text-gray-600 italic">Waiting for server output...</p>
) : (
lines.map((line, i) => (
<div key={i} className="whitespace-pre-wrap break-all hover:bg-gray-900/50">
<div key={i} className="whitespace-pre-wrap break-all hover:bg-white/[0.04]">
{line}
</div>
))
@ -103,8 +103,8 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
{/* Command Input */}
{!readOnly && (
<div className="flex items-center gap-2 p-3 border-t border-gray-700/50 bg-gray-900/50">
<span className="text-cyan-500 font-mono text-sm">&gt;</span>
<div className="flex items-center gap-2 p-3 border-t border-white/[0.1] bg-white/[0.04]">
<span className="text-cyan-400 font-mono text-sm">&gt;</span>
<input
ref={inputRef}
type="text"
@ -112,11 +112,11 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
onChange={e => setCommand(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a command..."
className="flex-1 bg-transparent text-gray-100 font-mono text-sm placeholder-gray-600 focus:outline-none"
className="flex-1 bg-transparent text-gray-100 font-mono text-sm placeholder:text-white/25 focus:outline-none"
/>
<button
onClick={() => setLines([])}
className="p-1.5 rounded text-gray-500 hover:text-gray-300 hover:bg-gray-800 transition-colors"
className="p-1.5 rounded text-gray-500 hover:text-gray-300 hover:bg-white/[0.08] transition-colors"
aria-label="Clear console"
>
<Trash2 size={16} />
@ -124,7 +124,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
<button
onClick={handleSendCommand}
disabled={!command.trim() || sending}
className="p-1.5 rounded text-cyan-400 hover:text-cyan-300 hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="p-1.5 rounded text-cyan-400 hover:text-cyan-300 hover:bg-white/[0.08] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Send command"
>
<Send size={16} />

View File

@ -33,17 +33,17 @@ export default function Drawer({ isOpen, onClose, title, children }: DrawerProps
return (
<div className="fixed inset-0 z-40">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="absolute inset-0 bg-black/70 backdrop-blur-md" onClick={onClose} />
{/* Drawer Panel */}
<div className="absolute inset-y-0 right-0 w-full max-w-3xl animate-slide-in-right">
<div className="h-full flex flex-col bg-gray-900/95 backdrop-blur-md border-l border-gray-700/50 shadow-2xl">
<div className="h-full flex flex-col bg-[#0a0a1a]/90 backdrop-blur-2xl border-l border-white/[0.1] shadow-2xl shadow-black/40">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700/50 bg-gray-900/60 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.1]">
<h2 className="text-lg font-semibold text-white">{title}</h2>
<button
onClick={onClose}
className="p-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors"
className="p-2 rounded-lg text-gray-400 hover:bg-white/[0.08] hover:text-gray-200 transition-colors"
aria-label="Close drawer"
>
<X size={20} />

View File

@ -33,16 +33,16 @@ export default function Modal({ isOpen, onClose, title, children, maxWidth = 'ma
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div className="absolute inset-0 bg-black/70 backdrop-blur-md" onClick={onClose} />
{/* Modal Panel */}
<div className={`relative ${maxWidth} w-full mx-4 bg-gray-900 border border-gray-700 rounded-lg shadow-2xl animate-scale-in`}>
<div className={`relative ${maxWidth} w-full mx-4 glass-lg rounded-xl shadow-2xl shadow-black/40 animate-scale-in glass-shine`}>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700/50">
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.1]">
<h2 className="text-lg font-semibold text-white">{title}</h2>
<button
onClick={onClose}
className="p-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors"
className="p-2 rounded-lg text-gray-400 hover:bg-white/[0.08] hover:text-gray-200 transition-colors"
aria-label="Close modal"
>
<X size={20} />

View File

@ -9,12 +9,12 @@ interface PageHeaderProps {
export default function PageHeader({ title, description, icon: Icon, actions }: PageHeaderProps) {
return (
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg shadow-lg border border-gray-700/50 p-4 sm:p-6">
<div className="glass rounded-xl glass-shine p-4 sm:p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3 min-w-0">
{Icon && <Icon size={28} className="text-gray-400 flex-shrink-0" />}
{Icon && <Icon size={28} className="text-cyan-400/80 flex-shrink-0" />}
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-bold text-gray-100 truncate">{title}</h1>
<h1 className="text-xl sm:text-2xl font-bold text-white truncate">{title}</h1>
{description && <p className="text-sm text-gray-400 mt-1 truncate">{description}</p>}
</div>
</div>

View File

@ -8,28 +8,28 @@ interface ServerStatusBadgeProps {
const statusConfig: Record<ServerStatus, { label: string; className: string; dot: string }> = {
online: {
label: 'Online',
className: 'bg-emerald-500/20 text-emerald-400',
dot: 'bg-emerald-400',
className: 'bg-emerald-400/20 text-emerald-400 border border-emerald-400/30',
dot: 'bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.6)]',
},
offline: {
label: 'Offline',
className: 'bg-gray-500/20 text-gray-400',
className: 'bg-white/[0.1] text-gray-400 border border-white/[0.15]',
dot: 'bg-gray-400',
},
starting: {
label: 'Starting',
className: 'bg-amber-500/20 text-amber-400',
dot: 'bg-amber-400 animate-pulse',
className: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
dot: 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.6)]',
},
stopping: {
label: 'Stopping',
className: 'bg-amber-500/20 text-amber-400',
dot: 'bg-amber-400 animate-pulse',
className: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
dot: 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.6)]',
},
crashed: {
label: 'Crashed',
className: 'bg-red-500/20 text-red-400',
dot: 'bg-red-400',
className: 'bg-red-400/20 text-red-400 border border-red-400/30',
dot: 'bg-red-400 shadow-[0_0_8px_rgba(248,113,113,0.6)]',
},
}

View File

@ -63,16 +63,19 @@ export default function Sidebar() {
const sidebarContent = (
<>
{/* Logo */}
<div className="flex items-center gap-3 px-4 h-16 border-b border-gray-700/50">
<Gamepad2 size={28} className="text-cyan-500 flex-shrink-0" />
<div className="flex items-center gap-3 px-4 h-16 border-b border-white/[0.1]">
<div className="relative">
<Gamepad2 size={28} className="text-cyan-400 flex-shrink-0" />
<div className="absolute inset-0 blur-lg bg-cyan-400/40 rounded-full" />
</div>
{(!collapsed || mobileOpen) && (
<span className="text-lg font-bold text-gray-100 truncate">MC-Manager</span>
<span className="text-lg font-bold text-white truncate">MC-Manager</span>
)}
{/* Mobile close button */}
{mobileOpen && (
<button
onClick={() => setMobileOpen(false)}
className="ml-auto p-1.5 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors lg:hidden"
className="ml-auto p-1.5 rounded-lg text-gray-400 hover:bg-white/[0.08] hover:text-gray-200 transition-colors lg:hidden"
aria-label="Close menu"
>
<X size={20} />
@ -90,10 +93,10 @@ export default function Sidebar() {
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors text-sm font-medium ${
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 text-sm font-medium ${
isActive
? 'bg-cyan-500/20 text-cyan-400'
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200'
? 'bg-cyan-500/20 text-cyan-400 shadow-[0_0_24px_rgba(6,182,212,0.15)] border border-cyan-500/25'
: 'text-gray-400 hover:bg-white/[0.08] hover:text-gray-200'
}`}
>
<Icon size={20} className="flex-shrink-0" />
@ -104,7 +107,7 @@ export default function Sidebar() {
</nav>
{/* Footer */}
<div className="border-t border-gray-700/50 p-2 space-y-1">
<div className="border-t border-white/[0.1] p-2 space-y-1">
{/* User Info */}
{user && (!collapsed || mobileOpen) && (
<div className="px-3 py-2">
@ -116,7 +119,7 @@ export default function Sidebar() {
{/* Logout */}
<button
onClick={logout}
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-400 hover:bg-red-500/10 hover:text-red-400 transition-colors text-sm font-medium w-full"
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-400 hover:bg-red-500/10 hover:text-red-400 transition-all duration-200 text-sm font-medium w-full"
>
<LogOut size={20} className="flex-shrink-0" />
{(!collapsed || mobileOpen) && <span>Logout</span>}
@ -125,7 +128,7 @@ export default function Sidebar() {
{/* Collapse Toggle — desktop only */}
<button
onClick={() => setCollapsed(prev => !prev)}
className="hidden lg:flex items-center justify-center gap-3 px-3 py-2.5 rounded-lg text-gray-500 hover:bg-gray-800 hover:text-gray-300 transition-colors w-full"
className="hidden lg:flex items-center justify-center gap-3 px-3 py-2.5 rounded-lg text-gray-500 hover:bg-white/[0.06] hover:text-gray-300 transition-all duration-200 w-full"
>
{collapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
{!collapsed && <span className="text-sm">Collapse</span>}
@ -137,10 +140,10 @@ export default function Sidebar() {
return (
<>
{/* Mobile top bar */}
<div className="lg:hidden fixed top-0 left-0 right-0 z-30 flex items-center gap-3 px-4 h-14 bg-gray-900 border-b border-gray-700/50">
<div className="lg:hidden fixed top-0 left-0 right-0 z-30 flex items-center gap-3 px-4 h-14 glass border-b border-white/[0.1]">
<button
onClick={() => setMobileOpen(true)}
className="p-2 -ml-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors"
className="p-2 -ml-2 rounded-lg text-gray-400 hover:bg-white/[0.08] hover:text-gray-200 transition-colors"
aria-label="Open menu"
>
<Menu size={22} />
@ -159,7 +162,7 @@ export default function Sidebar() {
{/* Mobile sidebar drawer */}
<aside
className={`fixed inset-y-0 left-0 z-50 flex flex-col bg-gray-900 border-r border-gray-700/50 w-72
className={`fixed inset-y-0 left-0 z-50 flex flex-col bg-[#0a0a1a]/90 backdrop-blur-xl border-r border-white/[0.1] w-72
transform transition-transform duration-300 ease-in-out lg:hidden ${
mobileOpen ? 'translate-x-0' : '-translate-x-full'
}`}
@ -169,7 +172,7 @@ export default function Sidebar() {
{/* Desktop sidebar */}
<aside
className={`hidden lg:flex flex-col bg-gray-900 border-r border-gray-700/50 transition-all duration-300 ${
className={`hidden lg:flex flex-col bg-[#0a0a1a]/70 backdrop-blur-2xl border-r border-white/[0.1] transition-all duration-300 ${
collapsed ? 'w-16' : 'w-64'
}`}
>

View File

@ -66,7 +66,7 @@ export default function StatsChart({
}, [data, maxDataPoints, height, maxY])
return (
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-4 overflow-hidden">
<div className="glass rounded-xl p-4 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">

View File

@ -132,7 +132,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
/>
{/* Search & Filters Bar */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-4">
<div className="glass rounded-xl p-4">
<div className="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-3 sm:gap-4">
{/* Search */}
<div className="relative flex-1 min-w-0 sm:min-w-[200px]">
@ -142,8 +142,8 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
value={search}
onChange={e => setSearch(e.target.value)}
placeholder={searchPlaceholder}
className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent text-sm"
className="w-full pl-10 pr-4 py-2 glass-input text-gray-100 rounded-lg
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent text-sm"
/>
</div>
@ -153,8 +153,8 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
key={filter.filterKey}
value={activeFilters[filter.filterKey] || ''}
onChange={e => setActiveFilters(prev => ({ ...prev, [filter.filterKey]: e.target.value }))}
className="px-3 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg text-sm
focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
className="px-3 py-2 glass-input text-gray-100 rounded-lg text-sm
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent"
>
<option value="">{filter.label}</option>
{filter.options.map(opt => (
@ -168,7 +168,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
</div>
{/* Table */}
<div className="bg-gray-900 rounded-lg border border-gray-700/50 overflow-hidden">
<div className="glass rounded-xl overflow-hidden">
{loading ? (
<div className="flex items-center justify-center py-16">
<Spinner size="lg" />
@ -176,7 +176,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
) : filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
{EmptyIcon && (
<div className="bg-gray-800 rounded-full p-4 mb-4">
<div className="bg-white/[0.08] rounded-full p-4 mb-4">
<EmptyIcon size={32} className="text-gray-500" />
</div>
)}
@ -190,8 +190,8 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-700">
<thead className="bg-gray-800/50">
<table className="min-w-full divide-y divide-white/[0.1]">
<thead className="bg-white/[0.05]">
<tr>
{columns.map(col => (
<th
@ -211,12 +211,12 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
<tbody className="divide-y divide-white/[0.08]">
{filteredItems.map(item => (
<tr
key={getRowKey(item)}
onClick={() => onRowClick?.(item)}
className={`hover:bg-gray-800 transition-colors ${
className={`hover:bg-white/[0.06] transition-colors ${
onRowClick ? 'cursor-pointer' : ''
}`}
>

View File

@ -5,11 +5,11 @@ interface BadgeProps {
}
const badgeVariants = {
info: 'bg-cyan-500/20 text-cyan-400',
success: 'bg-emerald-500/20 text-emerald-400',
warning: 'bg-amber-500/20 text-amber-400',
error: 'bg-red-500/20 text-red-400',
neutral: 'bg-gray-500/20 text-gray-400',
info: 'bg-cyan-400/20 text-cyan-400 border border-cyan-400/30',
success: 'bg-emerald-400/20 text-emerald-400 border border-emerald-400/30',
warning: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
error: 'bg-red-400/20 text-red-400 border border-red-400/30',
neutral: 'bg-white/[0.1] text-gray-400 border border-white/[0.12]',
}
export default function Badge({ variant = 'neutral', children, className = '' }: BadgeProps) {

View File

@ -10,10 +10,10 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
}
const variants = {
primary: 'bg-cyan-500 hover:bg-cyan-600 text-white shadow-sm hover:shadow-md',
secondary: 'bg-gray-700 hover:bg-gray-600 text-gray-200 border border-gray-600',
danger: 'bg-red-500 hover:bg-red-600 text-white',
ghost: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200',
primary: 'bg-cyan-500 hover:bg-cyan-400 text-white shadow-lg shadow-cyan-500/25 hover:shadow-cyan-400/35',
secondary: 'bg-white/[0.08] hover:bg-white/[0.14] text-gray-200 border border-white/[0.12] hover:border-white/[0.2]',
danger: 'bg-red-500 hover:bg-red-400 text-white shadow-lg shadow-red-500/25',
ghost: 'text-gray-400 hover:bg-white/[0.08] hover:text-gray-200',
}
const sizes = {
@ -36,7 +36,7 @@ export default function Button({
return (
<Component
className={`inline-flex items-center justify-center gap-2 font-medium rounded-lg
transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 focus:ring-offset-gray-900
transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-cyan-400/40 focus:ring-offset-2 focus:ring-offset-[#050510]
disabled:opacity-50 disabled:cursor-not-allowed
${variants[variant]} ${sizes[size]} ${className}`}
disabled={disabled || loading}

View File

@ -18,9 +18,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
)}
<input
ref={ref}
className={`w-full px-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
focus:bg-white/[0.06] focus:shadow-[0_0_20px_rgba(6,182,212,0.08)]
disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 ${
error ? 'border-red-500 focus:ring-red-500' : ''
} ${className}`}
{...props}

View File

@ -20,9 +20,10 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>(
)}
<select
ref={ref}
className={`w-full px-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
focus:ring-2 focus:ring-cyan-500 focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
focus:bg-white/[0.06] focus:shadow-[0_0_20px_rgba(6,182,212,0.08)]
disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 ${
error ? 'border-red-500 focus:ring-red-500' : ''
} ${className}`}
{...props}

View File

@ -12,7 +12,9 @@ const sizes = {
export default function Spinner({ size = 'md', className = '' }: SpinnerProps) {
return (
<div className={`flex items-center justify-center ${className}`}>
<div className={`animate-spin rounded-full border-b-2 border-cyan-500 ${sizes[size]}`} />
<div className={`animate-spin rounded-full border-2 border-white/[0.08] border-t-cyan-400 ${sizes[size]}`}
style={{ filter: 'drop-shadow(0 0 8px rgba(6, 182, 212, 0.3))' }}
/>
</div>
)
}

View File

@ -20,16 +20,16 @@ const ConfirmationContext = createContext<ConfirmationContextType | undefined>(u
const typeStyles = {
danger: {
icon: 'bg-red-500/20 text-red-400',
button: 'bg-red-500 hover:bg-red-600 text-white',
icon: 'bg-red-400/15 text-red-400 border border-red-400/20',
button: 'bg-red-500/90 hover:bg-red-400/90 text-white shadow-lg shadow-red-500/20 hover:shadow-red-400/30',
},
warning: {
icon: 'bg-amber-500/20 text-amber-400',
button: 'bg-amber-500 hover:bg-amber-600 text-white',
icon: 'bg-amber-400/15 text-amber-400 border border-amber-400/20',
button: 'bg-amber-500/90 hover:bg-amber-400/90 text-white shadow-lg shadow-amber-500/20 hover:shadow-amber-400/30',
},
info: {
icon: 'bg-cyan-500/20 text-cyan-400',
button: 'bg-cyan-500 hover:bg-cyan-600 text-white',
icon: 'bg-cyan-400/15 text-cyan-400 border border-cyan-400/20',
button: 'bg-cyan-500/90 hover:bg-cyan-400/90 text-white shadow-lg shadow-cyan-500/20 hover:shadow-cyan-400/30',
},
}
@ -70,17 +70,17 @@ export function ConfirmationProvider({ children }: { children: ReactNode }) {
{isOpen && options && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={handleCancel} />
<div className="absolute inset-0 bg-black/70 backdrop-blur-md" onClick={handleCancel} />
{/* Dialog */}
<div className="relative bg-gray-900 border border-gray-700 rounded-lg shadow-2xl p-6 max-w-md w-full mx-4 animate-scale-in">
<div className="relative glass-lg rounded-xl shadow-2xl shadow-black/40 p-6 max-w-md w-full mx-4 animate-scale-in glass-shine">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full ${styles.icon}`}>
<AlertTriangle size={24} />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-100">{options.title}</h3>
<h3 className="text-lg font-semibold text-white">{options.title}</h3>
<p className="mt-2 text-sm text-gray-400">
{options.message}
{options.itemName && (
@ -94,7 +94,7 @@ export function ConfirmationProvider({ children }: { children: ReactNode }) {
<div className="flex justify-end gap-3 mt-6">
<button
onClick={handleCancel}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-200 rounded-lg transition-colors text-sm font-medium"
className="px-4 py-2 bg-white/[0.08] hover:bg-white/[0.14] border border-white/[0.12] hover:border-white/[0.2] text-gray-200 rounded-lg transition-all duration-200 text-sm font-medium"
>
{options.cancelText || 'Cancel'}
</button>

View File

@ -20,10 +20,10 @@ const ToastContext = createContext<ToastContextType | undefined>(undefined)
const TOAST_DURATION = 5000
const toastStyles: Record<ToastType, string> = {
success: 'bg-emerald-500/20 border-emerald-500/50 text-emerald-400',
error: 'bg-red-500/20 border-red-500/50 text-red-400',
info: 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400',
warning: 'bg-amber-500/20 border-amber-500/50 text-amber-400',
success: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400',
error: 'bg-red-500/10 border-red-500/30 text-red-400',
info: 'bg-cyan-500/10 border-cyan-500/30 text-cyan-400',
warning: 'bg-amber-500/10 border-amber-500/30 text-amber-400',
}
const toastIcons: Record<ToastType, typeof CheckCircle> = {
@ -58,7 +58,8 @@ export function ToastProvider({ children }: { children: ReactNode }) {
return (
<div
key={toast.id}
className={`flex items-center gap-3 px-4 py-3 rounded-lg border backdrop-blur-lg shadow-xl animate-slide-in-right ${toastStyles[toast.type]}`}
className={`flex items-center gap-3 px-4 py-3 rounded-xl border shadow-2xl shadow-black/30 animate-slide-in-right ${toastStyles[toast.type]}`}
style={{ backdropFilter: 'blur(24px) saturate(1.3)', WebkitBackdropFilter: 'blur(24px) saturate(1.3)' }}
>
<Icon size={18} className="flex-shrink-0" />
<span className="text-sm font-medium">{toast.message}</span>