feat: Add professional web-based optimization dashboard

Complete dashboard UI for controlling and monitoring optimization runs.

Backend API (Flask):
- RESTful endpoints for study management
- Start/stop/resume optimization runs
- Real-time status monitoring
- Configuration management
- Visualization data endpoints

Frontend (HTML/CSS/JS + Chart.js):
- Modern gradient design with cards and charts
- Study list sidebar with metadata
- Active optimizations monitoring (5s polling)
- Interactive charts (progress, design vars, constraints)
- Trial history table
- New optimization modal
- Resume/delete study actions

Features:
- List all studies with trial counts
- View detailed study results
- Start new optimizations from UI
- Resume existing studies with additional trials
- Real-time progress monitoring
- Delete unwanted studies
- Chart.js visualizations (progress, DVs, constraints)
- Configuration file selection
- Study metadata tracking

Usage:
  python dashboard/start_dashboard.py
  # Opens browser to http://localhost:5000

Dependencies:
  flask, flask-cors (auto-installed)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-15 13:37:33 -05:00
parent 2c99497f0a
commit 1dab9d638d
7 changed files with 1933 additions and 0 deletions

507
dashboard/frontend/app.js Normal file
View File

@@ -0,0 +1,507 @@
// Atomizer Dashboard - Frontend JavaScript
const API_BASE = 'http://localhost:5000/api';
let currentStudy = null;
let charts = {
progress: null,
designVars: null,
constraints: null
};
// Initialize dashboard
document.addEventListener('DOMContentLoaded', () => {
console.log('Atomizer Dashboard loaded');
refreshStudies();
startActiveOptimizationsPolling();
});
// ====================
// Studies Management
// ====================
async function refreshStudies() {
try {
const response = await fetch(`${API_BASE}/studies`);
const data = await response.json();
if (data.success) {
renderStudiesList(data.studies);
} else {
showError('Failed to load studies: ' + data.error);
}
} catch (error) {
showError('Connection error: ' + error.message);
}
}
function renderStudiesList(studies) {
const container = document.getElementById('studiesList');
if (!studies || studies.length === 0) {
container.innerHTML = '<p class="empty">No studies found</p>';
return;
}
const html = studies.map(study => `
<div class="study-item" onclick="loadStudy('${study.study_name}')">
<div class="study-name">${study.study_name}</div>
<div class="study-info">
<span class="badge">${study.total_trials || 0} trials</span>
${study.resume_count > 0 ? `<span class="badge-secondary">Resumed ${study.resume_count}x</span>` : ''}
</div>
<div class="study-date">${formatDate(study.created_at)}</div>
</div>
`).join('');
container.innerHTML = html;
}
async function loadStudy(studyName) {
try {
const response = await fetch(`${API_BASE}/studies/${studyName}`);
const data = await response.json();
if (data.success) {
currentStudy = studyName;
displayStudyDetails(data);
} else {
showError('Failed to load study: ' + data.error);
}
} catch (error) {
showError('Connection error: ' + error.message);
}
}
function displayStudyDetails(data) {
// Hide welcome, show details
document.getElementById('welcomeScreen').style.display = 'none';
document.getElementById('studyDetails').style.display = 'block';
// Update header
document.getElementById('studyTitle').textContent = data.study_name;
document.getElementById('studyMeta').textContent =
`Created: ${formatDate(data.metadata.created_at)} | Config Hash: ${data.metadata.config_hash.substring(0, 8)}`;
// Update summary cards
if (data.summary && data.summary.best_value !== undefined) {
document.getElementById('bestObjective').textContent = data.summary.best_value.toFixed(4);
document.getElementById('totalTrials').textContent = data.summary.n_trials || data.history.length;
// Best parameters
const paramsHtml = Object.entries(data.summary.best_params || {})
.map(([name, value]) => `<div><strong>${name}:</strong> ${value.toFixed(4)}</div>`)
.join('');
document.getElementById('bestParams').innerHTML = paramsHtml || '<p>No data</p>';
}
// Render charts
renderCharts(data.history);
// Render history table
renderHistoryTable(data.history);
}
// ====================
// Charts
// ====================
async function renderCharts(history) {
if (!history || history.length === 0) return;
// Get visualization data
try {
const response = await fetch(`${API_BASE}/results/visualization/${currentStudy}`);
const data = await response.json();
if (!data.success) return;
// Progress Chart
renderProgressChart(data.trials, data.total_objectives, data.running_best);
// Design Variables Chart
renderDesignVarsChart(data.trials, data.design_variables);
// Constraints Chart
renderConstraintsChart(data.trials, data.constraints);
} catch (error) {
console.error('Error rendering charts:', error);
}
}
function renderProgressChart(trials, objectives, runningBest) {
const ctx = document.getElementById('progressChart').getContext('2d');
if (charts.progress) {
charts.progress.destroy();
}
charts.progress = new Chart(ctx, {
type: 'line',
data: {
labels: trials,
datasets: [
{
label: 'Total Objective',
data: objectives,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.1
},
{
label: 'Running Best',
data: runningBest,
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
tension: 0.1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
x: {
title: {
display: true,
text: 'Trial Number'
}
},
y: {
title: {
display: true,
text: 'Objective Value'
}
}
}
}
});
}
function renderDesignVarsChart(trials, designVars) {
const ctx = document.getElementById('designVarsChart').getContext('2d');
if (charts.designVars) {
charts.designVars.destroy();
}
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
const datasets = Object.entries(designVars).map(([name, values], index) => ({
label: name,
data: values,
borderColor: colors[index % colors.length],
backgroundColor: colors[index % colors.length] + '20',
tension: 0.1
}));
charts.designVars = new Chart(ctx, {
type: 'line',
data: {
labels: trials,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
x: {
title: {
display: true,
text: 'Trial Number'
}
},
y: {
title: {
display: true,
text: 'Value'
}
}
}
}
});
}
function renderConstraintsChart(trials, constraints) {
const ctx = document.getElementById('constraintsChart').getContext('2d');
if (charts.constraints) {
charts.constraints.destroy();
}
const colors = ['#3b82f6', '#10b981', '#f59e0b'];
const datasets = Object.entries(constraints).map(([name, values], index) => ({
label: name,
data: values,
borderColor: colors[index % colors.length],
backgroundColor: colors[index % colors.length] + '20',
tension: 0.1
}));
charts.constraints = new Chart(ctx, {
type: 'line',
data: {
labels: trials,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
x: {
title: {
display: true,
text: 'Trial Number'
}
},
y: {
title: {
display: true,
text: 'Value'
}
}
}
}
});
}
// ====================
// History Table
// ====================
function renderHistoryTable(history) {
const tbody = document.querySelector('#historyTable tbody');
if (!history || history.length === 0) {
tbody.innerHTML = '<tr><td colspan="5">No trials yet</td></tr>';
return;
}
const html = history.map(trial => `
<tr>
<td>${trial.trial_number}</td>
<td>${trial.total_objective.toFixed(4)}</td>
<td>${formatDesignVars(trial.design_variables)}</td>
<td>${formatConstraints(trial.constraints)}</td>
<td>${formatDateTime(trial.timestamp)}</td>
</tr>
`).join('');
tbody.innerHTML = html;
}
function formatDesignVars(vars) {
return Object.entries(vars)
.map(([name, value]) => `${name}=${value.toFixed(4)}`)
.join(', ');
}
function formatConstraints(constraints) {
return Object.entries(constraints)
.map(([name, value]) => `${name}=${value.toFixed(4)}`)
.join(', ');
}
// ====================
// New Optimization
// ====================
function showNewOptimizationModal() {
document.getElementById('newOptimizationModal').style.display = 'flex';
}
function closeNewOptimizationModal() {
document.getElementById('newOptimizationModal').style.display = 'none';
}
async function startOptimization() {
const studyName = document.getElementById('newStudyName').value || `study_${Date.now()}`;
const nTrials = parseInt(document.getElementById('newTrials').value) || 50;
const configPath = document.getElementById('newConfigPath').value;
const resume = document.getElementById('resumeExisting').checked;
try {
const response = await fetch(`${API_BASE}/optimization/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
study_name: studyName,
n_trials: nTrials,
config_path: configPath,
resume: resume
})
});
const data = await response.json();
if (data.success) {
showSuccess(`Optimization "${studyName}" started successfully!`);
closeNewOptimizationModal();
setTimeout(refreshStudies, 1000);
} else {
showError('Failed to start optimization: ' + data.error);
}
} catch (error) {
showError('Connection error: ' + error.message);
}
}
// ====================
// Active Optimizations Polling
// ====================
function startActiveOptimizationsPolling() {
setInterval(updateActiveOptimizations, 5000); // Poll every 5 seconds
updateActiveOptimizations();
}
async function updateActiveOptimizations() {
try {
const response = await fetch(`${API_BASE}/optimization/status`);
const data = await response.json();
if (data.success) {
renderActiveOptimizations(data.active_optimizations);
}
} catch (error) {
console.error('Failed to update active optimizations:', error);
}
}
function renderActiveOptimizations(optimizations) {
const container = document.getElementById('activeOptimizations');
const entries = Object.entries(optimizations);
if (entries.length === 0) {
container.innerHTML = '<p class="empty">No active optimizations</p>';
return;
}
const html = entries.map(([name, opt]) => `
<div class="active-item">
<div class="active-name">${name}</div>
<div class="active-status status-${opt.status}">${opt.status}</div>
${opt.status === 'running' ? `
<div class="progress-bar">
<div class="progress-fill" style="width: ${(opt.current_trial / opt.n_trials * 100).toFixed(0)}%"></div>
</div>
<div class="active-progress">${opt.current_trial || 0} / ${opt.n_trials} trials</div>
` : ''}
</div>
`).join('');
container.innerHTML = html;
}
// ====================
// Study Actions
// ====================
async function resumeCurrentStudy() {
if (!currentStudy) return;
const nTrials = prompt('Number of additional trials:', '25');
if (!nTrials) return;
try {
const response = await fetch(`${API_BASE}/optimization/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
study_name: currentStudy,
n_trials: parseInt(nTrials),
resume: true
})
});
const data = await response.json();
if (data.success) {
showSuccess(`Study "${currentStudy}" resumed with ${nTrials} additional trials`);
} else {
showError('Failed to resume study: ' + data.error);
}
} catch (error) {
showError('Connection error: ' + error.message);
}
}
async function deleteCurrentStudy() {
if (!currentStudy) return;
if (!confirm(`Are you sure you want to delete study "${currentStudy}"? This cannot be undone.`)) {
return;
}
try {
const response = await fetch(`${API_BASE}/studies/${currentStudy}/delete`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showSuccess(`Study "${currentStudy}" deleted successfully`);
currentStudy = null;
document.getElementById('studyDetails').style.display = 'none';
document.getElementById('welcomeScreen').style.display = 'block';
refreshStudies();
} else {
showError('Failed to delete study: ' + data.error);
}
} catch (error) {
showError('Connection error: ' + error.message);
}
}
// ====================
// Utility Functions
// ====================
function formatDate(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleDateString();
}
function formatDateTime(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleString();
}
function showSuccess(message) {
// Simple success notification
alert('✓ ' + message);
}
function showError(message) {
// Simple error notification
alert('✗ Error: ' + message);
console.error(message);
}

