mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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:
+148
-6
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user