| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var _typoKeys = { |
| 5 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 6 |
decoration:'text-decoration', transform:'text-transform', |
| 7 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 8 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 9 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 10 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 11 |
}; |
| 12 |
function typoCssVarsForEl(el, obj, prefix) { |
| 13 |
if (!obj || typeof obj !== 'object') return; |
| 14 |
Object.keys(_typoKeys).forEach(function (k) { |
| 15 |
var v = obj[k]; |
| 16 |
if (v === undefined || v === '' || v === null) return; |
| 17 |
if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px'); |
| 18 |
else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || ''); |
| 19 |
else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px'); |
| 20 |
else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px'); |
| 21 |
el.style.setProperty(prefix + _typoKeys[k], String(v)); |
| 22 |
}); |
| 23 |
} |
| 24 |
|
| 25 |
var BRACKETS = { |
| 26 |
single: [ |
| 27 |
{ lo: 0, hi: 11925, rate: 0.10 }, |
| 28 |
{ lo: 11925, hi: 48475, rate: 0.12 }, |
| 29 |
{ lo: 48475, hi: 103350, rate: 0.22 }, |
| 30 |
{ lo: 103350, hi: 197300, rate: 0.24 }, |
| 31 |
{ lo: 197300, hi: 250525, rate: 0.32 }, |
| 32 |
{ lo: 250525, hi: 626350, rate: 0.35 }, |
| 33 |
{ lo: 626350, hi: Infinity, rate: 0.37 } |
| 34 |
], |
| 35 |
married: [ |
| 36 |
{ lo: 0, hi: 23850, rate: 0.10 }, |
| 37 |
{ lo: 23850, hi: 96950, rate: 0.12 }, |
| 38 |
{ lo: 96950, hi: 206700, rate: 0.22 }, |
| 39 |
{ lo: 206700, hi: 394600, rate: 0.24 }, |
| 40 |
{ lo: 394600, hi: 501050, rate: 0.32 }, |
| 41 |
{ lo: 501050, hi: 751600, rate: 0.35 }, |
| 42 |
{ lo: 751600, hi: Infinity, rate: 0.37 } |
| 43 |
], |
| 44 |
hoh: [ |
| 45 |
{ lo: 0, hi: 17000, rate: 0.10 }, |
| 46 |
{ lo: 17000, hi: 64850, rate: 0.12 }, |
| 47 |
{ lo: 64850, hi: 103350, rate: 0.22 }, |
| 48 |
{ lo: 103350, hi: 197300, rate: 0.24 }, |
| 49 |
{ lo: 197300, hi: 250500, rate: 0.32 }, |
| 50 |
{ lo: 250500, hi: 626350, rate: 0.35 }, |
| 51 |
{ lo: 626350, hi: Infinity, rate: 0.37 } |
| 52 |
] |
| 53 |
}; |
| 54 |
var STD_DED = { single: 15000, married: 30000, hoh: 22500 }; |
| 55 |
var SS_CAP = 176100; |
| 56 |
|
| 57 |
function calcFedTax(taxable, filing) { |
| 58 |
var brackets = BRACKETS[filing] || BRACKETS.single; |
| 59 |
var tax = 0; |
| 60 |
for (var i = 0; i < brackets.length; i++) { |
| 61 |
var b = brackets[i]; |
| 62 |
if (taxable <= b.lo) break; |
| 63 |
tax += (Math.min(taxable, b.hi) - b.lo) * b.rate; |
| 64 |
} |
| 65 |
return Math.max(0, tax); |
| 66 |
} |
| 67 |
|
| 68 |
function toAnnual(val, period) { |
| 69 |
if (period === 'monthly') return val * 12; |
| 70 |
if (period === 'hourly') return val * 2080; |
| 71 |
return val; |
| 72 |
} |
| 73 |
|
| 74 |
function calcAll(income, period, filing, stateTaxPct, k401Pct, otherDed) { |
| 75 |
var gross = toAnnual(parseFloat(income) || 0, period); |
| 76 |
var ded401k = gross * ((parseFloat(k401Pct) || 0) / 100); |
| 77 |
var other = parseFloat(otherDed) || 0; |
| 78 |
var preTax = ded401k + other; |
| 79 |
var taxable = Math.max(0, gross - preTax - (STD_DED[filing] || 15000)); |
| 80 |
var fedTax = calcFedTax(taxable, filing); |
| 81 |
var stateTax = (gross - preTax) * ((parseFloat(stateTaxPct) || 0) / 100); |
| 82 |
var ss = Math.min(gross, SS_CAP) * 0.062; |
| 83 |
var medicare = gross * 0.0145; |
| 84 |
var totalTax = fedTax + stateTax + ss + medicare; |
| 85 |
var netAnnual = Math.max(0, gross - totalTax - preTax); |
| 86 |
return { |
| 87 |
gross: gross, preTax: preTax, ded401k: ded401k, otherDed: other, |
| 88 |
fedTax: fedTax, stateTax: stateTax, ss: ss, medicare: medicare, |
| 89 |
totalTax: totalTax, netAnnual: netAnnual, |
| 90 |
effectiveRate: gross > 0 ? (totalTax / gross) * 100 : 0 |
| 91 |
}; |
| 92 |
} |
| 93 |
|
| 94 |
function fmt(n, cur) { |
| 95 |
return (cur || '$') + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| 96 |
} |
| 97 |
|
| 98 |
function pct(n, base) { |
| 99 |
return base > 0 ? Math.min(100, (n / base) * 100) : 0; |
| 100 |
} |
| 101 |
|
| 102 |
document.querySelectorAll('.bkbg-slc-app').forEach(function (root) { |
| 103 |
var opts = {}; |
| 104 |
try { opts = JSON.parse(root.getAttribute('data-opts') || '{}'); } catch(e) {} |
| 105 |
|
| 106 |
var cur = opts.currency || '$'; |
| 107 |
var showBreakdown = opts.showBreakdown !== false; |
| 108 |
var showAllPeriods = opts.showAllPeriods !== false; |
| 109 |
var showEffective = opts.showEffectiveRate !== false; |
| 110 |
var accentColor = opts.accentColor || '#0ea5e9'; |
| 111 |
var taxColor = opts.taxColor || '#ef4444'; |
| 112 |
var dedColor = opts.dedColor || '#f59e0b'; |
| 113 |
var sectionBg = opts.sectionBg || '#f0f9ff'; |
| 114 |
var cardBg = opts.cardBg || '#ffffff'; |
| 115 |
var inputBg = opts.inputBg || '#f9fafb'; |
| 116 |
var resultBg = opts.resultBg || '#e0f2fe'; |
| 117 |
var titleColor = opts.titleColor || '#0c4a6e'; |
| 118 |
var subtitleColor = opts.subtitleColor || '#6b7280'; |
| 119 |
var labelColor = opts.labelColor || '#374151'; |
| 120 |
|
| 121 |
var state = { |
| 122 |
income: opts.defaultIncome || 75000, |
| 123 |
period: opts.defaultPeriod || 'annual', |
| 124 |
filing: opts.defaultFiling || 'single', |
| 125 |
stateTax: opts.defaultStateTax != null ? opts.defaultStateTax : 5, |
| 126 |
k401: opts.default401k != null ? opts.default401k : 6, |
| 127 |
otherDed: opts.defaultOtherDed || 0 |
| 128 |
}; |
| 129 |
|
| 130 |
var FILING_LABELS = { single: 'Single', married: 'Married (Joint)', hoh: 'Head of Household' }; |
| 131 |
var PERIOD_LABELS = { annual: 'Annual', monthly: 'Monthly', hourly: 'Hourly' }; |
| 132 |
|
| 133 |
function brow(label, val, barPct, color) { |
| 134 |
return '<div class="bkbg-slc-brow">' + |
| 135 |
'<span class="bkbg-slc-brow-label" style="color:' + labelColor + ';">' + label + '</span>' + |
| 136 |
'<div class="bkbg-slc-brow-bar-wrap"><div class="bkbg-slc-brow-bar" style="width:' + barPct.toFixed(1) + '%;background:' + color + ';"></div></div>' + |
| 137 |
'<span class="bkbg-slc-brow-val" style="color:' + color + ';">' + val + '</span>' + |
| 138 |
'</div>'; |
| 139 |
} |
| 140 |
|
| 141 |
function render() { |
| 142 |
var r = calcAll(state.income, state.period, state.filing, state.stateTax, state.k401, state.otherDed); |
| 143 |
|
| 144 |
root.innerHTML = |
| 145 |
'<div class="bkbg-slc-card" style="background:' + cardBg + ';max-width:' + (opts.contentMaxWidth || 720) + 'px;">' + |
| 146 |
'<h2 class="bkbg-slc-title" style="color:' + titleColor + ';">' + (opts.title || 'Salary & Take-Home Pay Calculator') + '</h2>' + |
| 147 |
'<p class="bkbg-slc-subtitle" style="color:' + subtitleColor + ';">' + (opts.subtitle || 'Estimate your net pay after taxes and deductions') + '</p>' + |
| 148 |
|
| 149 |
'<div class="bkbg-slc-inputs" style="background:' + sectionBg + ';">' + |
| 150 |
'<div class="bkbg-slc-row">' + |
| 151 |
'<div class="bkbg-slc-field">' + |
| 152 |
'<label class="bkbg-slc-label" style="color:' + labelColor + ';">Income</label>' + |
| 153 |
'<div class="bkbg-slc-input-wrap">' + |
| 154 |
'<span class="bkbg-slc-prefix">' + cur + '</span>' + |
| 155 |
'<input type="number" class="bkbg-slc-input" id="slc-income" value="' + state.income + '" style="background:' + inputBg + ';padding-left:28px;" min="0" step="1000">' + |
| 156 |
'</div>' + |
| 157 |
'</div>' + |
| 158 |
'<div class="bkbg-slc-field">' + |
| 159 |
'<label class="bkbg-slc-label" style="color:' + labelColor + ';">Pay Period</label>' + |
| 160 |
'<select class="bkbg-slc-select" id="slc-period" style="background:' + inputBg + ';">' + |
| 161 |
'<option value="annual"' + (state.period==='annual' ? ' selected':'' ) + '>Annual</option>' + |
| 162 |
'<option value="monthly"' + (state.period==='monthly' ? ' selected':'' ) + '>Monthly</option>' + |
| 163 |
'<option value="hourly"' + (state.period==='hourly' ? ' selected':'' ) + '>Hourly</option>' + |
| 164 |
'</select>' + |
| 165 |
'</div>' + |
| 166 |
'<div class="bkbg-slc-field">' + |
| 167 |
'<label class="bkbg-slc-label" style="color:' + labelColor + ';">Filing Status</label>' + |
| 168 |
'<select class="bkbg-slc-select" id="slc-filing" style="background:' + inputBg + ';">' + |
| 169 |
'<option value="single"' + (state.filing==='single' ? ' selected':'') + '>Single</option>' + |
| 170 |
'<option value="married"' + (state.filing==='married' ? ' selected':'') + '>Married (Joint)</option>' + |
| 171 |
'<option value="hoh"' + (state.filing==='hoh' ? ' selected':'') + '>Head of Household</option>' + |
| 172 |
'</select>' + |
| 173 |
'</div>' + |
| 174 |
'</div>' + |
| 175 |
'<div class="bkbg-slc-row">' + |
| 176 |
'<div class="bkbg-slc-field">' + |
| 177 |
'<label class="bkbg-slc-label" style="color:' + labelColor + ';">State Tax Rate (%)</label>' + |
| 178 |
'<input type="number" class="bkbg-slc-input" id="slc-statetax" value="' + state.stateTax + '" style="background:' + inputBg + ';" min="0" max="20" step="0.1">' + |
| 179 |
'</div>' + |
| 180 |
'<div class="bkbg-slc-field">' + |
| 181 |
'<label class="bkbg-slc-label" style="color:' + labelColor + ';">401(k) Contribution (%)</label>' + |
| 182 |
'<input type="number" class="bkbg-slc-input" id="slc-401k" value="' + state.k401 + '" style="background:' + inputBg + ';" min="0" max="30" step="0.5">' + |
| 183 |
'</div>' + |
| 184 |
'<div class="bkbg-slc-field">' + |
| 185 |
'<label class="bkbg-slc-label" style="color:' + labelColor + ';">Other Deductions (' + cur + ')</label>' + |
| 186 |
'<input type="number" class="bkbg-slc-input" id="slc-other" value="' + state.otherDed + '" style="background:' + inputBg + ';" min="0" step="100">' + |
| 187 |
'</div>' + |
| 188 |
'</div>' + |
| 189 |
'</div>' + |
| 190 |
|
| 191 |
'<div class="bkbg-slc-net-hero" style="background:' + resultBg + ';">' + |
| 192 |
'<div class="bkbg-slc-net-label" style="color:' + labelColor + ';">💵 Annual Take-Home Pay</div>' + |
| 193 |
'<div class="bkbg-slc-net-val" style="color:' + accentColor + ';">' + fmt(r.netAnnual, cur) + '</div>' + |
| 194 |
(showEffective ? '<div class="bkbg-slc-net-eff" style="color:' + labelColor + ';">Effective tax rate: ' + r.effectiveRate.toFixed(1) + '%</div>' : '') + |
| 195 |
'</div>' + |
| 196 |
|
| 197 |
(showAllPeriods ? |
| 198 |
'<div class="bkbg-slc-periods">' + |
| 199 |
['Monthly','Bi-weekly','Weekly','Daily','Hourly'].map(function(lbl, i) { |
| 200 |
var divisors = [12, 26, 52, 260, 2080]; |
| 201 |
return '<div class="bkbg-slc-period-card" style="border-color:' + accentColor + '55;background:' + accentColor + '12;">' + |
| 202 |
'<div class="bkbg-slc-period-label" style="color:' + labelColor + ';">' + lbl + '</div>' + |
| 203 |
'<div class="bkbg-slc-period-val" style="color:' + accentColor + ';">' + fmt(r.netAnnual / divisors[i], cur) + '</div>' + |
| 204 |
'</div>'; |
| 205 |
}).join('') + |
| 206 |
'</div>' : '') + |
| 207 |
|
| 208 |
(showBreakdown ? |
| 209 |
'<div class="bkbg-slc-breakdown">' + |
| 210 |
'<h3 class="bkbg-slc-breakdown-title" style="color:' + labelColor + ';">📊 Annual Breakdown</h3>' + |
| 211 |
brow('Gross Income', fmt(r.gross, cur), 100, accentColor) + |
| 212 |
(r.ded401k > 0 ? brow('401(k) Contribution', '−'+fmt(r.ded401k, cur), pct(r.ded401k, r.gross), dedColor) : '') + |
| 213 |
(r.otherDed > 0 ? brow('Other Deductions', '−'+fmt(r.otherDed, cur), pct(r.otherDed,r.gross), dedColor) : '') + |
| 214 |
brow('Federal Income Tax', '−'+fmt(r.fedTax, cur), pct(r.fedTax, r.gross), taxColor) + |
| 215 |
brow('State Income Tax', '−'+fmt(r.stateTax, cur), pct(r.stateTax, r.gross), taxColor) + |
| 216 |
brow('Social Security', '−'+fmt(r.ss, cur), pct(r.ss, r.gross), taxColor) + |
| 217 |
brow('Medicare', '−'+fmt(r.medicare, cur), pct(r.medicare, r.gross), taxColor) + |
| 218 |
'<div class="bkbg-slc-divider"></div>' + |
| 219 |
brow('✓ Net Take-Home Pay', fmt(r.netAnnual, cur), pct(r.netAnnual,r.gross), accentColor) + |
| 220 |
'</div>' : '') + |
| 221 |
'</div>'; |
| 222 |
|
| 223 |
bindEvents(); |
| 224 |
} |
| 225 |
|
| 226 |
function bindEvents() { |
| 227 |
var inputs = { |
| 228 |
'slc-income': function(v){ state.income = parseFloat(v) || 0; }, |
| 229 |
'slc-statetax': function(v){ state.stateTax = parseFloat(v) || 0; }, |
| 230 |
'slc-401k': function(v){ state.k401 = parseFloat(v) || 0; }, |
| 231 |
'slc-other': function(v){ state.otherDed = parseFloat(v) || 0; } |
| 232 |
}; |
| 233 |
var selects = { |
| 234 |
'slc-period': function(v){ state.period = v; }, |
| 235 |
'slc-filing': function(v){ state.filing = v; } |
| 236 |
}; |
| 237 |
|
| 238 |
Object.keys(inputs).forEach(function(id) { |
| 239 |
var el = root.querySelector('#' + id); |
| 240 |
if (el) el.addEventListener('input', function() { inputs[id](this.value); render(); }); |
| 241 |
}); |
| 242 |
Object.keys(selects).forEach(function(id) { |
| 243 |
var el = root.querySelector('#' + id); |
| 244 |
if (el) el.addEventListener('change', function() { selects[id](this.value); render(); }); |
| 245 |
}); |
| 246 |
} |
| 247 |
|
| 248 |
render(); |
| 249 |
typoCssVarsForEl(root, opts.titleTypo, '--bkslc-tt-'); |
| 250 |
typoCssVarsForEl(root, opts.subtitleTypo, '--bkslc-st-'); |
| 251 |
}); |
| 252 |
|
| 253 |
})(); |
| 254 |
|