Mobile pass: 8 fixes per MOBILE-FIX.md (single-row 55px header, 560px-scroll charts w/ bigger SVG fonts + swipe hint, content-height chips sheet w/ scrim, 44px touch targets, year table 13px/-2 cols/fade/sticky Age, vision manifest-race fix, honest share fallback, theme-color+type bumps); before/after audits + geo-check evidence

This commit is contained in:
krisf committed 2026-10-09 06:04:03 -04:00
1 parent be57deb5a2
commit 666b6c5e29
37 files changed
+625 -21

No files matched your search

+51 -15
View File
@@ -201,6 +201,7 @@
state.screen = name;
['intro', 'quiz', 'results'].forEach(function (s) { $('#scr-' + s).hidden = s !== name; });
$('#btn-restart').hidden = name === 'intro';
syncScrim();
window.scrollTo(0, 0);
}
@@ -263,6 +264,8 @@
if (f1) {
f1.oninput = function () { s.set(f1.value); save(); liveUpdate(); if (s.type === 'slider') $('#sv').textContent = s.fmt(+f1.value); };
if (s.type === 'slider') f1.oninput();
/* Fix 8: Enter on a number step = press Next (when enabled). */
if (s.type === 'number') f1.onkeydown = function (e) { if (e.key === 'Enter' && !btnNext.disabled) btnNext.click(); };
}
stepBody.querySelectorAll('input[type=number][data-k]').forEach(function (inp) {
inp.oninput = function () { s.set(inp.dataset.k, inp.value); save(); liveUpdate(); };
@@ -345,14 +348,14 @@
for (i = 0; i <= yt; i++) {
var v = ymax * i / yt, y = Y(v);
s += '<line x1="' + L + '" x2="' + (W - R) + '" y1="' + y.toFixed(1) + '" y2="' + y.toFixed(1) + '" stroke="#eef2f8"/>' +
'<text x="' + (L - 6) + '" y="' + (y + 4).toFixed(1) + '" text-anchor="end" font-size="11" fill="#8a97ad">' + (opts && opts.money === false ? v : k(v)) + '</text>';
'<text x="' + (L - 6) + '" y="' + (y + 4).toFixed(1) + '" text-anchor="end" font-size="13" fill="#8a97ad">' + (opts && opts.money === false ? v : k(v)) + '</text>';
}
var step = a1 - a0 > 34 ? 10 : 5;
for (var a = Math.ceil(a0 / step) * step; a <= a1; a += step) {
var x = X(a);
s += '<text x="' + x.toFixed(1) + '" y="' + (H - B + 16) + '" text-anchor="middle" font-size="11" fill="#8a97ad">' + a + '</text>';
s += '<text x="' + x.toFixed(1) + '" y="' + (H - B + 16) + '" text-anchor="middle" font-size="13" fill="#8a97ad">' + a + '</text>';
}
s += '<text x="' + (W / 2) + '" y="' + (H - 4) + '" text-anchor="middle" font-size="11" fill="#8a97ad">age</text>';
s += '<text x="' + (W / 2) + '" y="' + (H - 4) + '" text-anchor="middle" font-size="12" fill="#8a97ad">age</text>';
return s;
}
function svgWrap(inner) {
@@ -377,11 +380,11 @@
if (proj.depletionAge) {
var x = X(proj.depletionAge);
inner += '<line x1="' + x + '" x2="' + x + '" y1="' + T + '" y2="' + (H - B) + '" stroke="#dc2626" stroke-dasharray="4 4" stroke-width="2"/>' +
'<text x="' + (x + 5) + '" y="' + (T + 12) + '" font-size="12" fill="#dc2626" font-weight="700">runs out ≈ ' + proj.depletionAge + '</text>';
'<text x="' + (x + 5) + '" y="' + (T + 12) + '" font-size="13" fill="#dc2626" font-weight="700">runs out ≈ ' + proj.depletionAge + '</text>';
}
var ret = Math.max(a0, inputs().retireAge);
inner += '<line x1="' + X(ret) + '" x2="' + X(ret) + '" y1="' + T + '" y2="' + (H - B) + '" stroke="#94a3b8" stroke-dasharray="2 4"/>' +
'<text x="' + (X(ret) + 4) + '" y="' + (H - B - 6) + '" font-size="10.5" fill="#94a3b8">retire</text>';
'<text x="' + (X(ret) + 4) + '" y="' + (H - B - 6) + '" font-size="12" fill="#94a3b8">retire</text>';
$('#g1').innerHTML = svgWrap(inner) + legend([['Total savings (today’s $)', COL.bal]]);
}
@@ -423,7 +426,7 @@
var first = yrs.map(function (r, i) { return r.unfunded > 1 ? ages[i] : null; }).filter(function (x) { return x; })[0];
if (first) {
inner += '<circle cx="' + X(first) + '" cy="' + Y(need[ages.indexOf(first)]) + '" r="4.5" fill="#dc2626"/>' +
'<text x="' + (X(first) + 6) + '" y="' + (Y(need[ages.indexOf(first)]) - 6) + '" font-size="12" fill="#dc2626" font-weight="700">shortfall from ~' + first + '</text>';
'<text x="' + (X(first) + 6) + '" y="' + (Y(need[ages.indexOf(first)]) - 6) + '" font-size="13" fill="#dc2626" font-weight="700">shortfall from ~' + first + '</text>';
}
$('#g3').innerHTML = svgWrap(inner) + legend([['Cash you’ll have (after tax)', COL.cash], ['Planned spending', COL.need]]);
}
@@ -451,22 +454,32 @@
for (var i = 0; i <= 4; i++) {
var v = ymax * i / 4, y = sy(ymax)(v);
inner += '<line x1="' + L + '" x2="' + (W - R) + '" y1="' + y + '" y2="' + y + '" stroke="#eef2f8"/>' +
'<text x="' + (L - 6) + '" y="' + (y + 4) + '" text-anchor="end" font-size="11" fill="#8a97ad">' + k(v) + '</text>';
'<text x="' + (L - 6) + '" y="' + (y + 4) + '" text-anchor="end" font-size="13" fill="#8a97ad">' + k(v) + '</text>';
}
res.forEach(function (r, i) {
var hgt = (H - T - B) * r[1] / ymax, x = L + i * bw + bw * 0.16, y = H - B - hgt;
var col = i === 0 ? '#94a3b8' : (r[1] >= res[0][1] ? '#059669' : '#d97706');
inner += '<rect x="' + x + '" y="' + y + '" width="' + bw * 0.68 + '" height="' + hgt + '" rx="6" fill="' + col + '"/>' +
'<text x="' + (x + bw * 0.34) + '" y="' + (y - 6) + '" text-anchor="middle" font-size="12.5" font-weight="700" fill="#16233b">' + k(r[1]) + '</text>' +
'<text x="' + (x + bw * 0.34) + '" y="' + (H - B + 14) + '" text-anchor="middle" font-size="10.5" fill="#8a97ad">' + r[0] + '</text>';
'<text x="' + (x + bw * 0.34) + '" y="' + (y - 6) + '" text-anchor="middle" font-size="13.5" font-weight="700" fill="#16233b">' + k(r[1]) + '</text>' +
'<text x="' + (x + bw * 0.34) + '" y="' + (H - B + 14) + '" text-anchor="middle" font-size="12" fill="#8a97ad">' + r[0] + '</text>';
});
$('#g4').innerHTML = svgWrap(inner);
}
/* ---------- vision ---------- */
var visionManifest = null;
fetch('vision/manifest.json').then(function (r) { return r.ok ? r.json() : null; })
.then(function (m) { visionManifest = m; }).catch(function () { });
/* Fix 6: a fresh/direct load into results can render before the manifest lands.
When it resolves (or fails, or times out at 2.5s — then we just keep the gradient),
re-render the vision card if results is on screen, so real images replace the fallback. */
var visionReady = (function () {
var p = fetch('vision/manifest.json').then(function (r) { return r.ok ? r.json() : null; });
var timeout = new Promise(function (res) { setTimeout(function () { res(null); }, 2500); });
return Promise.race([p, timeout]);
})();
visionReady
.then(function (m) { visionManifest = m || null; })
.catch(function () { visionManifest = visionManifest || null; })
.then(function () { if (state.screen === 'results') renderVision(state.a); });
function renderVision(a) {
var el = $('#vision-card');
@@ -596,13 +609,36 @@
$('#btn-print').onclick = function () { window.print(); };
$('#btn-share').onclick = function () {
var url = location.origin + location.pathname + '#p=' + encState();
var done = function () { $('#btn-share').textContent = '✅ Link copied'; setTimeout(function () { $('#btn-share').textContent = '🔗 Copy link with my answers'; }, 1800); };
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(url).then(done, done);
else done();
var restore = function () { $('#btn-share').textContent = '🔗 Copy link with my answers'; };
var done = function () { $('#btn-share').textContent = '✅ Link copied'; setTimeout(restore, 1800); };
/* Fix 7: never claim "copied" when the clipboard API is missing or rejected. */
var fail = function () {
window.prompt('Copy your link:', url);
$('#btn-share').textContent = '🔗 Link shown — copy it';
setTimeout(restore, 1800);
};
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(url).then(done, fail);
else fail();
};
document.addEventListener('click', function (e) {
var h = document.querySelector('.panel-h');
if (h && h.contains(e.target)) document.getElementById('panel').classList.toggle('open');
if (h && h.contains(e.target)) {
document.getElementById('panel').classList.toggle('open');
syncScrim();
}
});
/* Fix 3: scrim behind the open sheet on mobile; tapping it closes the sheet. */
function syncScrim() {
var scrim = document.getElementById('scrim');
if (!scrim) return;
var open = document.getElementById('panel').classList.contains('open');
scrim.hidden = !open;
scrim.classList.toggle('on', open);
}
var scrimEl = document.getElementById('scrim');
if (scrimEl) scrimEl.addEventListener('click', function () {
document.getElementById('panel').classList.remove('open');
syncScrim();
});
var savedScreen = state.screen, savedStep = state.step;
showScreen(savedScreen);