View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Atomizer - Optimization Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="dashboard-container">
<!-- Header -->
<header class="dashboard-header">
<div class="header-content">
<h1>⚛️ Atomizer</h1>
<p class="subtitle">Optimization Dashboard</p>
</div>
<div class="header-actions">
<button class="btn btn-primary" onclick="showNewOptimizationModal()">
▶️ New Optimization
</button>
<button class="btn btn-secondary" onclick="refreshStudies()">
🔄 Refresh
</button>
</div>
</header>
<!-- Main Content -->
<div class="main-content">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-section">
<h3>Studies</h3>
<div id="studiesList" class="studies-list">
<p class="loading">Loading studies...</p>
</div>
</div>
<div class="sidebar-section">
<h3>Active Optimizations</h3>
<div id="activeOptimizations" class="active-list">
<p class="empty">No active optimizations</p>
</div>
</div>
</aside>
<!-- Content Area -->
<main class="content-area">
<!-- Welcome Screen -->
<div id="welcomeScreen" class="welcome-screen">
<h2>Welcome to Atomizer</h2>
<p>Select a study from the sidebar or start a new optimization</p>
<div class="quick-actions">
<button class="btn btn-large btn-primary" onclick="showNewOptimizationModal()">
Start New Optimization
</button>
<button class="btn btn-large btn-secondary" onclick="refreshStudies()">
View Existing Studies
</button>
</div>
</div>
<!-- Study Details View -->
<div id="studyDetails" class="study-details" style="display: none;">
<div class="study-header">
<div>
<h2 id="studyTitle">Study Name</h2>
<p id="studyMeta" class="study-meta"></p>
</div>
<div class="study-actions">
<button class="btn btn-primary" onclick="resumeCurrentStudy()">
▶️ Resume Study
</button>
<button class="btn btn-danger" onclick="deleteCurrentStudy()">
🗑️ Delete
</button>
</div>
</div>
<!-- Best Result Card -->
<div class="results-cards">
<div class="result-card">
<h3>Best Result</h3>
<div class="result-value" id="bestObjective">-</div>
<p class="result-label">Objective Value</p>
</div>
<div class="result-card">
<h3>Total Trials</h3>
<div class="result-value" id="totalTrials">-</div>
<p class="result-label">Completed</p>
</div>
<div class="result-card">
<h3>Best Parameters</h3>
<div id="bestParams" class="params-list"></div>
</div>
</div>
<!-- Charts -->
<div class="charts-container">
<div class="chart-card">
<h3>Optimization Progress</h3>
<canvas id="progressChart"></canvas>
</div>
<div class="chart-card">
<h3>Design Variables</h3>
<canvas id="designVarsChart"></canvas>
</div>
<div class="chart-card">
<h3>Constraints</h3>
<canvas id="constraintsChart"></canvas>
</div>
</div>
<!-- History Table -->
<div class="history-section">
<h3>Trial History</h3>
<div class="table-container">
<table id="historyTable">
<thead>
<tr>
<th>Trial</th>
<th>Objective</th>
<th>Design Variables</th>
<th>Constraints</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- New Optimization Modal -->
<div id="newOptimizationModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h2>Start New Optimization</h2>
<button class="close-btn" onclick="closeNewOptimizationModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Study Name</label>
<input type="text" id="newStudyName" placeholder="my_optimization_study" />
</div>
<div class="form-group">
<label>Number of Trials</label>
<input type="number" id="newTrials" value="50" min="1" />
</div>
<div class="form-group">
<label>Configuration File</label>
<select id="newConfigPath">
<option value="examples/bracket/optimization_config_stress_displacement.json">
Bracket - Stress Minimization
</option>
<option value="examples/bracket/optimization_config.json">
Bracket - Multi-objective
</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="resumeExisting" />
Resume existing study (if exists)
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeNewOptimizationModal()">
Cancel
</button>
<button class="btn btn-primary" onclick="startOptimization()">
Start Optimization
</button>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,544 @@
/* Atomizer Dashboard Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--secondary: #64748b;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #1e293b;
--text-light: #64748b;
--border: #e2e8f0;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
.dashboard-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.dashboard-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-lg);
}
.header-content h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.subtitle {
opacity: 0.9;
font-size: 0.95rem;
}
.header-actions {
display: flex;
gap: 1rem;
}
/* Main Content */
.main-content {
flex: 1;
display: flex;
gap: 2rem;
padding: 2rem;
max-width: 1800px;
width: 100%;
margin: 0 auto;
}
/* Sidebar */
.sidebar {
width: 300px;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.sidebar-section {
background: var(--card-bg);
border-radius: 12px;
padding: 1.5rem;
box-shadow: var(--shadow);
}
.sidebar-section h3 {
font-size: 1.1rem;
margin-bottom: 1rem;
color: var(--text);
}
.studies-list, .active-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.study-item {
padding: 1rem;
background: var(--bg);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.study-item:hover {
background: #f1f5f9;
border-color: var(--primary);
transform: translateY(-2px);
}
.study-name {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text);
}
.study-info {
display: flex;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.study-date {
font-size: 0.85rem;
color: var(--text-light);
}
.badge {
display: inline-block;
padding: 0.2rem 0.5rem;
background: var(--primary);
color: white;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-secondary {
background: var(--secondary);
padding: 0.2rem 0.5rem;
color: white;
border-radius: 4px;
font-size: 0.75rem;
}
.active-item {
padding: 1rem;
background: var(--bg);
border-radius: 8px;
border-left: 4px solid var(--success);
}
.active-name {
font-weight: 600;
margin-bottom: 0.5rem;
}
.active-status {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.status-running {
background: var(--success);
color: white;
}
.status-completed {
background: var(--primary);
color: white;
}
.status-failed {
background: var(--danger);
color: white;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
margin: 0.5rem 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--success), var(--primary));
transition: width 0.3s;
}
.active-progress {
font-size: 0.85rem;
color: var(--text-light);
}
/* Content Area */
.content-area {
flex: 1;
background: var(--card-bg);
border-radius: 12px;
padding: 2rem;
box-shadow: var(--shadow);
overflow-y: auto;
}
.welcome-screen {
text-align: center;
padding: 4rem 2rem;
}
.welcome-screen h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: var(--text);
}
.welcome-screen p {
font-size: 1.2rem;
color: var(--text-light);
margin-bottom: 3rem;
}
.quick-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
/* Study Details */
.study-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 2px solid var(--border);
}
.study-header h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.study-meta {
color: var(--text-light);
font-size: 0.9rem;
}
.study-actions {
display: flex;
gap: 0.75rem;
}
/* Result Cards */
.results-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.result-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 12px;
box-shadow: var(--shadow-lg);
}
.result-card h3 {
font-size: 0.9rem;
opacity: 0.9;
margin-bottom: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.result-value {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.result-label {
font-size: 0.9rem;
opacity: 0.8;
}
.params-list {
font-size: 0.95rem;
}
.params-list div {
margin-bottom: 0.5rem;
}
/* Charts */
.charts-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.chart-card {
background: var(--bg);
padding: 1.5rem;
border-radius: 12px;
border: 1px solid var(--border);
}
.chart-card h3 {
margin-bottom: 1rem;
color: var(--text);
}
.chart-card canvas {
max-height: 300px;
}
/* History Table */
.history-section {
margin-top: 2rem;
}
.history-section h3 {
margin-bottom: 1rem;
}
.table-container {
overflow-x: auto;
border-radius: 8px;
border: 1px solid var(--border);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
thead {
background: var(--bg);
}
th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--text);
border-bottom: 2px solid var(--border);
}
td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
font-size: 0.9rem;
}
tbody tr:hover {
background: var(--bg);
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-size: 0.95rem;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
.btn-secondary {
background: var(--secondary);
color: white;
}
.btn-secondary:hover {
background: #475569;
transform: translateY(-1px);
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-large {
padding: 1rem 2rem;
font-size: 1.1rem;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
font-size: 1.5rem;
}
.close-btn {
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
color: var(--text-light);
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.close-btn:hover {
background: var(--bg);
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
padding: 1.5rem;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
/* Form Elements */
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--text);
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 0.75rem;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary);
}
.form-group input[type="checkbox"] {
margin-right: 0.5rem;
}
/* Utility Classes */
.loading, .empty {
text-align: center;
padding: 2rem;
color: var(--text-light);
}
/* Responsive */
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.charts-container {
grid-template-columns: 1fr;
}
}