Window & Door Header Calculator

Config format: { "title": "Calculator Name", "desc": "Brief description", "inputs": [ { "id":"w", "label":"Width", "unit":"ft", "default":10, "min":0, "step":0.5 }, { "id":"mat", "label":"Material", "type":"select", "options":[{"label":"Steel","value":7.85},{"label":"Aluminum","value":2.71}], "default":7.85 } ], "outputs": [ { "id":"area", "label":"Area", "unit":"sq ft", "formula":"w * l", "primary":true }, { "id":"perim", "label":"Perimeter", "unit":"ft", "formula":"2*(w+l)" } ], "formula_display": "Area = Width × Length", "notes": "Optional notes or reference text" } ============================================================ */ (function() { 'use strict'; var _uid = 0; /* ---- Number formatter ---- */ function fmt(n, unit) { if (n === null || n === undefined || isNaN(n)) return '—'; if (!isFinite(n)) return n > 0 ? '∞' : '-∞'; var abs = Math.abs(n); var str; if (abs === 0) { str = '0'; } else if (abs >= 1e6) { str = n.toExponential(3); } else if (abs >= 10000){ str = n.toFixed(1); } else if (abs >= 1000) { str = n.toFixed(2); } else if (abs >= 100) { str = n.toFixed(3); } else if (abs >= 10) { str = n.toFixed(4); } else if (abs >= 1) { str = n.toFixed(4); } else if (abs >= 0.01) { str = n.toFixed(5); } else { str = n.toExponential(3); } // Strip trailing zeros after decimal if (str.indexOf('.') !== -1 && str.indexOf('e') === -1) { str = str.replace(/\.?0+$/, ''); } return unit ? str + ' ' + unit : str; } /* ---- Safe formula evaluator ---- */ var MATH_SCOPE = (function() { var s = {}; ['abs','ceil','floor','round','sqrt','cbrt','pow','log','log2','log10', 'sin','cos','tan','asin','acos','atan','atan2','sinh','cosh','tanh', 'min','max','sign','trunc','hypot','exp'].forEach(function(f) { s[f] = Math[f]; }); s['PI'] = Math.PI; s['E'] = Math.E; s['LN2'] = Math.LN2; s['LN10'] = Math.LN10; return s; })(); function evalFormula(formula, vars) { try { var scope = Object.assign({}, MATH_SCOPE, vars); var expr = formula.replace(/\^/g, '**'); var fn = new Function(Object.keys(scope), '"use strict"; return (' + expr + ');'); var result = fn.apply(null, Object.values(scope)); return (typeof result === 'number') ? result : NaN; } catch(e) { return NaN; } } /* ---- HTML builder ---- */ function esc(s) { return String(s) .replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function buildUI(uid, cfg) { var inputs = cfg.inputs || []; var outputs = cfg.outputs || []; var primary = outputs.filter(function(o){ return o.primary; }); var secondary = outputs.filter(function(o){ return !o.primary; }); var inputsHTML = inputs.map(function(inp) { var id = 'prc_' + uid + '_' + inp.id; var field = ''; if (inp.type === 'select') { var opts = (inp.options || []).map(function(o) { var sel = (String(o.value) === String(inp.default)) ? ' selected' : ''; return ''; }).join(''); field = ''; } else { var attrs = ''; if (inp.min !== undefined) attrs += ' min="' + esc(inp.min) + '"'; if (inp.max !== undefined) attrs += ' max="' + esc(inp.max) + '"'; if (inp.step !== undefined) attrs += ' step="' + esc(inp.step) + '"'; var val = (inp.default !== undefined) ? inp.default : ''; field = ''; } var unitLabel = inp.unit ? ' ' + esc(inp.unit) + '' : ''; return '
' + field + unitLabel + '
'; }).join(''); var primaryHTML = ''; if (primary.length > 0) { var p = primary[0]; primaryHTML = '
' + esc(p.label) + '
' + '
' + (p.unit ? ' ' + esc(p.unit) + '' : '') + '
'; } else if (outputs.length > 0) { // Make first output the primary if none marked var p0 = outputs[0]; primaryHTML = '
' + esc(p0.label) + '
' + '
' + (p0.unit ? ' ' + esc(p0.unit) + '' : '') + '
'; secondary = outputs.slice(1); } var secHTML = ''; if (secondary.length > 0) { secHTML = '
' + secondary.map(function(o) { return '
' + esc(o.label) + '
' + '
' + (o.unit ? '
' + esc(o.unit) + '
' : '') + '
'; }).join('') + '
'; } var formulaHTML = cfg.formula_display ? '
' + esc(cfg.formula_display) + '
' : ''; var notesHTML = cfg.notes ? '
' + esc(cfg.notes) + '
' : ''; return '
' + '
' + '

' + esc(cfg.title || 'Calculator') + '

' + (cfg.desc ? '

' + esc(cfg.desc) + '

' : '') + '
' + '
' + '
' + '
' + '' + inputsHTML + '
' + '
' + '' + primaryHTML + secHTML + formulaHTML + notesHTML + '
' + '
' + '
' + '
'; } /* ---- Calculation runner ---- */ function runCalc(uid, cfg) { var inputs = cfg.inputs || []; var outputs = cfg.outputs || []; var vars = {}; inputs.forEach(function(inp) { var el = document.getElementById('prc_' + uid + '_' + inp.id); if (!el) return; vars[inp.id] = parseFloat(el.value); if (isNaN(vars[inp.id])) vars[inp.id] = inp.default || 0; }); outputs.forEach(function(out) { var el = document.getElementById('prc_' + uid + '_out_' + out.id); if (!el) return; var result = evalFormula(out.formula, vars); var precision = out.precision !== undefined ? out.precision : null; var display; if (isNaN(result) || !isFinite(result)) { display = isFinite(result) ? '—' : (result > 0 ? '∞' : '-∞'); } else if (precision !== null) { display = result.toFixed(precision); } else { // Smart formatting: strip unit from fmt() result display = fmt(result); } el.textContent = display; }); } /* ---- Public API ---- */ window.PrcEngine = { _configs: {}, calc: function(uid) { var cfg = this._configs[uid]; if (cfg) runCalc(uid, cfg); }, init: function(el) { var cfg; try { cfg = JSON.parse(el.getAttribute('data-config')); } catch(e) { el.innerHTML = '

Invalid calculator config

'; return; } var uid = 'c' + (++_uid); this._configs[uid] = cfg; el.innerHTML = buildUI(uid, cfg); runCalc(uid, cfg); } }; /* ---- Auto-init on DOMContentLoaded ---- */ function initAll() { document.querySelectorAll('.pr-calc-auto[data-config]').forEach(function(el) { PrcEngine.init(el); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAll); } else { initAll(); } /* ---- Extra CSS for engine-specific elements ---- */ var style = document.createElement('style'); style.textContent = [ '.pr-field { margin-bottom: 14px; }', '.pr-field label { display:block; font-size:.75em; color:var(--pr-muted); text-transform:uppercase; letter-spacing:.06em; margin-bottom:4px; }', '.pr-field-input { width:100%; background:var(--pr-bg); color:var(--pr-text); border:1px solid var(--pr-border); padding:8px 10px; font-family:var(--pr-mono); font-size:.95em; box-sizing:border-box; border-radius:0; }', '.pr-field-input:focus { border-color:var(--pr-accent); outline:none; }', 'select.pr-field-input option { background:var(--pr-surface); color:var(--pr-text); }', '.pr-field-unit { font-size:.75em; color:var(--pr-muted); margin-left:6px; }', '.pr-result-main { background:var(--pr-surface2); border:1px solid var(--pr-border); border-left:3px solid var(--pr-green); padding:16px; margin-bottom:10px; }', '.pr-result-main .label { font-size:.7em; color:var(--pr-muted); text-transform:uppercase; letter-spacing:.08em; margin-bottom:4px; }', '.pr-result-main .value { font-size:2.2em; color:var(--pr-green); font-family:var(--pr-mono); line-height:1.1; }', '.pr-result-main .unit { font-size:.85em; color:var(--pr-muted); margin-left:4px; }', '.pr-result-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:8px; margin-top:8px; }', '.pr-result-item { background:var(--pr-surface2); border:1px solid var(--pr-border); padding:10px 12px; }', '.pr-result-item .label { font-size:.68em; color:var(--pr-muted); text-transform:uppercase; letter-spacing:.06em; margin-bottom:2px; }', '.pr-result-item .value { font-size:1.15em; color:var(--pr-accent); font-family:var(--pr-mono); word-break:break-all; }', '.pr-formula { background:var(--pr-bg); border:1px solid var(--pr-border); border-left:3px solid var(--pr-muted); padding:10px 14px; font-size:.8em; color:var(--pr-muted); margin-top:12px; }', '.pr-note { font-size:.75em; color:var(--pr-muted); border-top:1px solid var(--pr-border); margin-top:12px; padding-top:10px; line-height:1.5; }', '.pr-calc-grid { display:grid; grid-template-columns:1fr 1fr; gap:28px; }', '@media(max-width:640px){ .pr-calc-grid { grid-template-columns:1fr; } }', ].join('\n'); document.head.appendChild(style); })();