feat: Add WFW-Aushang web app with PWA support, offline caching, and dark mode
- Created index.html for the main application interface with responsive design and dark mode support. - Added manifest.webmanifest for PWA configuration, including app icons and display settings. - Implemented service worker (sw.js) for offline caching of assets and network-first strategy for versioning. - Introduced version.json to manage app versioning.
This commit is contained in:
BIN
CableCalc/icon.png
Normal file
BIN
CableCalc/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
481
CableCalc/index.php
Normal file
481
CableCalc/index.php
Normal file
@@ -0,0 +1,481 @@
|
||||
<?php
|
||||
// index.php - simple cable size calculator
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>CableCalc – Cable Size Calculator</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f4f4;
|
||||
--card-bg: #ffffff;
|
||||
--text: #222;
|
||||
--text-muted: #555;
|
||||
--border: #ccc;
|
||||
--accent: #0078d7;
|
||||
--result-bg: #f0f7ff;
|
||||
--result-border: #c5ddff;
|
||||
--error-bg: #ffecec;
|
||||
--error-border: #ffb3b3;
|
||||
}
|
||||
|
||||
/* DARK MODE VARIABLES */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #111;
|
||||
--card-bg: #1b1b1b;
|
||||
--text: #eee;
|
||||
--text-muted: #bbb;
|
||||
--border: #444;
|
||||
--accent: #3ea0ff;
|
||||
--result-bg: #102233;
|
||||
--result-border: #1a3a57;
|
||||
--error-bg: #331111;
|
||||
--error-border: #883333;
|
||||
}
|
||||
}
|
||||
|
||||
/* USER OVERRIDE – DARK */
|
||||
body.dark {
|
||||
--bg: #111;
|
||||
--card-bg: #1b1b1b;
|
||||
--text: #eee;
|
||||
--text-muted: #bbb;
|
||||
--border: #444;
|
||||
--accent: #3ea0ff;
|
||||
--result-bg: #102233;
|
||||
--result-border: #1a3a57;
|
||||
--error-bg: #331111;
|
||||
--error-border: #883333;
|
||||
}
|
||||
|
||||
/* USER OVERRIDE – LIGHT */
|
||||
body.light {
|
||||
--bg: #f4f4f4;
|
||||
--card-bg: #ffffff;
|
||||
--text: #222;
|
||||
--text-muted: #555;
|
||||
--border: #ccc;
|
||||
--accent: #0078d7;
|
||||
--result-bg: #f0f7ff;
|
||||
--result-border: #c5ddff;
|
||||
--error-bg: #ffecec;
|
||||
--error-border: #ffb3b3;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
transition: background 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 20px auto 40px;
|
||||
background: var(--card-bg);
|
||||
padding: 20px 24px 28px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.20);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
gap: 16px 24px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card-bg);
|
||||
color: var(--text);
|
||||
transition: background 0.3s, color 0.3s, border 0.3s;
|
||||
}
|
||||
|
||||
.inline-options label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 12px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--result-bg);
|
||||
border: 1px solid var(--result-border);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--error-bg);
|
||||
border: 1px solid var(--error-border);
|
||||
color: #ff8080;
|
||||
}
|
||||
|
||||
.notes {
|
||||
margin-top: 14px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
background: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Toggle button */
|
||||
#modeToggle {
|
||||
float: right;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: var(--border);
|
||||
color: var(--text);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>CableCalc <span class="badge">beta</span></h1>
|
||||
<p style="font-size:0.9rem; color:#555;">
|
||||
Enter your data – the calculator suggests the smallest copper cable size that keeps current
|
||||
within a basic ampacity and voltage drop limit.
|
||||
</p>
|
||||
|
||||
<div class="grid">
|
||||
<!-- AC / DC -->
|
||||
<div class="field">
|
||||
<label for="acdc">AC / DC</label>
|
||||
<select id="acdc">
|
||||
<option value="AC">AC</option>
|
||||
<option value="DC">DC</option>
|
||||
</select>
|
||||
<small>AC/DC is currently only shown for reference; calculation uses the same resistive model.</small>
|
||||
</div>
|
||||
|
||||
<!-- Voltage -->
|
||||
<div class="field">
|
||||
<label for="voltage">Voltage [V]</label>
|
||||
<input type="number" id="voltage" min="0" step="0.1" value="230">
|
||||
</div>
|
||||
|
||||
<!-- Current / Power selector -->
|
||||
<div class="field">
|
||||
<label>What is known?</label>
|
||||
<div class="inline-options">
|
||||
<label>
|
||||
<input type="radio" name="loadType" value="current" checked>
|
||||
Current (A) known
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="loadType" value="power">
|
||||
Power (W) known
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current input -->
|
||||
<div class="field" id="field-current">
|
||||
<label for="current">Current [A]</label>
|
||||
<input type="number" id="current" min="0" step="0.01">
|
||||
</div>
|
||||
|
||||
<!-- Power input -->
|
||||
<div class="field" id="field-power" style="display:none;">
|
||||
<label for="power">Power [W]</label>
|
||||
<input type="number" id="power" min="0" step="1">
|
||||
<small>Assumes cos φ / power factor = 1.0 for simplicity.</small>
|
||||
</div>
|
||||
|
||||
<!-- Length -->
|
||||
<div class="field">
|
||||
<label for="length">Cable length (one way)</label>
|
||||
<input type="number" id="length" min="0" step="0.1" value="10">
|
||||
</div>
|
||||
|
||||
<!-- Length unit -->
|
||||
<div class="field">
|
||||
<label for="lengthUnit">Length unit</label>
|
||||
<select id="lengthUnit">
|
||||
<option value="m">Meters</option>
|
||||
<option value="ft">Feet</option>
|
||||
</select>
|
||||
<small>Voltage drop is calculated over the round-trip (out & back).</small>
|
||||
</div>
|
||||
|
||||
<!-- Max voltage drop -->
|
||||
<div class="field">
|
||||
<label for="maxDrop">Max voltage drop [%]</label>
|
||||
<input type="number" id="maxDrop" min="0.5" step="0.1" value="3.0">
|
||||
</div>
|
||||
|
||||
<!-- Output units -->
|
||||
<div class="field">
|
||||
<label for="unitSystem">Preferred output</label>
|
||||
<select id="unitSystem">
|
||||
<option value="mixed">AWG + mm²</option>
|
||||
<option value="awg">AWG only</option>
|
||||
<option value="mm2">mm² only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" onclick="calculateCable()">Calculate cable size</button>
|
||||
|
||||
<div id="error" class="error" style="display:none;"></div>
|
||||
<div id="result" class="result" style="display:none;"></div>
|
||||
|
||||
<div id="modeToggle" onclick="toggleMode()">🌓</div>
|
||||
|
||||
<div class="notes">
|
||||
<strong>Notes & limitations:</strong>
|
||||
<ul>
|
||||
<li>Assumes copper conductors, single circuit, free air / light installation.</li>
|
||||
<li>Uses generic ampacity values (conservative but not a substitute for local electrical codes).</li>
|
||||
<li>For mains or critical installations always have a qualified electrician verify the result.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cable data: copper, approx. resistance at 20°C and conservative ampacity in A.
|
||||
// Values are generic and simplified – not a substitute for national standards.
|
||||
const cableSizes = [
|
||||
// AWG, mm², diameter(mm, bare conductor approx), R[ohm/km], ampacity[A]
|
||||
{ awg: 18, mm2: 0.82, diameter: 1.02, r_km: 21.0, amp: 14 },
|
||||
{ awg: 16, mm2: 1.31, diameter: 1.29, r_km: 13.3, amp: 18 },
|
||||
{ awg: 14, mm2: 2.08, diameter: 1.63, r_km: 8.4, amp: 24 },
|
||||
{ awg: 12, mm2: 3.31, diameter: 2.05, r_km: 5.3, amp: 32 },
|
||||
{ awg: 10, mm2: 5.26, diameter: 2.59, r_km: 3.3, amp: 45 },
|
||||
{ awg: 8, mm2: 8.37, diameter: 3.26, r_km: 2.1, amp: 60 },
|
||||
{ awg: 6, mm2: 13.3, diameter: 4.11, r_km: 1.3, amp: 80 },
|
||||
{ awg: 4, mm2: 21.1, diameter: 5.19, r_km: 0.8, amp: 105 },
|
||||
{ awg: 2, mm2: 33.6, diameter: 6.54, r_km: 0.51, amp: 140 },
|
||||
{ awg: 1, mm2: 42.4, diameter: 7.35, r_km: 0.40, amp: 165 },
|
||||
{ awg: 0, mm2: 53.5, diameter: 8.25, r_km: 0.32, amp: 195 }
|
||||
];
|
||||
|
||||
// Toggle current / power fields
|
||||
document.querySelectorAll('input[name="loadType"]').forEach(r => {
|
||||
r.addEventListener('change', function () {
|
||||
const type = this.value;
|
||||
document.getElementById('field-current').style.display = (type === 'current') ? 'block' : 'none';
|
||||
document.getElementById('field-power').style.display = (type === 'power') ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
function showError(msg) {
|
||||
const err = document.getElementById('error');
|
||||
err.textContent = msg;
|
||||
err.style.display = 'block';
|
||||
document.getElementById('result').style.display = 'none';
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
const err = document.getElementById('error');
|
||||
err.style.display = 'none';
|
||||
err.textContent = '';
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
if (document.body.classList.contains("dark")) {
|
||||
document.body.classList.remove("dark");
|
||||
document.body.classList.add("light");
|
||||
localStorage.setItem("cablecalc-theme", "light");
|
||||
} else if (document.body.classList.contains("light")) {
|
||||
document.body.classList.remove("light");
|
||||
document.body.classList.add("dark");
|
||||
localStorage.setItem("cablecalc-theme", "dark");
|
||||
} else {
|
||||
// No override yet → force dark
|
||||
document.body.classList.add("dark");
|
||||
localStorage.setItem("cablecalc-theme", "dark");
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved preference
|
||||
(function() {
|
||||
const saved = localStorage.getItem("cablecalc-theme");
|
||||
if (saved === "dark") document.body.classList.add("dark");
|
||||
if (saved === "light") document.body.classList.add("light");
|
||||
})();
|
||||
|
||||
function calculateCable() {
|
||||
clearError();
|
||||
const voltage = parseFloat(document.getElementById('voltage').value);
|
||||
const lengthOneWay = parseFloat(document.getElementById('length').value);
|
||||
const lengthUnit = document.getElementById('lengthUnit').value;
|
||||
const maxDropPct = parseFloat(document.getElementById('maxDrop').value);
|
||||
const unitSystem = document.getElementById('unitSystem').value;
|
||||
const loadType = document.querySelector('input[name="loadType"]:checked').value;
|
||||
|
||||
if (isNaN(voltage) || voltage <= 0) {
|
||||
showError('Please enter a valid voltage.');
|
||||
return;
|
||||
}
|
||||
if (isNaN(lengthOneWay) || lengthOneWay <= 0) {
|
||||
showError('Please enter a valid cable length.');
|
||||
return;
|
||||
}
|
||||
if (isNaN(maxDropPct) || maxDropPct <= 0) {
|
||||
showError('Please enter a valid maximum voltage drop percentage.');
|
||||
return;
|
||||
}
|
||||
|
||||
let current;
|
||||
if (loadType === 'current') {
|
||||
current = parseFloat(document.getElementById('current').value);
|
||||
if (isNaN(current) || current <= 0) {
|
||||
showError('Please enter a valid current.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const power = parseFloat(document.getElementById('power').value);
|
||||
if (isNaN(power) || power <= 0) {
|
||||
showError('Please enter a valid power.');
|
||||
return;
|
||||
}
|
||||
// Power factor assumed 1.0
|
||||
current = power / voltage;
|
||||
}
|
||||
|
||||
// Convert length to meters (one way)
|
||||
let length_m = lengthOneWay;
|
||||
if (lengthUnit === 'ft') {
|
||||
length_m = lengthOneWay * 0.3048;
|
||||
}
|
||||
|
||||
// Total circuit length = out + back
|
||||
const totalLength_m = length_m * 2;
|
||||
|
||||
const maxDrop_V = voltage * (maxDropPct / 100.0);
|
||||
|
||||
let chosen = null;
|
||||
let worstCase = null; // largest size if nothing meets both criteria
|
||||
|
||||
for (let i = 0; i < cableSizes.length; i++) {
|
||||
const c = cableSizes[i];
|
||||
|
||||
const rPerMeter = c.r_km / 1000.0; // ohm/m
|
||||
const rTotal = rPerMeter * totalLength_m; // ohm
|
||||
const vDrop = current * rTotal; // V
|
||||
const vDropPct = (vDrop / voltage) * 100;
|
||||
|
||||
const okAmpacity = current <= c.amp;
|
||||
const okDrop = vDrop <= maxDrop_V;
|
||||
|
||||
// Remember largest size for fallback
|
||||
worstCase = {
|
||||
cable: c,
|
||||
vDrop: vDrop,
|
||||
vDropPct: vDropPct,
|
||||
okAmpacity: okAmpacity,
|
||||
okDrop: okDrop
|
||||
};
|
||||
|
||||
if (okAmpacity && okDrop) {
|
||||
chosen = {
|
||||
cable: c,
|
||||
vDrop: vDrop,
|
||||
vDropPct: vDropPct
|
||||
};
|
||||
break; // smallest that satisfies
|
||||
}
|
||||
}
|
||||
|
||||
const res = document.getElementById('result');
|
||||
res.style.display = 'block';
|
||||
|
||||
if (!chosen && worstCase) {
|
||||
const c = worstCase.cable;
|
||||
const vDrop = worstCase.vDrop;
|
||||
const vDropPct = worstCase.vDropPct;
|
||||
|
||||
res.innerHTML = `
|
||||
<strong>No cable size in the built-in table fully meets the limits.</strong><br><br>
|
||||
Suggested <em>minimum</em> size (table maximum):<br>
|
||||
AWG ${c.awg}, approx. ${c.mm2.toFixed(1)} mm², conductor diameter ≈ ${c.diameter.toFixed(2)} mm<br>
|
||||
Ampacity (approx.): ${c.amp} A<br>
|
||||
Estimated voltage drop: ${vDrop.toFixed(2)} V (${vDropPct.toFixed(2)} %)<br><br>
|
||||
<span style="color:#a40000;">Warning:</span> Either the desired voltage drop or ampacity is exceeded.
|
||||
Consider using a larger cable than listed here or shortening the run.
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chosen) {
|
||||
showError('No valid cable size found. Please check inputs.');
|
||||
return;
|
||||
}
|
||||
|
||||
const c = chosen.cable;
|
||||
const vDrop = chosen.vDrop;
|
||||
const vDropPct = chosen.vDropPct;
|
||||
|
||||
let mainLine = '';
|
||||
if (unitSystem === 'awg') {
|
||||
mainLine = `Recommended cable size: <strong>AWG ${c.awg}</strong>`;
|
||||
} else if (unitSystem === 'mm2') {
|
||||
mainLine = `Recommended cable size: <strong>${c.mm2.toFixed(1)} mm²</strong>`;
|
||||
} else {
|
||||
mainLine = `Recommended cable size: <strong>AWG ${c.awg} (~${c.mm2.toFixed(1)} mm²)</strong>`;
|
||||
}
|
||||
|
||||
res.innerHTML = `
|
||||
${mainLine}<br>
|
||||
Conductor diameter (approx.): <strong>${c.diameter.toFixed(2)} mm</strong><br>
|
||||
Ampacity (approx.): <strong>${c.amp} A</strong><br>
|
||||
Estimated voltage drop over ${length_m.toFixed(1)} m one-way (round-trip ${totalLength_m.toFixed(1)} m):<br>
|
||||
<strong>${vDrop.toFixed(2)} V (${vDropPct.toFixed(2)} %)</strong>
|
||||
`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user