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:
1 parent
be57deb5a2
commit
666b6c5e29
37 files changed
+625
-21
No files matched your search
+51
-15
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user