feat(ui): redesign with modern styling and resource display

UI Improvements:
- Add gradient backgrounds to all components
- Add smooth hover/active animations with transforms
- Add subtle shadows and glow effects
- Add animated progress bar shimmer effect
- Add gradient logo with text fill

Button Redesign:
- Add gradient backgrounds and hover effects
- Add shadow on hover with lift animation
- Add btn-xs size for compact buttons
- Improve Kill All button styling

Session Cards:
- Show resource usage (memory, messages)
- Color-coded usage indicators (green/yellow/red)
- Add kill button per session
- Click to switch active session and load terminal

Process Management:
- Add Kill All Sessions button in panel header
- Add killAllSessions() and deleteSession() methods
- Add formatBytes() helper for display
- Add selectSession() to switch between sessions

Performance:
- Add requestAnimationFrame batching for terminal writes
- Smooth 60fps rendering with write batching

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-18 21:36:37 +01:00
co-authored by Claude Opus 4.5
parent 4712af433c
commit 09877c66b2
3 changed files with 506 additions and 83 deletions
+148 -6
View File
@@ -15,9 +15,28 @@ class ClaudemanApp {
this.respawnStatus = {}; // Respawn status per session
this.respawnEnabled = false; // Whether respawn is enabled for new sessions (disabled by default)
// Terminal write batching for performance
this.pendingWrites = '';
this.writeFrameScheduled = false;
this.init();
}
// Batch terminal writes using requestAnimationFrame for smooth rendering
batchTerminalWrite(data) {
this.pendingWrites += data;
if (!this.writeFrameScheduled) {
this.writeFrameScheduled = true;
requestAnimationFrame(() => {
if (this.pendingWrites && this.terminal) {
this.terminal.write(this.pendingWrites);
this.pendingWrites = '';
}
this.writeFrameScheduled = false;
});
}
}
init() {
this.initTerminal();
this.connectSSE();
@@ -195,10 +214,10 @@ class ClaudemanApp {
this.renderSessions();
});
// Handle raw terminal data from PTY
// Handle raw terminal data from PTY - use batched writes for performance
this.eventSource.addEventListener('session:terminal', (e) => {
const data = JSON.parse(e.data);
this.terminal.write(data.data);
this.batchTerminalWrite(data.data);
});
this.eventSource.addEventListener('session:output', (e) => {
@@ -932,7 +951,7 @@ class ClaudemanApp {
<span class="session-respawn-dot ${respawn.state === 'stopped' ? 'stopped' : ''}"></span>
<span class="session-respawn-state">${respawn.state.replace(/_/g, ' ')}</span>
<span class="session-respawn-actions">
<button class="btn btn-sm" onclick="app.stopRespawnForSession('${s.id}')">Stop</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); app.stopRespawnForSession('${s.id}')">Stop</button>
</span>
</div>
` : `
@@ -940,26 +959,79 @@ class ClaudemanApp {
<span class="session-respawn-dot stopped"></span>
<span class="session-respawn-state">No respawn</span>
<span class="session-respawn-actions">
<button class="btn btn-sm" onclick="app.startRespawnForSession('${s.id}')">Start</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); app.startRespawnForSession('${s.id}')">Start</button>
</span>
</div>
`;
// Resource usage display
const bufferStats = s.bufferStats || {};
const terminalSize = bufferStats.terminalBufferSize || 0;
const textSize = bufferStats.textOutputSize || 0;
const msgCount = bufferStats.messageCount || 0;
const totalMemory = terminalSize + textSize;
// Color coding based on usage
const memoryPercent = bufferStats.maxTerminalBuffer ?
(terminalSize / bufferStats.maxTerminalBuffer * 100) : 0;
const memoryClass = memoryPercent > 80 ? 'high' : memoryPercent > 50 ? 'medium' : 'low';
const resourceHtml = `
<div class="session-resources">
<div class="resource-item">
<span class="resource-label">Memory:</span>
<span class="resource-value ${memoryClass}">${this.formatBytes(totalMemory)}</span>
</div>
<div class="resource-item">
<span class="resource-label">Messages:</span>
<span class="resource-value">${msgCount}</span>
</div>
</div>
`;
return `
<div class="session-card" data-session="${s.id}">
<div class="session-card ${this.activeSessionId === s.id ? 'active' : ''}" data-session="${s.id}" onclick="app.selectSession('${s.id}')">
<div class="session-card-header">
<div class="session-status">
<span class="session-status-dot ${s.status}"></span>
<span>${s.id.slice(0, 8)}</span>
</div>
<span class="session-cost">$${(s.totalCost || 0).toFixed(4)}</span>
<div class="session-actions">
<span class="session-cost">$${(s.totalCost || 0).toFixed(4)}</span>
<button class="btn btn-xs btn-danger" onclick="event.stopPropagation(); app.deleteSession('${s.id}')" title="Kill session">✕</button>
</div>
</div>
${resourceHtml}
${respawnHtml}
</div>
`;
}).join('');
}
// Select a session to view its terminal
selectSession(sessionId) {
this.activeSessionId = sessionId;
this.renderSessions();
// Load the session's terminal buffer
this.loadSessionTerminal(sessionId);
}
// Load terminal buffer for a session
async loadSessionTerminal(sessionId) {
try {
const res = await fetch(`/api/sessions/${sessionId}/terminal`);
const data = await res.json();
if (data.terminalBuffer) {
this.terminal.clear();
this.terminal.writeln(`\x1b[90m--- Session ${sessionId.slice(0, 8)} terminal ---\x1b[0m`);
this.terminal.write(data.terminalBuffer);
}
} catch (err) {
console.error('Failed to load terminal buffer:', err);
}
}
async stopRespawnForSession(sessionId) {
try {
const res = await fetch(`/api/sessions/${sessionId}/respawn/stop`, {
@@ -974,6 +1046,76 @@ class ClaudemanApp {
}
}
// Kill all sessions
async killAllSessions() {
const count = this.sessions.size;
if (count === 0) {
this.terminal.writeln('\x1b[90mNo sessions to kill\x1b[0m');
return;
}
if (!confirm(`Kill all ${count} session(s)? This will stop all running Claude processes.`)) {
return;
}
this.terminal.writeln(`\x1b[1;33m⚠ Killing ${count} session(s)...\x1b[0m`);
try {
const res = await fetch('/api/sessions', {
method: 'DELETE'
});
const data = await res.json();
if (data.success) {
this.terminal.writeln(`\x1b[1;32m✓ Killed ${data.data.killed} session(s)\x1b[0m`);
this.sessions.clear();
this.activeSessionId = null;
this.respawnStatus = {};
this.hideRespawnBanner();
this.renderSessions();
} else {
throw new Error(data.error);
}
} catch (err) {
this.terminal.writeln(`\x1b[1;31m❌ Error: ${err.message}\x1b[0m`);
}
}
// Kill a single session
async deleteSession(sessionId) {
if (!confirm(`Kill session ${sessionId.slice(0, 8)}?`)) {
return;
}
try {
const res = await fetch(`/api/sessions/${sessionId}`, {
method: 'DELETE'
});
const data = await res.json();
if (data.success) {
this.terminal.writeln(`\x1b[1;32m✓ Session ${sessionId.slice(0, 8)} killed\x1b[0m`);
this.sessions.delete(sessionId);
if (this.activeSessionId === sessionId) {
this.activeSessionId = null;
}
delete this.respawnStatus[sessionId];
this.renderSessions();
} else {
throw new Error(data.error);
}
} catch (err) {
this.terminal.writeln(`\x1b[1;31m❌ Error: ${err.message}\x1b[0m`);
}
}
// Format bytes for display
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
updateSessionOutput(sessionId, text) {
// Sessions list shows brief info, main terminal shows full output
}
+8 -3
View File
@@ -242,9 +242,14 @@
<!-- Sessions Panel (collapsible) -->
<aside class="sessions-panel" id="sessionsPanel">
<div class="sessions-header" onclick="app.toggleSessions()">
<h3>Sessions <span class="session-count" id="sessionCount">0</span></h3>
<span class="toggle-icon" id="toggleIcon">▼</span>
<div class="sessions-header">
<div class="sessions-header-left" onclick="app.toggleSessions()">
<h3>Sessions <span class="session-count" id="sessionCount">0</span></h3>
<span class="toggle-icon" id="toggleIcon">▼</span>
</div>
<button class="btn btn-sm btn-danger kill-all-btn" onclick="event.stopPropagation(); app.killAllSessions()" title="Kill all sessions">
✕ Kill All
</button>
</div>
<div class="sessions-list" id="sessionsList"></div>
</aside>
+350 -74
View File
@@ -39,8 +39,9 @@ body {
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--bg-card);
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
border-bottom: 1px solid var(--border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.header-left {
@@ -50,8 +51,13 @@ body {
}
.logo {
font-size: 1.25rem;
font-weight: 600;
font-size: 1.3rem;
font-weight: 700;
background: linear-gradient(135deg, #60a5fa, #3b82f6, #818cf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.02em;
}
.connection-status {
@@ -67,10 +73,24 @@ body {
height: 8px;
border-radius: 50%;
background: var(--yellow);
box-shadow: 0 0 8px var(--yellow);
animation: statusPulse 2s ease-in-out infinite;
}
.status-dot.connected { background: var(--green); }
.status-dot.disconnected { background: var(--red); }
.status-dot.connected {
background: var(--green);
box-shadow: 0 0 8px var(--green);
}
.status-dot.disconnected {
background: var(--red);
box-shadow: 0 0 8px var(--red);
}
@keyframes statusPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.1); }
}
.header-stats {
display: flex;
@@ -98,9 +118,10 @@ body {
/* Timer Banner */
.timer-banner {
background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%);
border-bottom: 1px solid var(--accent);
padding: 1rem 1.5rem;
background: linear-gradient(135deg, #1e3a5f 0%, #152238 50%, #0f172a 100%);
border-bottom: 1px solid rgba(59, 130, 246, 0.4);
padding: 1.25rem 1.5rem;
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.15);
}
.timer-info {
@@ -124,17 +145,26 @@ body {
}
.timer-progress {
height: 6px;
background: var(--bg-input);
border-radius: 3px;
height: 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.75rem;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
}
.timer-progress-fill {
height: 100%;
background: var(--accent);
background: linear-gradient(90deg, #3b82f6, #60a5fa, #3b82f6);
background-size: 200% 100%;
animation: progressShimmer 2s linear infinite;
transition: width 1s linear;
border-radius: 4px;
}
@keyframes progressShimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.timer-meta {
@@ -185,13 +215,21 @@ body {
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
background: var(--bg-input);
padding: 0.75rem 1rem;
background: linear-gradient(135deg, #1a1a1a 0%, #141414 100%);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: 8px;
color: var(--text);
font-size: 0.875rem;
font-family: inherit;
transition: all 0.2s ease;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.form-group input::placeholder,
.form-group textarea::placeholder {
color: var(--text-muted);
opacity: 0.7;
}
.form-group textarea {
@@ -203,6 +241,12 @@ body {
.form-group textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15), inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.form-group input:hover:not(:focus),
.form-group textarea:hover:not(:focus) {
border-color: var(--text-muted);
}
.form-hint {
@@ -228,53 +272,127 @@ body {
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
padding: 0.625rem 1.125rem;
border: none;
border-radius: var(--radius);
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
letter-spacing: 0.01em;
}
.btn:active {
transform: translateY(1px);
box-shadow: none;
}
.btn-primary {
background: var(--accent);
background: linear-gradient(135deg, #4f8ff7 0%, #3b82f6 50%, #2563eb 100%);
color: white;
border: 1px solid rgba(59, 130, 246, 0.3);
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:hover {
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #2563eb 100%);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.35);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-danger {
background: var(--red);
background: linear-gradient(135deg, #f87171 0%, #ef4444 50%, #dc2626 100%);
color: white;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.btn-danger:hover {
background: linear-gradient(135deg, #fca5a5 0%, #ef4444 50%, #dc2626 100%);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.35);
transform: translateY(-1px);
}
.btn-success {
background: linear-gradient(135deg, #4ade80 0%, #22c55e 50%, #16a34a 100%);
color: white;
border: 1px solid rgba(34, 197, 94, 0.3);
}
.btn-success:hover {
background: linear-gradient(135deg, #86efac 0%, #22c55e 50%, #16a34a 100%);
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.35);
transform: translateY(-1px);
}
.btn-large {
width: 100%;
padding: 0.875rem 1.5rem;
font-size: 1rem;
border-radius: 8px;
}
.btn-sm {
padding: 0.375rem 0.75rem;
padding: 0.4rem 0.85rem;
font-size: 0.75rem;
background: var(--bg-input);
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
color: var(--text-dim);
border: 1px solid var(--border);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.btn-sm:hover {
background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%);
color: var(--text);
border-color: var(--text-muted);
}
.btn-sm.btn-danger {
background: transparent;
border-color: var(--red);
color: var(--red);
}
.btn-sm.btn-danger:hover {
background: var(--red);
color: white;
}
.btn-xs {
padding: 0.25rem 0.5rem;
font-size: 0.65rem;
background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: none;
}
.btn-xs:hover {
background: var(--bg-hover);
color: var(--text);
}
.btn-xs.btn-danger {
border-color: rgba(239, 68, 68, 0.5);
color: var(--red);
}
.btn-xs.btn-danger:hover {
background: var(--red);
color: white;
border-color: var(--red);
}
.btn-icon {
font-size: 0.875rem;
opacity: 0.9;
}
/* Output Panel */
@@ -315,16 +433,17 @@ body {
/* Terminal container for xterm.js */
.terminal-container {
flex: 1;
background: #1a1a2e;
border: 1px solid var(--border);
border-radius: var(--radius);
background: linear-gradient(135deg, #1a1a2e 0%, #141424 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 12px;
overflow: hidden;
min-height: 400px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.02);
}
.terminal-container .xterm {
height: 100%;
padding: 0.5rem;
padding: 0.75rem;
}
.terminal-container .xterm-viewport {
@@ -373,12 +492,19 @@ body {
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.5rem;
cursor: pointer;
user-select: none;
}
.sessions-header:hover {
background: var(--bg-hover);
.sessions-header-left {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
flex: 1;
}
.sessions-header-left:hover {
opacity: 0.8;
}
.sessions-header h3 {
@@ -387,6 +513,30 @@ body {
color: var(--text-dim);
}
.kill-all-btn {
padding: 0.4rem 0.85rem;
font-size: 0.7rem;
background: transparent;
border: 1px solid rgba(239, 68, 68, 0.4);
color: var(--red);
border-radius: 6px;
font-weight: 500;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.kill-all-btn:hover {
background: linear-gradient(135deg, #f87171 0%, #ef4444 50%, #dc2626 100%);
color: white;
border-color: var(--red);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.35);
transform: translateY(-1px);
}
.kill-all-btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3);
}
.session-count {
display: inline-flex;
align-items: center;
@@ -420,18 +570,20 @@ body {
.session-card {
flex-shrink: 0;
width: 300px;
background: var(--bg-input);
background: linear-gradient(135deg, #1a1a1a 0%, #141414 100%);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: 10px;
overflow: hidden;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.session-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--bg-dark);
padding: 0.875rem 1rem;
background: linear-gradient(135deg, #141414 0%, #0a0a0a 100%);
border-bottom: 1px solid var(--border);
}
@@ -457,12 +609,73 @@ body {
50% { opacity: 0.4; }
}
.session-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.session-cost {
font-size: 0.75rem;
color: var(--green);
font-weight: 500;
}
/* Session Resources Display */
.session-resources {
display: flex;
gap: 1rem;
padding: 0.5rem 0.75rem;
background: var(--bg-dark);
border-top: 1px solid var(--border);
font-size: 0.65rem;
}
.resource-item {
display: flex;
align-items: center;
gap: 0.25rem;
}
.resource-label {
color: var(--text-muted);
}
.resource-value {
font-weight: 500;
color: var(--text-dim);
font-family: 'SF Mono', Monaco, monospace;
}
.resource-value.low {
color: var(--green);
}
.resource-value.medium {
color: var(--yellow);
}
.resource-value.high {
color: var(--red);
}
/* Active session card */
.session-card.active {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3), 0 4px 12px rgba(59, 130, 246, 0.15);
}
.session-card:hover {
border-color: var(--text-muted);
cursor: pointer;
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.session-card:active {
transform: translateY(0);
}
.session-output {
height: 120px;
padding: 0.75rem;
@@ -543,31 +756,44 @@ body {
/* Tabs */
.tabs {
display: flex;
gap: 0.25rem;
gap: 0.5rem;
margin-bottom: 1.25rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.5rem;
padding-bottom: 0.75rem;
}
.tab {
padding: 0.5rem 1rem;
padding: 0.625rem 1.25rem;
background: transparent;
border: none;
border-radius: var(--radius) var(--radius) 0 0;
border-radius: 8px 8px 0 0;
color: var(--text-dim);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
transition: all 0.2s ease;
position: relative;
}
.tab:hover {
color: var(--text);
background: var(--bg-hover);
background: rgba(255, 255, 255, 0.05);
}
.tab.active {
color: var(--accent);
background: var(--bg-input);
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(59, 130, 246, 0.05) 100%);
}
.tab.active::after {
content: '';
position: absolute;
bottom: -0.75rem;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent), rgba(59, 130, 246, 0.5));
border-radius: 2px 2px 0 0;
}
.tab-content {
@@ -605,18 +831,36 @@ body {
/* Select */
select {
width: 100%;
padding: 0.75rem;
background: var(--bg-input);
padding: 0.75rem 2.5rem 0.75rem 1rem;
background: linear-gradient(135deg, #1a1a1a 0%, #141414 100%);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: 8px;
color: var(--text);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px;
}
select:hover {
border-color: var(--text-muted);
}
select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15), inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
select option {
background: var(--bg-card);
color: var(--text);
padding: 0.5rem;
}
/* Cases List */
@@ -632,17 +876,24 @@ select:focus {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--bg-input);
padding: 0.875rem 1rem;
background: linear-gradient(135deg, #1a1a1a 0%, #141414 100%);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: 10px;
cursor: pointer;
transition: all 0.15s;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.case-card:hover {
border-color: var(--accent);
background: var(--bg-hover);
background: linear-gradient(135deg, #222 0%, #1a1a1a 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}
.case-card:active {
transform: translateY(0);
}
.case-icon {
@@ -702,49 +953,68 @@ input[type="checkbox"] {
.modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.7);
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
background: var(--bg-card);
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
animation: modalSlideIn 0.2s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, #141414 0%, #0a0a0a 100%);
}
.modal-header h3 {
font-size: 1rem;
font-weight: 500;
font-size: 1.125rem;
font-weight: 600;
}
.modal-close {
background: none;
border: none;
background: transparent;
border: 1px solid var(--border);
color: var(--text-dim);
font-size: 1.5rem;
font-size: 1.25rem;
cursor: pointer;
padding: 0;
padding: 0.25rem 0.5rem;
line-height: 1;
border-radius: 6px;
transition: all 0.2s ease;
}
.modal-close:hover {
color: var(--text);
background: var(--bg-hover);
border-color: var(--text-muted);
}
.modal-body {
padding: 1rem;
padding: 1.5rem;
max-height: 60vh;
overflow-y: auto;
}
@@ -795,13 +1065,14 @@ input[type="checkbox"] {
/* Respawn Banner */
.respawn-banner {
background: linear-gradient(135deg, #1e3a1e 0%, #0f1f0f 100%);
border-bottom: 1px solid var(--green);
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #1e3a1e 0%, #132513 50%, #0f1f0f 100%);
border-bottom: 1px solid rgba(34, 197, 94, 0.4);
padding: 0.875rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
box-shadow: 0 2px 12px rgba(34, 197, 94, 0.15);
}
.respawn-status-info {
@@ -888,11 +1159,12 @@ input[type="checkbox"] {
/* Quick Start Section */
.quick-start-section {
background: linear-gradient(135deg, #0d4020 0%, #0a2510 100%);
border: 1px solid var(--green);
border-radius: var(--radius);
padding: 1rem;
background: linear-gradient(135deg, #0d4020 0%, #082010 50%, #051208 100%);
border: 1px solid rgba(34, 197, 94, 0.4);
border-radius: 12px;
padding: 1.25rem;
margin-bottom: 1rem;
box-shadow: 0 4px 20px rgba(34, 197, 94, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.02);
}
.quick-start-header {
@@ -935,24 +1207,28 @@ input[type="checkbox"] {
}
.btn-quick-start {
background: var(--green);
background: linear-gradient(135deg, #4ade80 0%, #22c55e 50%, #16a34a 100%);
color: #000;
font-weight: 600;
padding: 0.625rem 1.25rem;
border: none;
border-radius: var(--radius);
padding: 0.75rem 1.5rem;
border: 1px solid rgba(34, 197, 94, 0.4);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
}
.btn-quick-start:hover {
background: #16a34a;
transform: translateY(-1px);
background: linear-gradient(135deg, #86efac 0%, #4ade80 50%, #22c55e 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4);
}
.btn-quick-start:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(34, 197, 94, 0.3);
}
.quick-start-hint {