mirror of
https://github.com/DeNNiiInc/Advanced-Raid-Calculator.git
synced 2026-04-17 12:45:59 +00:00
Incorporate BCT branding: Add Logo, favicon, and update styles
This commit is contained in:
205
index.html
Normal file
205
index.html
Normal file
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Beyond Cloud Technology - RAID Calculator</title>
|
||||
<meta name="description"
|
||||
content="Professional RAID calculator by Beyond Cloud Technology supporting standard RAID, Synology SHR, ZFS, and Unraid configurations with accurate usable storage calculations.">
|
||||
<link rel="icon" type="image/png" href="Logo.png">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<h1 class="title">
|
||||
<img src="Logo.png" alt="BCT Logo" class="title-icon">
|
||||
Beyond Cloud Technology - RAID Calculator
|
||||
</h1>
|
||||
<p class="subtitle">Professional storage capacity planning for RAID, Synology SHR, ZFS & Unraid
|
||||
configurations
|
||||
</p>
|
||||
<a href="https://www.youtube.com/@beyondcloudtechnology" target="_blank" rel="noopener noreferrer"
|
||||
class="youtube-link">
|
||||
<svg class="youtube-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</svg>
|
||||
Watch on YouTube @beyondcloudtechnology
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="calculator-grid">
|
||||
<!-- Configuration Panel -->
|
||||
<section class="config-panel card">
|
||||
<h2 class="section-title">Configuration</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="raid-type" class="label">RAID Type</label>
|
||||
<select id="raid-type" class="select">
|
||||
<optgroup label="Standard RAID">
|
||||
<option value="raid0">RAID 0 - Striping (No Redundancy)</option>
|
||||
<option value="raid1">RAID 1 - Mirroring</option>
|
||||
<option value="raid5" selected>RAID 5 - Single Parity</option>
|
||||
<option value="raid6">RAID 6 - Double Parity</option>
|
||||
<option value="raid10">RAID 10 - Mirrored Stripes</option>
|
||||
</optgroup>
|
||||
<optgroup label="Synology RAID">
|
||||
<option value="shr">SHR - Synology Hybrid RAID</option>
|
||||
<option value="shr2">SHR-2 - Synology Hybrid RAID (2-disk fault tolerance)</option>
|
||||
</optgroup>
|
||||
<optgroup label="ZFS RAID">
|
||||
<option value="zfs-stripe">ZFS Stripe (No Redundancy)</option>
|
||||
<option value="zfs-mirror">ZFS Mirror</option>
|
||||
<option value="raidz1">RAIDZ1 - Single Parity</option>
|
||||
<option value="raidz2">RAIDZ2 - Double Parity</option>
|
||||
<option value="raidz3">RAIDZ3 - Triple Parity</option>
|
||||
</optgroup>
|
||||
<optgroup label="Unraid">
|
||||
<option value="unraid-1">Unraid - 1 Parity Drive</option>
|
||||
<option value="unraid-2">Unraid - 2 Parity Drives</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="num-drives" class="label">Number of Drives</label>
|
||||
<div class="input-with-display">
|
||||
<input type="range" id="num-drives" class="slider" min="2" max="24" value="4">
|
||||
<span id="num-drives-display" class="value-display">4</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="drive-size" class="label">Drive Size (TB)</label>
|
||||
<select id="drive-size" class="select">
|
||||
<option value="1">1 TB</option>
|
||||
<option value="2">2 TB</option>
|
||||
<option value="3">3 TB</option>
|
||||
<option value="4">4 TB</option>
|
||||
<option value="6">6 TB</option>
|
||||
<option value="8">8 TB</option>
|
||||
<option value="10">10 TB</option>
|
||||
<option value="12" selected>12 TB</option>
|
||||
<option value="14">14 TB</option>
|
||||
<option value="16">16 TB</option>
|
||||
<option value="18">18 TB</option>
|
||||
<option value="20">20 TB</option>
|
||||
<option value="22">22 TB</option>
|
||||
<option value="24">24 TB</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="zfs-vdev-config" class="form-group" style="display: none;">
|
||||
<label for="num-vdevs" class="label">Number of vdevs</label>
|
||||
<div class="input-with-display">
|
||||
<input type="range" id="num-vdevs" class="slider" min="1" max="8" value="2">
|
||||
<span id="num-vdevs-display" class="value-display">2</span>
|
||||
</div>
|
||||
<p class="helper-text">Each vdev will use the selected RAID type (mirror/RAIDZ)</p>
|
||||
</div>
|
||||
|
||||
<div id="drives-per-vdev-config" class="form-group" style="display: none;">
|
||||
<label for="drives-per-vdev" class="label">Drives per vdev</label>
|
||||
<div class="input-with-display">
|
||||
<input type="range" id="drives-per-vdev" class="slider" min="2" max="12" value="3">
|
||||
<span id="drives-per-vdev-display" class="value-display">3</span>
|
||||
</div>
|
||||
<p class="helper-text" id="total-drives-text">Total drives: 6</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">Mixed Drive Sizes</label>
|
||||
<div class="toggle-container">
|
||||
<input type="checkbox" id="mixed-drives" class="toggle-input">
|
||||
<label for="mixed-drives" class="toggle-label">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span class="toggle-text">Enable custom drive configuration</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="custom-drives-container" class="custom-drives-container" style="display: none;">
|
||||
<div id="drive-inputs" class="drive-inputs"></div>
|
||||
</div>
|
||||
|
||||
<button id="calculate-btn" class="btn-primary">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
Calculate Storage
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Results Panel -->
|
||||
<section class="results-panel card">
|
||||
<h2 class="section-title">Results</h2>
|
||||
|
||||
<div id="results-content" class="results-content">
|
||||
<div class="empty-state">
|
||||
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<p>Configure your RAID array and click "Calculate Storage" to see results</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Information Cards -->
|
||||
<div class="info-grid">
|
||||
<div class="info-card card">
|
||||
<div class="info-icon info-icon-blue">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4m0-4h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="info-title">Accurate Calculations</h3>
|
||||
<p class="info-text">Accounts for binary vs decimal differences - 12TB drives show actual ~10.9TB
|
||||
usable capacity</p>
|
||||
</div>
|
||||
|
||||
<div class="info-card card">
|
||||
<div class="info-icon info-icon-green">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="info-title">Fault Tolerance</h3>
|
||||
<p class="info-text">See exactly how many drives can fail while maintaining data integrity</p>
|
||||
</div>
|
||||
|
||||
<div class="info-card card">
|
||||
<div class="info-icon info-icon-purple">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="info-title">Performance Insights</h3>
|
||||
<p class="info-text">Understand read/write performance characteristics of each RAID configuration
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2025 Beyond Cloud Technology. Professional storage planning tool.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
718
script.js
Normal file
718
script.js
Normal file
@@ -0,0 +1,718 @@
|
||||
// ===================================
|
||||
// RAID Calculator - Core Logic
|
||||
// ===================================
|
||||
|
||||
// Storage conversion constants
|
||||
const TB_TO_BYTES_DECIMAL = 1000000000000; // 1 TB = 1,000,000,000,000 bytes (decimal)
|
||||
const TB_TO_BYTES_BINARY = 1099511627776; // 1 TiB = 1,099,511,627,776 bytes (binary)
|
||||
|
||||
// RAID Configuration Metadata
|
||||
const RAID_CONFIGS = {
|
||||
raid0: {
|
||||
name: 'RAID 0',
|
||||
description: 'Striping - Maximum performance, no redundancy',
|
||||
minDrives: 2,
|
||||
faultTolerance: 0,
|
||||
readPerformance: 'Excellent',
|
||||
writePerformance: 'Excellent',
|
||||
efficiency: 1.0
|
||||
},
|
||||
raid1: {
|
||||
name: 'RAID 1',
|
||||
description: 'Mirroring - 100% redundancy',
|
||||
minDrives: 2,
|
||||
faultTolerance: 1,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: 0.5
|
||||
},
|
||||
raid5: {
|
||||
name: 'RAID 5',
|
||||
description: 'Single parity - Good balance of performance and redundancy',
|
||||
minDrives: 3,
|
||||
faultTolerance: 1,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated: (n-1)/n
|
||||
},
|
||||
raid6: {
|
||||
name: 'RAID 6',
|
||||
description: 'Double parity - Enhanced fault tolerance',
|
||||
minDrives: 4,
|
||||
faultTolerance: 2,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated: (n-2)/n
|
||||
},
|
||||
raid10: {
|
||||
name: 'RAID 10',
|
||||
description: 'Mirrored stripes - Best performance with redundancy',
|
||||
minDrives: 4,
|
||||
faultTolerance: 1,
|
||||
readPerformance: 'Excellent',
|
||||
writePerformance: 'Good',
|
||||
efficiency: 0.5
|
||||
},
|
||||
shr: {
|
||||
name: 'Synology Hybrid RAID',
|
||||
description: 'Flexible RAID with single disk fault tolerance',
|
||||
minDrives: 2,
|
||||
faultTolerance: 1,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated based on drive configuration
|
||||
},
|
||||
shr2: {
|
||||
name: 'Synology Hybrid RAID 2',
|
||||
description: 'Flexible RAID with dual disk fault tolerance',
|
||||
minDrives: 4,
|
||||
faultTolerance: 2,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated based on drive configuration
|
||||
},
|
||||
'zfs-stripe': {
|
||||
name: 'ZFS Stripe',
|
||||
description: 'No redundancy - Maximum capacity',
|
||||
minDrives: 1,
|
||||
faultTolerance: 0,
|
||||
readPerformance: 'Excellent',
|
||||
writePerformance: 'Excellent',
|
||||
efficiency: 1.0
|
||||
},
|
||||
'zfs-mirror': {
|
||||
name: 'ZFS Mirror',
|
||||
description: 'Mirrored vdevs - High redundancy',
|
||||
minDrives: 2,
|
||||
faultTolerance: 1,
|
||||
readPerformance: 'Excellent',
|
||||
writePerformance: 'Good',
|
||||
efficiency: 0.5
|
||||
},
|
||||
raidz1: {
|
||||
name: 'RAIDZ1',
|
||||
description: 'Single parity - ZFS equivalent to RAID 5',
|
||||
minDrives: 3,
|
||||
faultTolerance: 1,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated: (n-1)/n
|
||||
},
|
||||
raidz2: {
|
||||
name: 'RAIDZ2',
|
||||
description: 'Double parity - ZFS equivalent to RAID 6',
|
||||
minDrives: 4,
|
||||
faultTolerance: 2,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated: (n-2)/n
|
||||
},
|
||||
raidz3: {
|
||||
name: 'RAIDZ3',
|
||||
description: 'Triple parity - Maximum fault tolerance',
|
||||
minDrives: 5,
|
||||
faultTolerance: 3,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated: (n-3)/n
|
||||
},
|
||||
'unraid-1': {
|
||||
name: 'Unraid (1 Parity)',
|
||||
description: 'Flexible array with single parity drive - Individual drive access',
|
||||
minDrives: 2,
|
||||
faultTolerance: 1,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated: (n-1)/n
|
||||
},
|
||||
'unraid-2': {
|
||||
name: 'Unraid (2 Parity)',
|
||||
description: 'Flexible array with dual parity drives - Individual drive access',
|
||||
minDrives: 3,
|
||||
faultTolerance: 2,
|
||||
readPerformance: 'Good',
|
||||
writePerformance: 'Moderate',
|
||||
efficiency: null // Calculated: (n-2)/n
|
||||
}
|
||||
};
|
||||
|
||||
// ===================================
|
||||
// Storage Calculation Functions
|
||||
// ===================================
|
||||
|
||||
/**
|
||||
* Convert TB (decimal) to actual usable TiB (binary)
|
||||
* Example: 12 TB = 10.91 TiB usable
|
||||
*/
|
||||
function convertTBtoTiB(tb) {
|
||||
const bytes = tb * TB_TO_BYTES_DECIMAL;
|
||||
const tib = bytes / TB_TO_BYTES_BINARY;
|
||||
return tib;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate usable storage for RAID configuration
|
||||
*/
|
||||
function calculateRAIDStorage(raidType, drives) {
|
||||
const numDrives = drives.length;
|
||||
const config = RAID_CONFIGS[raidType];
|
||||
|
||||
// Validate minimum drives
|
||||
if (numDrives < config.minDrives) {
|
||||
throw new Error(`${config.name} requires at least ${config.minDrives} drives`);
|
||||
}
|
||||
|
||||
// Convert all drives to TiB (binary)
|
||||
const drivesInTiB = drives.map(tb => convertTBtoTiB(tb));
|
||||
|
||||
let usableCapacity = 0;
|
||||
let rawCapacity = drivesInTiB.reduce((sum, size) => sum + size, 0);
|
||||
|
||||
switch (raidType) {
|
||||
case 'raid0':
|
||||
case 'zfs-stripe':
|
||||
// Sum of all drives
|
||||
usableCapacity = rawCapacity;
|
||||
break;
|
||||
|
||||
case 'raid1':
|
||||
case 'zfs-mirror':
|
||||
// Smallest drive capacity (all drives mirror the smallest)
|
||||
usableCapacity = Math.min(...drivesInTiB);
|
||||
break;
|
||||
|
||||
case 'raid5':
|
||||
case 'raidz1':
|
||||
// (n-1) * smallest drive
|
||||
usableCapacity = (numDrives - 1) * Math.min(...drivesInTiB);
|
||||
break;
|
||||
|
||||
case 'raid6':
|
||||
case 'raidz2':
|
||||
// (n-2) * smallest drive
|
||||
usableCapacity = (numDrives - 2) * Math.min(...drivesInTiB);
|
||||
break;
|
||||
|
||||
case 'raidz3':
|
||||
// (n-3) * smallest drive
|
||||
usableCapacity = (numDrives - 3) * Math.min(...drivesInTiB);
|
||||
break;
|
||||
|
||||
case 'raid10':
|
||||
// n/2 * smallest drive (assumes even number of drives)
|
||||
if (numDrives % 2 !== 0) {
|
||||
throw new Error('RAID 10 requires an even number of drives');
|
||||
}
|
||||
usableCapacity = (numDrives / 2) * Math.min(...drivesInTiB);
|
||||
break;
|
||||
|
||||
case 'shr':
|
||||
// Synology Hybrid RAID calculation
|
||||
usableCapacity = calculateSHR(drivesInTiB, 1);
|
||||
break;
|
||||
|
||||
case 'shr2':
|
||||
// Synology Hybrid RAID 2 calculation
|
||||
usableCapacity = calculateSHR(drivesInTiB, 2);
|
||||
break;
|
||||
|
||||
case 'unraid-1':
|
||||
// Unraid with 1 parity drive - sum of all drives minus largest (parity)
|
||||
usableCapacity = calculateUnraid(drivesInTiB, 1);
|
||||
break;
|
||||
|
||||
case 'unraid-2':
|
||||
// Unraid with 2 parity drives - sum of all drives minus 2 largest (parity)
|
||||
usableCapacity = calculateUnraid(drivesInTiB, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
usableCapacity,
|
||||
rawCapacity,
|
||||
efficiency: usableCapacity / rawCapacity,
|
||||
wastedSpace: rawCapacity - usableCapacity
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Synology Hybrid RAID capacity
|
||||
* SHR optimizes storage by using different RAID levels based on drive sizes
|
||||
*/
|
||||
function calculateSHR(drivesInTiB, parityDisks) {
|
||||
const sorted = [...drivesInTiB].sort((a, b) => a - b);
|
||||
const numDrives = sorted.length;
|
||||
|
||||
if (numDrives < parityDisks + 1) {
|
||||
throw new Error(`SHR-${parityDisks} requires at least ${parityDisks + 1} drives`);
|
||||
}
|
||||
|
||||
let usableCapacity = 0;
|
||||
|
||||
// SHR algorithm: Build up capacity by size tiers
|
||||
for (let i = 0; i < numDrives; i++) {
|
||||
const currentSize = sorted[i];
|
||||
const availableDrives = numDrives - i;
|
||||
|
||||
if (availableDrives > parityDisks) {
|
||||
// Calculate contribution of this drive
|
||||
if (i === 0) {
|
||||
// First (smallest) drives contribute their full capacity minus parity
|
||||
usableCapacity += currentSize * (availableDrives - parityDisks) / availableDrives;
|
||||
} else {
|
||||
// Larger drives contribute the difference from the previous tier
|
||||
const difference = currentSize - sorted[i - 1];
|
||||
usableCapacity += difference * (availableDrives - parityDisks) / availableDrives;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usableCapacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Unraid capacity
|
||||
* Unraid uses individual drives with parity - largest drive(s) become parity
|
||||
* Usable capacity = sum of all drives minus the largest N drives (where N = parity count)
|
||||
*/
|
||||
function calculateUnraid(drivesInTiB, parityCount) {
|
||||
const sorted = [...drivesInTiB].sort((a, b) => b - a); // Sort descending
|
||||
const numDrives = sorted.length;
|
||||
|
||||
if (numDrives < parityCount + 1) {
|
||||
throw new Error(`Unraid with ${parityCount} parity requires at least ${parityCount + 1} drives`);
|
||||
}
|
||||
|
||||
// Remove the largest N drives (parity drives)
|
||||
const dataDrives = sorted.slice(parityCount);
|
||||
|
||||
// Sum remaining drives for usable capacity
|
||||
return dataDrives.reduce((sum, size) => sum + size, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate ZFS storage with multiple vdevs
|
||||
* Each vdev uses the specified RAID type, and total capacity is the sum of all vdevs
|
||||
*/
|
||||
function calculateZFSVdevStorage(raidType, allDrives, numVdevs, drivesPerVdev) {
|
||||
const totalDrives = allDrives.length;
|
||||
|
||||
if (totalDrives !== numVdevs * drivesPerVdev) {
|
||||
throw new Error(`Total drives (${totalDrives}) must equal vdevs (${numVdevs}) × drives per vdev (${drivesPerVdev})`);
|
||||
}
|
||||
|
||||
// Convert all drives to TiB
|
||||
const drivesInTiB = allDrives.map(tb => convertTBtoTiB(tb));
|
||||
|
||||
let totalUsableCapacity = 0;
|
||||
const rawCapacity = drivesInTiB.reduce((sum, size) => sum + size, 0);
|
||||
|
||||
// Calculate capacity for each vdev
|
||||
for (let vdevIndex = 0; vdevIndex < numVdevs; vdevIndex++) {
|
||||
const startIdx = vdevIndex * drivesPerVdev;
|
||||
const endIdx = startIdx + drivesPerVdev;
|
||||
const vdevDrives = drivesInTiB.slice(startIdx, endIdx);
|
||||
const smallestInVdev = Math.min(...vdevDrives);
|
||||
|
||||
let vdevCapacity = 0;
|
||||
|
||||
switch (raidType) {
|
||||
case 'zfs-mirror':
|
||||
// Mirror: capacity of smallest drive in vdev
|
||||
vdevCapacity = smallestInVdev;
|
||||
break;
|
||||
|
||||
case 'raidz1':
|
||||
// RAIDZ1: (n-1) * smallest drive
|
||||
vdevCapacity = (drivesPerVdev - 1) * smallestInVdev;
|
||||
break;
|
||||
|
||||
case 'raidz2':
|
||||
// RAIDZ2: (n-2) * smallest drive
|
||||
vdevCapacity = (drivesPerVdev - 2) * smallestInVdev;
|
||||
break;
|
||||
|
||||
case 'raidz3':
|
||||
// RAIDZ3: (n-3) * smallest drive
|
||||
vdevCapacity = (drivesPerVdev - 3) * smallestInVdev;
|
||||
break;
|
||||
}
|
||||
|
||||
totalUsableCapacity += vdevCapacity;
|
||||
}
|
||||
|
||||
return {
|
||||
usableCapacity: totalUsableCapacity,
|
||||
rawCapacity,
|
||||
efficiency: totalUsableCapacity / rawCapacity,
|
||||
wastedSpace: rawCapacity - totalUsableCapacity
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format capacity for display
|
||||
*/
|
||||
function formatCapacity(tib, showBoth = true) {
|
||||
const tb = (tib * TB_TO_BYTES_BINARY) / TB_TO_BYTES_DECIMAL;
|
||||
|
||||
if (showBoth) {
|
||||
return `${tib.toFixed(2)} TiB (${tb.toFixed(2)} TB)`;
|
||||
}
|
||||
return `${tib.toFixed(2)} TiB`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage
|
||||
*/
|
||||
function formatPercentage(value) {
|
||||
return `${(value * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// UI Interaction Functions
|
||||
// ===================================
|
||||
|
||||
class RAIDCalculator {
|
||||
constructor() {
|
||||
this.initializeElements();
|
||||
this.attachEventListeners();
|
||||
this.updateDriveDisplay();
|
||||
this.handleRaidTypeChange();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.raidTypeSelect = document.getElementById('raid-type');
|
||||
this.numDrivesSlider = document.getElementById('num-drives');
|
||||
this.numDrivesDisplay = document.getElementById('num-drives-display');
|
||||
this.driveSizeSelect = document.getElementById('drive-size');
|
||||
this.mixedDrivesToggle = document.getElementById('mixed-drives');
|
||||
this.customDrivesContainer = document.getElementById('custom-drives-container');
|
||||
this.driveInputsContainer = document.getElementById('drive-inputs');
|
||||
this.calculateBtn = document.getElementById('calculate-btn');
|
||||
this.resultsContent = document.getElementById('results-content');
|
||||
|
||||
// ZFS vdev elements
|
||||
this.zfsVdevConfig = document.getElementById('zfs-vdev-config');
|
||||
this.numVdevsSlider = document.getElementById('num-vdevs');
|
||||
this.numVdevsDisplay = document.getElementById('num-vdevs-display');
|
||||
this.drivesPerVdevConfig = document.getElementById('drives-per-vdev-config');
|
||||
this.drivesPerVdevSlider = document.getElementById('drives-per-vdev');
|
||||
this.drivesPerVdevDisplay = document.getElementById('drives-per-vdev-display');
|
||||
this.totalDrivesText = document.getElementById('total-drives-text');
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
this.numDrivesSlider.addEventListener('input', () => this.updateDriveDisplay());
|
||||
this.mixedDrivesToggle.addEventListener('change', () => this.toggleMixedDrives());
|
||||
this.calculateBtn.addEventListener('click', () => this.calculate());
|
||||
this.raidTypeSelect.addEventListener('change', () => this.handleRaidTypeChange());
|
||||
|
||||
// ZFS vdev listeners
|
||||
this.numVdevsSlider.addEventListener('input', () => this.updateVdevDisplay());
|
||||
this.drivesPerVdevSlider.addEventListener('input', () => this.updateVdevDisplay());
|
||||
}
|
||||
|
||||
handleRaidTypeChange() {
|
||||
const raidType = this.raidTypeSelect.value;
|
||||
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
|
||||
|
||||
// Show/hide ZFS vdev configuration
|
||||
this.zfsVdevConfig.style.display = isZFSVdev ? 'block' : 'none';
|
||||
this.drivesPerVdevConfig.style.display = isZFSVdev ? 'block' : 'none';
|
||||
|
||||
// Show/hide regular drive count slider
|
||||
const numDrivesGroup = this.numDrivesSlider.closest('.form-group');
|
||||
numDrivesGroup.style.display = isZFSVdev ? 'none' : 'block';
|
||||
|
||||
if (isZFSVdev) {
|
||||
this.updateVdevDisplay();
|
||||
} else {
|
||||
this.validateConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
updateVdevDisplay() {
|
||||
const numVdevs = parseInt(this.numVdevsSlider.value);
|
||||
const drivesPerVdev = parseInt(this.drivesPerVdevSlider.value);
|
||||
const totalDrives = numVdevs * drivesPerVdev;
|
||||
|
||||
this.numVdevsDisplay.textContent = numVdevs;
|
||||
this.drivesPerVdevDisplay.textContent = drivesPerVdev;
|
||||
this.totalDrivesText.textContent = `Total drives: ${totalDrives}`;
|
||||
|
||||
// Update the main drive slider to match (for mixed drives feature)
|
||||
this.numDrivesSlider.value = totalDrives;
|
||||
this.numDrivesDisplay.textContent = totalDrives;
|
||||
|
||||
if (this.mixedDrivesToggle.checked) {
|
||||
this.generateDriveInputs(totalDrives);
|
||||
}
|
||||
|
||||
this.validateConfiguration();
|
||||
}
|
||||
|
||||
updateDriveDisplay() {
|
||||
const numDrives = parseInt(this.numDrivesSlider.value);
|
||||
this.numDrivesDisplay.textContent = numDrives;
|
||||
|
||||
if (this.mixedDrivesToggle.checked) {
|
||||
this.generateDriveInputs(numDrives);
|
||||
}
|
||||
|
||||
this.validateConfiguration();
|
||||
}
|
||||
|
||||
toggleMixedDrives() {
|
||||
const isEnabled = this.mixedDrivesToggle.checked;
|
||||
this.customDrivesContainer.style.display = isEnabled ? 'block' : 'none';
|
||||
this.driveSizeSelect.disabled = isEnabled;
|
||||
|
||||
if (isEnabled) {
|
||||
const raidType = this.raidTypeSelect.value;
|
||||
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
|
||||
const numDrives = isZFSVdev ?
|
||||
parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) :
|
||||
parseInt(this.numDrivesSlider.value);
|
||||
this.generateDriveInputs(numDrives);
|
||||
}
|
||||
}
|
||||
|
||||
generateDriveInputs(numDrives) {
|
||||
this.driveInputsContainer.innerHTML = '';
|
||||
|
||||
const driveSizes = [1, 2, 3, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24];
|
||||
|
||||
for (let i = 0; i < numDrives; i++) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'drive-input-row';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'drive-label';
|
||||
label.textContent = `Drive ${i + 1}:`;
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.className = 'drive-select';
|
||||
select.id = `drive-${i}`;
|
||||
|
||||
driveSizes.forEach(size => {
|
||||
const option = document.createElement('option');
|
||||
option.value = size;
|
||||
option.textContent = `${size} TB`;
|
||||
if (size === 12) option.selected = true;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(select);
|
||||
this.driveInputsContainer.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
validateConfiguration() {
|
||||
const raidType = this.raidTypeSelect.value;
|
||||
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
|
||||
const numDrives = isZFSVdev ?
|
||||
parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) :
|
||||
parseInt(this.numDrivesSlider.value);
|
||||
const config = RAID_CONFIGS[raidType];
|
||||
|
||||
if (numDrives < config.minDrives) {
|
||||
this.calculateBtn.disabled = true;
|
||||
this.calculateBtn.textContent = `Requires ${config.minDrives}+ drives`;
|
||||
} else if (raidType === 'raid10' && numDrives % 2 !== 0) {
|
||||
this.calculateBtn.disabled = true;
|
||||
this.calculateBtn.textContent = 'RAID 10 requires even number of drives';
|
||||
} else {
|
||||
this.calculateBtn.disabled = false;
|
||||
this.calculateBtn.innerHTML = `
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
Calculate Storage
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
getDriveSizes() {
|
||||
const raidType = this.raidTypeSelect.value;
|
||||
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
|
||||
const numDrives = isZFSVdev ?
|
||||
parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) :
|
||||
parseInt(this.numDrivesSlider.value);
|
||||
|
||||
if (this.mixedDrivesToggle.checked) {
|
||||
const drives = [];
|
||||
for (let i = 0; i < numDrives; i++) {
|
||||
const select = document.getElementById(`drive-${i}`);
|
||||
drives.push(parseFloat(select.value));
|
||||
}
|
||||
return drives;
|
||||
} else {
|
||||
const driveSize = parseFloat(this.driveSizeSelect.value);
|
||||
return Array(numDrives).fill(driveSize);
|
||||
}
|
||||
}
|
||||
|
||||
calculate() {
|
||||
try {
|
||||
const raidType = this.raidTypeSelect.value;
|
||||
const drives = this.getDriveSizes();
|
||||
const config = RAID_CONFIGS[raidType];
|
||||
|
||||
// Check if this is a ZFS vdev configuration
|
||||
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
|
||||
let result;
|
||||
|
||||
if (isZFSVdev) {
|
||||
const numVdevs = parseInt(this.numVdevsSlider.value);
|
||||
const drivesPerVdev = parseInt(this.drivesPerVdevSlider.value);
|
||||
result = calculateZFSVdevStorage(raidType, drives, numVdevs, drivesPerVdev);
|
||||
result.numVdevs = numVdevs;
|
||||
result.drivesPerVdev = drivesPerVdev;
|
||||
} else {
|
||||
result = calculateRAIDStorage(raidType, drives);
|
||||
}
|
||||
|
||||
this.displayResults(raidType, config, drives, result);
|
||||
} catch (error) {
|
||||
this.displayError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
displayResults(raidType, config, drives, result) {
|
||||
const { usableCapacity, rawCapacity, efficiency, wastedSpace } = result;
|
||||
|
||||
const html = `
|
||||
<div class="result-stats fade-in">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Usable Capacity</div>
|
||||
<div class="stat-value primary">${formatCapacity(usableCapacity)}</div>
|
||||
<div class="stat-subtext">Actual storage available for data</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Raw Capacity</div>
|
||||
<div class="stat-value">${formatCapacity(rawCapacity)}</div>
|
||||
<div class="stat-subtext">Total physical storage</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Storage Efficiency</div>
|
||||
<div class="stat-value">${formatPercentage(efficiency)}</div>
|
||||
<div class="stat-subtext">${formatCapacity(wastedSpace)} used for redundancy</div>
|
||||
</div>
|
||||
|
||||
<div class="capacity-visualization">
|
||||
<div class="capacity-bar">
|
||||
<div class="capacity-fill" style="width: ${efficiency * 100}%">
|
||||
${formatPercentage(efficiency)} Usable
|
||||
</div>
|
||||
</div>
|
||||
<div class="capacity-legend">
|
||||
<span>0 TiB</span>
|
||||
<span>${formatCapacity(rawCapacity, false)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="feature-list">
|
||||
<li class="feature-item">
|
||||
<svg class="feature-icon info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div class="feature-text">
|
||||
<div class="feature-title">${config.name}</div>
|
||||
<div class="feature-description">${config.description}</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="feature-item">
|
||||
<svg class="feature-icon ${config.faultTolerance > 0 ? 'success' : 'warning'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
<div class="feature-text">
|
||||
<div class="feature-title">Fault Tolerance</div>
|
||||
<div class="feature-description">
|
||||
${config.faultTolerance > 0
|
||||
? `Can survive ${config.faultTolerance} drive failure${config.faultTolerance > 1 ? 's' : ''} without data loss`
|
||||
: 'No redundancy - any drive failure results in data loss'}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="feature-item">
|
||||
<svg class="feature-icon info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
<div class="feature-text">
|
||||
<div class="feature-title">Performance</div>
|
||||
<div class="feature-description">
|
||||
Read: ${config.readPerformance} | Write: ${config.writePerformance}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="feature-item">
|
||||
<svg class="feature-icon success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="20" height="4" rx="1"/>
|
||||
<rect x="2" y="10" width="20" height="4" rx="1"/>
|
||||
<rect x="2" y="17" width="20" height="4" rx="1"/>
|
||||
</svg>
|
||||
<div class="feature-text">
|
||||
<div class="feature-title">Drive Configuration</div>
|
||||
<div class="feature-description">
|
||||
${drives.length} drive${drives.length > 1 ? 's' : ''}: ${this.formatDriveList(drives)}
|
||||
${result.numVdevs ? `<br><strong>${result.numVdevs} vdevs</strong> × ${result.drivesPerVdev} drives each` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="feature-item">
|
||||
<svg class="feature-icon info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/>
|
||||
</svg>
|
||||
<div class="feature-text">
|
||||
<div class="feature-title">Binary vs Decimal</div>
|
||||
<div class="feature-description">
|
||||
Calculations account for the difference between advertised TB (decimal) and actual TiB (binary) capacity
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.resultsContent.innerHTML = html;
|
||||
}
|
||||
|
||||
formatDriveList(drives) {
|
||||
const counts = {};
|
||||
drives.forEach(size => {
|
||||
counts[size] = (counts[size] || 0) + 1;
|
||||
});
|
||||
|
||||
return Object.entries(counts)
|
||||
.map(([size, count]) => `${count}x ${size}TB`)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
displayError(message) {
|
||||
this.resultsContent.innerHTML = `
|
||||
<div class="empty-state fade-in">
|
||||
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="stroke: var(--color-accent-danger);">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
<p style="color: var(--color-accent-danger);">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Initialize Application
|
||||
// ===================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new RAIDCalculator();
|
||||
});
|
||||
799
styles.css
Normal file
799
styles.css
Normal file
@@ -0,0 +1,799 @@
|
||||
/* ===================================
|
||||
Design System & CSS Variables
|
||||
=================================== */
|
||||
:root {
|
||||
/* Color Palette - Modern Dark Theme */
|
||||
--color-bg-primary: #0a0e1a;
|
||||
--color-bg-secondary: #111827;
|
||||
--color-bg-tertiary: #1a2332;
|
||||
--color-bg-card: rgba(30, 41, 59, 0.6);
|
||||
--color-bg-card-hover: rgba(30, 41, 59, 0.8);
|
||||
|
||||
/* Accent Colors */
|
||||
--color-accent-primary: #3b82f6;
|
||||
--color-accent-secondary: #8b5cf6;
|
||||
--color-accent-success: #10b981;
|
||||
--color-accent-warning: #f59e0b;
|
||||
--color-accent-danger: #ef4444;
|
||||
|
||||
/* Gradients */
|
||||
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
--gradient-success: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
--gradient-card: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
|
||||
/* Text Colors */
|
||||
--color-text-primary: #f8fafc;
|
||||
--color-text-secondary: #cbd5e1;
|
||||
--color-text-tertiary: #94a3b8;
|
||||
--color-text-muted: #64748b;
|
||||
|
||||
/* Border & Shadow */
|
||||
--color-border: rgba(148, 163, 184, 0.1);
|
||||
--color-border-focus: rgba(59, 130, 246, 0.5);
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||
--shadow-glow: 0 0 20px rgba(59, 130, 246, 0.3);
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.5rem;
|
||||
--spacing-sm: 0.75rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
|
||||
/* Typography */
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Base Styles & Reset
|
||||
=================================== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Animated Background */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 20%, rgba(16, 185, 129, 0.05) 0%, transparent 50%);
|
||||
z-index: -1;
|
||||
animation: backgroundShift 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes backgroundShift {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Layout Components
|
||||
=================================== */
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-xl) var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: 700;
|
||||
background: var(--gradient-secondary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
filter: drop-shadow(0 0 10px rgba(59, 130, 246, 0.5));
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 300;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* YouTube Link */
|
||||
.youtube-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: linear-gradient(135deg, #FF0000 0%, #CC0000 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
box-shadow: 0 2px 8px rgba(255, 0, 0, 0.3);
|
||||
transition: all var(--transition-base);
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.youtube-link:hover {
|
||||
transform: translateY(-2px) scale(1.03);
|
||||
box-shadow: 0 4px 15px rgba(255, 0, 0, 0.5);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.youtube-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 2px 8px rgba(255, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 2px 15px rgba(255, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
/* Card Component */
|
||||
.card {
|
||||
background: var(--color-bg-card);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: var(--color-bg-card-hover);
|
||||
box-shadow: var(--shadow-xl);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Calculator Grid */
|
||||
.calculator-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-xl);
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
.calculator-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Section Title */
|
||||
.section-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
color: var(--color-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 1.5rem;
|
||||
background: var(--gradient-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Form Components
|
||||
=================================== */
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Select Input */
|
||||
.select {
|
||||
width: 100%;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.select:hover {
|
||||
border-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-border-focus);
|
||||
}
|
||||
|
||||
.select option {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* Slider Input */
|
||||
.input-with-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 8px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
outline: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--gradient-secondary);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--gradient-secondary);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.value-display {
|
||||
min-width: 3rem;
|
||||
text-align: center;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.toggle-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 13px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--color-text-tertiary);
|
||||
border-radius: 50%;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.toggle-input:checked+.toggle-label {
|
||||
background: var(--gradient-secondary);
|
||||
border-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.toggle-input:checked+.toggle-label .toggle-slider {
|
||||
transform: translateX(24px);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.toggle-text {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Helper Text */
|
||||
.helper-text {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
margin-top: var(--spacing-xs);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Custom Drives Container */
|
||||
.custom-drives-container {
|
||||
margin-top: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.drive-inputs {
|
||||
display: grid;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.drive-input-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--spacing-md);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drive-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.drive-select {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
/* Primary Button */
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
background: var(--gradient-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all var(--transition-base);
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-glow), var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Results Panel
|
||||
=================================== */
|
||||
.results-content {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
text-align: center;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Result Stats */
|
||||
.result-stats {
|
||||
display: grid;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--color-bg-secondary);
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: var(--color-accent-primary);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-value.primary {
|
||||
background: var(--gradient-secondary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.stat-subtext {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Capacity Bar */
|
||||
.capacity-visualization {
|
||||
margin: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.capacity-bar {
|
||||
height: 40px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.capacity-fill {
|
||||
height: 100%;
|
||||
background: var(--gradient-success);
|
||||
transition: width var(--transition-slow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.capacity-legend {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* Feature List */
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
border-color: var(--color-accent-primary);
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.feature-icon.success {
|
||||
stroke: var(--color-accent-success);
|
||||
}
|
||||
|
||||
.feature-icon.warning {
|
||||
stroke: var(--color-accent-warning);
|
||||
}
|
||||
|
||||
.feature-icon.info {
|
||||
stroke: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Info Grid
|
||||
=================================== */
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-top: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.info-card {
|
||||
text-align: center;
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.info-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info-icon-blue {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.info-icon-green {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--color-accent-success);
|
||||
}
|
||||
|
||||
.info-icon-purple {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
color: var(--color-accent-secondary);
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Footer
|
||||
=================================== */
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: var(--spacing-2xl);
|
||||
padding: var(--spacing-xl) 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Animations
|
||||
=================================== */
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Responsive Design
|
||||
=================================== */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-3xl);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Utility Classes
|
||||
=================================== */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
Reference in New Issue
Block a user