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 73132a1edf
commit 53315e03e8
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);
+3
View File
@@ -4,6 +4,7 @@
<!-- retire-planner v1 -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#f7f9fc">
<title>Retire Planner — Canadian retirement, modelled properly</title>
<meta name="description" content="A Canadian retirement calculator with real CPP/OAS/GIS/RRIF mechanics and live 2026 tax rates. 100% client-side — answers never leave your browser.">
<link rel="stylesheet" href="style.css">
@@ -53,6 +54,8 @@
</div>
</section>
<div id="scrim" hidden></div>
<!-- RESULTS -->
<section id="scr-results" class="scr" hidden>
<div id="vision-card" class="vision"></div>
+34 -5
View File
@@ -19,7 +19,8 @@ b{color:var(--ink)}
.hdr-in{max-width:1060px;margin:0 auto;display:flex;align-items:center;gap:10px;padding:10px 12px;flex-wrap:wrap}
.logo{font-size:17px}
.pill{font-size:12px;background:#e8f5ee;color:#0a5c36;border:1px solid #bfe6d0;border-radius:99px;padding:3px 10px;color:#0a5c36}
.ghost{background:none;border:1px solid var(--line);border-radius:10px;padding:7px 12px;font-size:14px;color:var(--ink2);cursor:pointer}
.ghost{background:none;border:1px solid var(--line);border-radius:10px;padding:7px 12px;font-size:14px;color:var(--ink2);cursor:pointer;min-height:44px}
button,input{touch-action:manipulation}
.ghost:hover{border-color:var(--blue);color:var(--blue)}
/* screens */
@@ -36,13 +37,32 @@ b{color:var(--ink)}
/* quiz */
.quiz-wrap{display:grid;grid-template-columns:1fr 320px;gap:20px;align-items:start}
#scrim{display:none}
@media(max-width:860px){
.quiz-wrap{grid-template-columns:1fr}
.panel{position:fixed;left:8px;right:8px;bottom:8px;z-index:40;max-height:44vh;overflow:auto;box-shadow:0 -6px 24px #16233b22;transform:translateY(calc(100% - 58px));transition:transform .25s}
.panel.open{transform:none}
/* Fix 1: single-row compact header (mobile only; desktop untouched) */
.hdr{padding-top:env(safe-area-inset-top)}
.hdr-in{flex-wrap:nowrap;padding:5px 10px;gap:8px} /* 5px (not 6px) so .ghost's 44px min-height keeps the header ≤56px */
.logo{font-size:15px}
.hdr .ghost{padding:6px 9px;font-size:13px}
.hdr #btn-print{display:none}
/* Fix 2: charts keep desktop scale; scroll sideways on phones */
.chartbox{overflow-x:auto;overflow-y:hidden}
.chartbox::before{content:"swipe →";position:sticky;right:0;display:block;width:max-content;margin-bottom:-17px;padding:1px 8px;font-size:10px;color:var(--ink2);background:#ffffffcc;border:1px solid var(--line);border-radius:99px;pointer-events:none}
.chartbox svg{min-width:560px}
/* Fix 3: sheet height = its content; chips always visible; no transform peek */
.panel{position:fixed;left:8px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom));z-index:40;max-height:44vh;overflow:auto;box-shadow:0 -6px 24px #16233b22}
.panel .insight{display:none}
.panel.open .insight{display:block}
.panel-h{cursor:pointer;position:sticky;top:0;background:var(--card)}
.panel-h::after{content:" ▲";font-size:11px;color:var(--ink2)}
.panel.open .panel-h::after{content:" ▼"}
#scrim{position:fixed;inset:0;background:#16233b33;z-index:35}
#scrim.on{display:block}
.scr{padding-bottom:calc(185px + env(safe-area-inset-bottom))}
}
@media(max-width:700px){
.hdr .pill{display:none} /* privacy pill is repeated on the intro screen and the footer */
}
.panel{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:14px}
.panel-h{font-weight:700;font-size:14px;margin-bottom:8px}
@@ -67,7 +87,7 @@ input[type=number]:focus{outline:0;border-color:var(--blue)}
.opts{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin-top:6px}
.opt{border:2px solid var(--line);background:#fff;border-radius:10px;padding:11px 8px;text-align:center;font-size:15px;cursor:pointer}
.opt.sel{border-color:var(--blue);background:#eef4ff;font-weight:700}
.opt small{display:block;font-size:11.5px;color:var(--ink2);font-weight:400}
.opt small{display:block;font-size:12px;color:var(--ink2);font-weight:400}
.err{color:var(--red);font-size:13px;min-height:18px}
.range-row{display:flex;align-items:baseline;gap:10px}
input[type=range]{width:100%;accent-color:var(--blue)}
@@ -86,7 +106,7 @@ input[type=range]{width:100%;accent-color:var(--blue)}
.cap{font-size:13.5px;margin:.2em 0 .6em}
.chartbox{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:10px;overflow:hidden}
.chartbox svg{display:block;width:100%;height:auto}
.legend{display:flex;flex-wrap:wrap;gap:12px;font-size:12.5px;color:var(--ink2);padding:4px 6px 0}
.legend{display:flex;flex-wrap:wrap;gap:12px;font-size:13px;color:var(--ink2);padding:4px 6px 0}
.legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:5px;vertical-align:-1px}
/* vision */
@@ -114,6 +134,15 @@ tr.dep td{color:var(--red)}
.stamp{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}
.btn-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
/* Fix 5: year-by-year table on small screens (placed after the base table rules so font-size wins) */
@media(max-width:560px){
th:nth-child(4),td:nth-child(4),th:nth-child(9),td:nth-child(9){display:none} /* Non-reg, GIS */
table{font-size:13px}
/* right-edge fade so the horizontal scroll is discoverable (::before = first child, pinned to the visible top-right) */
.tbl-wrap{position:relative}
.tbl-wrap::before{content:"";float:right;width:26px;height:340px;background:linear-gradient(90deg,transparent,var(--card));pointer-events:none}
}
@media print{
.hdr,.whatif,.btn-row,#scr-quiz{display:none!important}
body{background:#fff}