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
73132a1edf
commit
53315e03e8
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);
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user