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} value={search}
onChange={e => { setSearch(e.target.value); setPage(1) }} onChange={e => { setSearch(e.target.value); setPage(1) }}
placeholder="Search audit logs..." 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>
</div> </div>
@ -81,7 +81,7 @@ export default function AuditPage() {
</div> </div>
{/* Table */} {/* 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 ? ( {loading ? (
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-12">
<Spinner size="lg" /> <Spinner size="lg" />
@ -95,7 +95,7 @@ export default function AuditPage() {
<> <>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/50"> <thead className="bg-white/[0.05]">
<tr> <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">Timestamp</th>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">User</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> <th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">IP</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-700"> <tbody className="divide-y divide-white/[0.08]">
{logs.map(log => ( {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"> <td className="px-6 py-3 text-xs text-gray-400 whitespace-nowrap">
{formatDateTime(log.createdAt)} {formatDateTime(log.createdAt)}
</td> </td>
@ -139,7 +139,7 @@ export default function AuditPage() {
{/* Pagination */} {/* Pagination */}
{totalPages > 1 && ( {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"> <p className="text-sm text-gray-500">
Page {page} of {totalPages} Page {page} of {totalPages}
</p> </p>
@ -147,14 +147,14 @@ export default function AuditPage() {
<button <button
onClick={() => setPage(p => Math.max(1, p - 1))} onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 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 Previous
</button> </button>
<button <button
onClick={() => setPage(p => Math.min(totalPages, p + 1))} onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages} 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 Next
</button> </button>

View File

@ -93,9 +93,9 @@ export default function DashboardPage() {
</div> </div>
{/* Server List */} {/* Server 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">
<div className="px-6 py-4 border-b border-gray-700/50"> <div className="px-6 py-4 border-b border-white/[0.1]">
<h2 className="text-lg font-semibold text-gray-100">Servers</h2> <h2 className="text-lg font-semibold text-white">Servers</h2>
</div> </div>
{servers.length === 0 ? ( {servers.length === 0 ? (
@ -103,12 +103,12 @@ export default function DashboardPage() {
No servers yet. Create your first Minecraft server! No servers yet. Create your first Minecraft server!
</div> </div>
) : ( ) : (
<div className="divide-y divide-gray-700"> <div className="divide-y divide-white/[0.08]">
{servers.map(server => ( {servers.map(server => (
<a <a
key={server._id} key={server._id}
href={`/servers/${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"> <div className="flex items-center gap-4">
<Server size={20} className="text-gray-500" /> <Server size={20} className="text-gray-500" />
@ -140,21 +140,21 @@ function StatCard({
value: number value: number
color: string color: string
}) { }) {
const colorMap: Record<string, string> = { const colorMap: Record<string, { badge: string; glow: string }> = {
cyan: 'bg-cyan-500/20 text-cyan-400', cyan: { badge: 'bg-cyan-400/20 text-cyan-400', glow: 'shadow-cyan-500/15' },
emerald: 'bg-emerald-500/20 text-emerald-400', emerald: { badge: 'bg-emerald-400/20 text-emerald-400', glow: 'shadow-emerald-500/15' },
amber: 'bg-amber-500/20 text-amber-400', amber: { badge: 'bg-amber-400/20 text-amber-400', glow: 'shadow-amber-500/15' },
gray: 'bg-gray-500/20 text-gray-400', gray: { badge: 'bg-white/[0.1] text-gray-400', glow: '' },
} }
return ( 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="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} /> <Icon size={20} />
</div> </div>
<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> <p className="text-xs text-gray-500">{label}</p>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
if (loading) { if (loading) {
return ( 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" /> <Spinner size="lg" />
</div> </div>
) )
@ -18,9 +18,9 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
if (!user) return null // Middleware will redirect if (!user) return null // Middleware will redirect
return ( return (
<div className="flex h-screen bg-gray-950"> <div className="flex h-screen">
<Sidebar /> <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} {children}
</main> </main>
</div> </div>

View File

@ -233,7 +233,7 @@ export default function RolesPage() {
const PermissionGrid = () => ( const PermissionGrid = () => (
<div className="space-y-3 max-h-[400px] overflow-y-auto"> <div className="space-y-3 max-h-[400px] overflow-y-auto">
{allResources.map(resource => ( {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> <p className="text-xs font-semibold text-gray-300 uppercase tracking-wider mb-2">{resource}</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{allActions[resource].map(action => ( {allActions[resource].map(action => (
@ -241,10 +241,10 @@ export default function RolesPage() {
key={`${resource}:${action}`} key={`${resource}:${action}`}
type="button" type="button"
onClick={() => togglePermission(resource, action)} 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) hasPermissionToggle(resource, action)
? 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/50' ? 'bg-cyan-400/20 text-cyan-400 border border-cyan-400/35 shadow-[0_0_10px_rgba(6,182,212,0.2)]'
: 'bg-gray-700 text-gray-400 border border-gray-600 hover:bg-gray-600' : 'bg-white/[0.08] text-gray-400 border border-white/[0.12] hover:bg-white/[0.14] hover:text-gray-300'
}`} }`}
> >
{action} {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 ? ( {backups.length === 0 ? (
<div className="p-8 text-center text-gray-500"> <div className="p-8 text-center text-gray-500">
<HardDrive size={40} className="mx-auto mb-3 text-gray-600" /> <HardDrive size={40} className="mx-auto mb-3 text-gray-600" />
@ -161,7 +161,7 @@ export default function BackupsPage() {
</div> </div>
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/50"> <thead className="bg-white/[0.05]">
<tr> <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">Filename</th>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Type</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> <th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-700"> <tbody className="divide-y divide-white/[0.08]">
{backups.map(backup => ( {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 text-sm text-gray-200 font-mono">{backup.filename}</td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<Badge variant={backup.type === 'manual' ? 'info' : 'neutral'}> <Badge variant={backup.type === 'manual' ? 'info' : 'neutral'}>

View File

@ -119,7 +119,7 @@ export default function ConfigurationPage() {
/> />
{/* JVM Args */} {/* 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> <h3 className="text-sm font-semibold text-gray-200 mb-3">JVM Arguments</h3>
<Input <Input
value={jvmArgs} value={jvmArgs}
@ -132,11 +132,11 @@ export default function ConfigurationPage() {
</div> </div>
{/* Server Properties */} {/* Server Properties */}
<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">
<div className="px-6 py-4 border-b border-gray-700/50"> <div className="px-6 py-4 border-b border-white/[0.1]">
<h3 className="text-sm font-semibold text-gray-200">server.properties</h3> <h3 className="text-sm font-semibold text-gray-200">server.properties</h3>
</div> </div>
<div className="divide-y divide-gray-700"> <div className="divide-y divide-white/[0.08]">
{sortedKeys.map(key => ( {sortedKeys.map(key => (
<div key={key} className="flex items-center gap-4 px-6 py-3"> <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}> <label className="w-1/3 text-sm text-gray-400 font-mono truncate" title={key}>
@ -145,7 +145,7 @@ export default function ConfigurationPage() {
<input <input
value={properties[key]} value={properties[key]}
onChange={e => handlePropertyChange(key, e.target.value)} 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> </div>
))} ))}

View File

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

View File

@ -129,8 +129,8 @@ export default function LogsPage() {
{selectedFile ? ( {selectedFile ? (
/* Log File Viewer */ /* Log File Viewer */
<div className="bg-gray-950 rounded-lg border border-gray-700/50 overflow-hidden"> <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-gray-700/50 bg-gray-900/50"> <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" /> <FileText size={16} className="text-cyan-400" />
<span className="text-sm font-medium text-gray-200">{selectedFile}</span> <span className="text-sm font-medium text-gray-200">{selectedFile}</span>
</div> </div>
@ -148,7 +148,7 @@ export default function LogsPage() {
</div> </div>
) : ( ) : (
/* File List */ /* 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 ? ( {files.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<FileText size={40} className="mx-auto text-gray-600 mb-3" /> <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> <p className="text-gray-600 text-sm mt-1">Logs will appear after the server has started</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-gray-700/50"> <div className="divide-y divide-white/[0.08]">
{files.map((file) => ( {files.map((file) => (
<button <button
key={file.name} key={file.name}
onClick={() => openFile(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" /> <FileText size={18} className="text-gray-500 group-hover:text-cyan-400 transition-colors flex-shrink-0" />
<div className="flex-1 min-w-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 ? ( {mods.length === 0 ? (
<div className="p-8 text-center text-gray-500"> <div className="p-8 text-center text-gray-500">
<Package size={40} className="mx-auto mb-3 text-gray-600" /> <Package size={40} className="mx-auto mb-3 text-gray-600" />
@ -170,7 +170,7 @@ export default function ModsPage() {
</div> </div>
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/50"> <thead className="bg-white/[0.05]">
<tr> <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">Mod</th>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Size</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> <th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-700"> <tbody className="divide-y divide-white/[0.08]">
{mods.map(mod => ( {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"> <td className="px-6 py-4">
<p className="text-sm font-medium text-gray-200">{mod.name}</p> <p className="text-sm font-medium text-gray-200">{mod.name}</p>
<p className="text-xs text-gray-500 font-mono">{mod.filename}</p> <p className="text-xs text-gray-500 font-mono">{mod.filename}</p>

View File

@ -266,10 +266,10 @@ export default function ServerDetailPage() {
<Link <Link
key={link.href} key={link.href}
href={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="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" /> <Icon size={20} className="text-gray-400 group-hover:text-cyan-400 transition-colors" />
</div> </div>
<span className="text-sm font-medium text-gray-200">{link.label}</span> <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 }) { function InfoCard({ label, value }: { label: string; value: string }) {
return ( 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-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> </div>
) )
} }

View File

@ -137,14 +137,14 @@ export default function PlayersPage() {
{/* Online Players */} {/* Online Players */}
{onlinePlayers.length > 0 && ( {onlinePlayers.length > 0 && (
<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">
<div className="px-6 py-4 border-b border-gray-700/50"> <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"> <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" /> <div className="w-2 h-2 bg-emerald-400 rounded-full" />
Online ({onlinePlayers.length}) Online ({onlinePlayers.length})
</h3> </h3>
</div> </div>
<div className="divide-y divide-gray-700"> <div className="divide-y divide-white/[0.08]">
{onlinePlayers.map(player => ( {onlinePlayers.map(player => (
<PlayerRow <PlayerRow
key={player.name} key={player.name}
@ -159,8 +159,8 @@ export default function PlayersPage() {
)} )}
{/* All Known Players */} {/* All Known Players */}
<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">
<div className="px-6 py-4 border-b border-gray-700/50"> <div className="px-6 py-4 border-b border-white/[0.1]">
<h3 className="text-sm font-semibold text-gray-200"> <h3 className="text-sm font-semibold text-gray-200">
All Players ({players.length}) All Players ({players.length})
</h3> </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> <p className="text-sm mt-1">Add players to the whitelist or start the server to see players.</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-gray-700"> <div className="divide-y divide-white/[0.08]">
{[...onlinePlayers, ...offlinePlayers].map(player => ( {[...onlinePlayers, ...offlinePlayers].map(player => (
<PlayerRow <PlayerRow
key={player.name} key={player.name}
@ -200,10 +200,10 @@ export default function PlayersPage() {
<button <button
type="button" type="button"
onClick={() => setAddAction('whitelist')} 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' addAction === 'whitelist'
? 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400' ? 'bg-cyan-400/20 border-cyan-400/35 text-cyan-400 shadow-lg shadow-cyan-500/10'
: 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700' : 'bg-white/[0.06] border-white/[0.12] text-gray-400 hover:bg-white/[0.1]'
}`} }`}
> >
Add to Whitelist Add to Whitelist
@ -211,10 +211,10 @@ export default function PlayersPage() {
<button <button
type="button" type="button"
onClick={() => setAddAction('op')} 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' addAction === 'op'
? 'bg-amber-500/20 border-amber-500/50 text-amber-400' ? 'bg-amber-400/20 border-amber-400/35 text-amber-400 shadow-lg shadow-amber-500/10'
: 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700' : 'bg-white/[0.06] border-white/[0.12] text-gray-400 hover:bg-white/[0.1]'
}`} }`}
> >
Make Operator 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 ? ( {plugins.length === 0 ? (
<div className="p-8 text-center text-gray-500"> <div className="p-8 text-center text-gray-500">
<Puzzle size={40} className="mx-auto mb-3 text-gray-600" /> <Puzzle size={40} className="mx-auto mb-3 text-gray-600" />
@ -170,7 +170,7 @@ export default function PluginsPage() {
</div> </div>
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/50"> <thead className="bg-white/[0.05]">
<tr> <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">Plugin</th>
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Size</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> <th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-700"> <tbody className="divide-y divide-white/[0.08]">
{plugins.map(plugin => ( {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"> <td className="px-6 py-4">
<p className="text-sm font-medium text-gray-200">{plugin.name}</p> <p className="text-sm font-medium text-gray-200">{plugin.name}</p>
<p className="text-xs text-gray-500 font-mono">{plugin.filename}</p> <p className="text-xs text-gray-500 font-mono">{plugin.filename}</p>

View File

@ -6,8 +6,94 @@
body { body {
font-family: var(--font-sans); font-family: var(--font-sans);
background-color: #030712; /* gray-950 */ background-color: #050510;
color: #f3f4f6; /* gray-100 */ 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 ──────────────────────────────────────────────── */ /* ─── Animations ──────────────────────────────────────────────── */
@ -18,8 +104,8 @@ body {
} }
@keyframes scale-in { @keyframes scale-in {
from { opacity: 0; transform: scale(0.95); } from { opacity: 0; transform: scale(0.96) translateY(8px); }
to { opacity: 1; transform: scale(1); } to { opacity: 1; transform: scale(1) translateY(0); }
} }
@keyframes slide-in-right { @keyframes slide-in-right {
@ -32,40 +118,57 @@ body {
to { opacity: 1; transform: translateY(0); } 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 { .animate-fade-in {
animation: fade-in 200ms ease-out; animation: fade-in 200ms ease-out;
} }
.animate-scale-in { .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 { .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 { .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 { ::-webkit-scrollbar {
width: 8px; width: 6px;
height: 8px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #111827; /* gray-900 */ background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #374151; /* gray-700 */ background: rgba(255, 255, 255, 0.14);
border-radius: 4px; border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #4b5563; /* gray-600 */ background: rgba(255, 255, 255, 0.25);
} }
/* ─── Console Font ────────────────────────────────────────────── */ /* ─── Console Font ────────────────────────────────────────────── */
@ -73,3 +176,10 @@ body {
.font-mono { .font-mono {
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace; 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 ( return (
<html lang="en"> <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> <AuthProvider>
<ToastProvider> <ToastProvider>
<ConfirmationProvider> <ConfirmationProvider>

View File

@ -55,22 +55,23 @@ export default function LoginPage() {
} }
return ( return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4"> <div className="min-h-screen flex items-center justify-center p-4 relative">
<div className="w-full max-w-md"> <div className="w-full max-w-md relative z-10">
{/* Logo */} {/* Logo */}
<div className="text-center mb-8"> <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"> <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" /> <Gamepad2 size={32} className="text-cyan-400 relative z-10" />
<div className="absolute inset-0 rounded-2xl bg-cyan-400/20 blur-xl" />
</div> </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> <p className="text-sm text-gray-500 mt-1">Minecraft Server Manager</p>
</div> </div>
{/* Card */} {/* 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' ? ( {step === 'credentials' ? (
<form onSubmit={handleLogin} className="space-y-5"> <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 && ( {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/30 text-red-400 text-sm rounded-lg p-3">
@ -103,13 +104,13 @@ export default function LoginPage() {
</form> </form>
) : ( ) : (
<form onSubmit={handleVerify} className="space-y-5"> <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"> <p className="text-sm text-gray-400 text-center">
A 6-digit code has been sent to your email. A 6-digit code has been sent to your email.
</p> </p>
{error && ( {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} {error}
</div> </div>
)} )}

View File

@ -20,7 +20,7 @@ export default function HomePage() {
}, [user, loading, router]) }, [user, loading, router])
return ( 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" /> <Spinner size="lg" />
</div> </div>
) )

View File

@ -84,7 +84,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
} }
return ( 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 */} {/* Console Output */}
<div <div
ref={containerRef} 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> <p className="text-gray-600 italic">Waiting for server output...</p>
) : ( ) : (
lines.map((line, i) => ( 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} {line}
</div> </div>
)) ))
@ -103,8 +103,8 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
{/* Command Input */} {/* Command Input */}
{!readOnly && ( {!readOnly && (
<div className="flex items-center gap-2 p-3 border-t border-gray-700/50 bg-gray-900/50"> <div className="flex items-center gap-2 p-3 border-t border-white/[0.1] bg-white/[0.04]">
<span className="text-cyan-500 font-mono text-sm">&gt;</span> <span className="text-cyan-400 font-mono text-sm">&gt;</span>
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
@ -112,11 +112,11 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
onChange={e => setCommand(e.target.value)} onChange={e => setCommand(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Type a command..." 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 <button
onClick={() => setLines([])} 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" aria-label="Clear console"
> >
<Trash2 size={16} /> <Trash2 size={16} />
@ -124,7 +124,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
<button <button
onClick={handleSendCommand} onClick={handleSendCommand}
disabled={!command.trim() || sending} 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" aria-label="Send command"
> >
<Send size={16} /> <Send size={16} />

View File

@ -33,17 +33,17 @@ export default function Drawer({ isOpen, onClose, title, children }: DrawerProps
return ( return (
<div className="fixed inset-0 z-40"> <div className="fixed inset-0 z-40">
{/* Backdrop */} {/* 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 */} {/* Drawer Panel */}
<div className="absolute inset-y-0 right-0 w-full max-w-3xl animate-slide-in-right"> <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 */} {/* 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"> <div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.1]">
<h2 className="text-lg font-semibold text-gray-100">{title}</h2> <h2 className="text-lg font-semibold text-white">{title}</h2>
<button <button
onClick={onClose} 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" aria-label="Close drawer"
> >
<X size={20} /> <X size={20} />

View File

@ -33,16 +33,16 @@ export default function Modal({ isOpen, onClose, title, children, maxWidth = 'ma
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */} {/* 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 */} {/* 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 */} {/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700/50"> <div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.1]">
<h2 className="text-lg font-semibold text-gray-100">{title}</h2> <h2 className="text-lg font-semibold text-white">{title}</h2>
<button <button
onClick={onClose} 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" aria-label="Close modal"
> >
<X size={20} /> <X size={20} />

View File

@ -9,12 +9,12 @@ interface PageHeaderProps {
export default function PageHeader({ title, description, icon: Icon, actions }: PageHeaderProps) { export default function PageHeader({ title, description, icon: Icon, actions }: PageHeaderProps) {
return ( 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 flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3 min-w-0"> <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"> <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>} {description && <p className="text-sm text-gray-400 mt-1 truncate">{description}</p>}
</div> </div>
</div> </div>

View File

@ -8,28 +8,28 @@ interface ServerStatusBadgeProps {
const statusConfig: Record<ServerStatus, { label: string; className: string; dot: string }> = { const statusConfig: Record<ServerStatus, { label: string; className: string; dot: string }> = {
online: { online: {
label: 'Online', label: 'Online',
className: 'bg-emerald-500/20 text-emerald-400', className: 'bg-emerald-400/20 text-emerald-400 border border-emerald-400/30',
dot: 'bg-emerald-400', dot: 'bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.6)]',
}, },
offline: { offline: {
label: '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', dot: 'bg-gray-400',
}, },
starting: { starting: {
label: 'Starting', label: 'Starting',
className: 'bg-amber-500/20 text-amber-400', className: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
dot: 'bg-amber-400 animate-pulse', dot: 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.6)]',
}, },
stopping: { stopping: {
label: 'Stopping', label: 'Stopping',
className: 'bg-amber-500/20 text-amber-400', className: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
dot: 'bg-amber-400 animate-pulse', dot: 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.6)]',
}, },
crashed: { crashed: {
label: 'Crashed', label: 'Crashed',
className: 'bg-red-500/20 text-red-400', className: 'bg-red-400/20 text-red-400 border border-red-400/30',
dot: 'bg-red-400', 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 = ( const sidebarContent = (
<> <>
{/* Logo */} {/* Logo */}
<div className="flex items-center gap-3 px-4 h-16 border-b border-gray-700/50"> <div className="flex items-center gap-3 px-4 h-16 border-b border-white/[0.1]">
<Gamepad2 size={28} className="text-cyan-500 flex-shrink-0" /> <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) && ( {(!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 */} {/* Mobile close button */}
{mobileOpen && ( {mobileOpen && (
<button <button
onClick={() => setMobileOpen(false)} 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" aria-label="Close menu"
> >
<X size={20} /> <X size={20} />
@ -90,10 +93,10 @@ export default function Sidebar() {
<Link <Link
key={item.href} key={item.href}
href={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 isActive
? 'bg-cyan-500/20 text-cyan-400' ? '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-gray-800 hover:text-gray-200' : 'text-gray-400 hover:bg-white/[0.08] hover:text-gray-200'
}`} }`}
> >
<Icon size={20} className="flex-shrink-0" /> <Icon size={20} className="flex-shrink-0" />
@ -104,7 +107,7 @@ export default function Sidebar() {
</nav> </nav>
{/* Footer */} {/* 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 Info */}
{user && (!collapsed || mobileOpen) && ( {user && (!collapsed || mobileOpen) && (
<div className="px-3 py-2"> <div className="px-3 py-2">
@ -116,7 +119,7 @@ export default function Sidebar() {
{/* Logout */} {/* Logout */}
<button <button
onClick={logout} 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" /> <LogOut size={20} className="flex-shrink-0" />
{(!collapsed || mobileOpen) && <span>Logout</span>} {(!collapsed || mobileOpen) && <span>Logout</span>}
@ -125,7 +128,7 @@ export default function Sidebar() {
{/* Collapse Toggle — desktop only */} {/* Collapse Toggle — desktop only */}
<button <button
onClick={() => setCollapsed(prev => !prev)} 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 ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
{!collapsed && <span className="text-sm">Collapse</span>} {!collapsed && <span className="text-sm">Collapse</span>}
@ -137,10 +140,10 @@ export default function Sidebar() {
return ( return (
<> <>
{/* Mobile top bar */} {/* 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 <button
onClick={() => setMobileOpen(true)} 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" aria-label="Open menu"
> >
<Menu size={22} /> <Menu size={22} />
@ -159,7 +162,7 @@ export default function Sidebar() {
{/* Mobile sidebar drawer */} {/* Mobile sidebar drawer */}
<aside <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 ${ transform transition-transform duration-300 ease-in-out lg:hidden ${
mobileOpen ? 'translate-x-0' : '-translate-x-full' mobileOpen ? 'translate-x-0' : '-translate-x-full'
}`} }`}
@ -169,7 +172,7 @@ export default function Sidebar() {
{/* Desktop sidebar */} {/* Desktop sidebar */}
<aside <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' collapsed ? 'w-16' : 'w-64'
}`} }`}
> >

View File

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

View File

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

View File

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

View File

@ -18,9 +18,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
)} )}
<input <input
ref={ref} ref={ref}
className={`w-full px-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${ 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' : '' error ? 'border-red-500 focus:ring-red-500' : ''
} ${className}`} } ${className}`}
{...props} {...props}

View File

@ -20,9 +20,10 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>(
)} )}
<select <select
ref={ref} ref={ref}
className={`w-full px-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
focus:ring-2 focus:ring-cyan-500 focus:border-transparent focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${ 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' : '' error ? 'border-red-500 focus:ring-red-500' : ''
} ${className}`} } ${className}`}
{...props} {...props}

View File

@ -12,7 +12,9 @@ const sizes = {
export default function Spinner({ size = 'md', className = '' }: SpinnerProps) { export default function Spinner({ size = 'md', className = '' }: SpinnerProps) {
return ( return (
<div className={`flex items-center justify-center ${className}`}> <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> </div>
) )
} }

View File

@ -20,16 +20,16 @@ const ConfirmationContext = createContext<ConfirmationContextType | undefined>(u
const typeStyles = { const typeStyles = {
danger: { danger: {
icon: 'bg-red-500/20 text-red-400', icon: 'bg-red-400/15 text-red-400 border border-red-400/20',
button: 'bg-red-500 hover:bg-red-600 text-white', button: 'bg-red-500/90 hover:bg-red-400/90 text-white shadow-lg shadow-red-500/20 hover:shadow-red-400/30',
}, },
warning: { warning: {
icon: 'bg-amber-500/20 text-amber-400', icon: 'bg-amber-400/15 text-amber-400 border border-amber-400/20',
button: 'bg-amber-500 hover:bg-amber-600 text-white', button: 'bg-amber-500/90 hover:bg-amber-400/90 text-white shadow-lg shadow-amber-500/20 hover:shadow-amber-400/30',
}, },
info: { info: {
icon: 'bg-cyan-500/20 text-cyan-400', icon: 'bg-cyan-400/15 text-cyan-400 border border-cyan-400/20',
button: 'bg-cyan-500 hover:bg-cyan-600 text-white', 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 && ( {isOpen && options && (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */} {/* 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 */} {/* 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="flex items-start gap-4">
<div className={`p-3 rounded-full ${styles.icon}`}> <div className={`p-3 rounded-full ${styles.icon}`}>
<AlertTriangle size={24} /> <AlertTriangle size={24} />
</div> </div>
<div className="flex-1"> <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"> <p className="mt-2 text-sm text-gray-400">
{options.message} {options.message}
{options.itemName && ( {options.itemName && (
@ -94,7 +94,7 @@ export function ConfirmationProvider({ children }: { children: ReactNode }) {
<div className="flex justify-end gap-3 mt-6"> <div className="flex justify-end gap-3 mt-6">
<button <button
onClick={handleCancel} 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'} {options.cancelText || 'Cancel'}
</button> </button>

View File

@ -20,10 +20,10 @@ const ToastContext = createContext<ToastContextType | undefined>(undefined)
const TOAST_DURATION = 5000 const TOAST_DURATION = 5000
const toastStyles: Record<ToastType, string> = { const toastStyles: Record<ToastType, string> = {
success: 'bg-emerald-500/20 border-emerald-500/50 text-emerald-400', success: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400',
error: 'bg-red-500/20 border-red-500/50 text-red-400', error: 'bg-red-500/10 border-red-500/30 text-red-400',
info: 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400', info: 'bg-cyan-500/10 border-cyan-500/30 text-cyan-400',
warning: 'bg-amber-500/20 border-amber-500/50 text-amber-400', warning: 'bg-amber-500/10 border-amber-500/30 text-amber-400',
} }
const toastIcons: Record<ToastType, typeof CheckCircle> = { const toastIcons: Record<ToastType, typeof CheckCircle> = {
@ -58,7 +58,8 @@ export function ToastProvider({ children }: { children: ReactNode }) {
return ( return (
<div <div
key={toast.id} 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" /> <Icon size={18} className="flex-shrink-0" />
<span className="text-sm font-medium">{toast.message}</span> <span className="text-sm font-medium">{toast.message}</span>