Matrix view: zoom controls so small screens can see the whole pyramid

The team pyramid overflows on phones. Add -/+/Fit zoom controls to the matrix
(pyramid) view that scale it via CSS zoom, defaulting to auto-fit so the whole
structure fits the screen width on load. Controls show only in pyramid mode,
re-fit on resize and when the dashboard tab opens, and guard against measuring
while the tab is hidden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-08 19:33:11 -05:00
parent 946a9d75b5
commit 860efc4912
3 changed files with 33 additions and 2 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
<section id="tabDash" class="mp-panel"> <section id="tabDash" class="mp-panel">
<div id="dFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:18px 0"></div> <div id="dFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:18px 0"></div>
<div id="dNextStep"></div><div id="dUpgrade"></div> <div id="dNextStep"></div><div id="dUpgrade"></div>
<div class="table-card" style="margin-bottom:18px"><div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap"><div><h2 style="margin:0 0 4px">Your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 14px">Your position's matrix — the rollup line on each card counts everyone underneath, all the way down. Click a position to drill into that leg. Open slots are where the next placements land.</p></div><button id="dTreeToggle" class="btn btn-secondary btn-sm">List view</button></div><div id="dOrgBar"></div><div id="dTreeNav" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="dTree"></div><div id="dGens" style="margin-top:14px"></div><p class="micro" style="margin:14px 0 0"><span class="mtp-qmark" style="position:static;display:inline-grid;vertical-align:middle">✓</span> qualified (2/2 directs) · <span class="mt-badge mt-prem">P</span> Premium · <span class="mt-badge">S</span> Standard · ⬇ everyone below that position (all generations) and the POL they've earned · <span style="color:var(--teal)">↧ spillover</span> = placed there by upline activity; only members who join with a position's own ID count toward its 2/2</p><div id="dSpillNote" class="hidden"></div></div> <div class="table-card" style="margin-bottom:18px"><div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap"><div><h2 style="margin:0 0 4px">Your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 14px">Your position's matrix — the rollup line on each card counts everyone underneath, all the way down. Click a position to drill into that leg. Open slots are where the next placements land.</p></div><div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><div id="dZoom" class="mt-zoom hidden"><button id="zOut" class="btn btn-secondary btn-sm mt-zbtn" title="Zoom out" aria-label="Zoom out">−</button><span id="zLbl" class="micro" style="min-width:40px;text-align:center;margin:0">100%</span><button id="zIn" class="btn btn-secondary btn-sm mt-zbtn" title="Zoom in" aria-label="Zoom in">+</button><button id="zFit" class="btn btn-secondary btn-sm" title="Fit the whole matrix on screen">Fit</button></div><button id="dTreeToggle" class="btn btn-secondary btn-sm">List view</button></div></div><div id="dOrgBar"></div><div id="dTreeNav" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="dTree"></div><div id="dGens" style="margin-top:14px"></div><p class="micro" style="margin:14px 0 0"><span class="mtp-qmark" style="position:static;display:inline-grid;vertical-align:middle">✓</span> qualified (2/2 directs) · <span class="mt-badge mt-prem">P</span> Premium · <span class="mt-badge">S</span> Standard · ⬇ everyone below that position (all generations) and the POL they've earned · <span style="color:var(--teal)">↧ spillover</span> = placed there by upline activity; only members who join with a position's own ID count toward its 2/2</p><div id="dSpillNote" class="hidden"></div></div>
<div class="table-card" style="margin-bottom:18px;border-color:rgba(123,224,161,.4)"><h2 style="margin:0 0 4px">Your pipeline</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Money forming below you. Each generation in your leg pays your position at exactly one level — when a member's level catches up to their depth, their <em>next</em> upgrade comes to you.</p><div id="dPipeline"></div></div> <div class="table-card" style="margin-bottom:18px;border-color:rgba(123,224,161,.4)"><h2 style="margin:0 0 4px">Your pipeline</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Money forming below you. Each generation in your leg pays your position at exactly one level — when a member's level catches up to their depth, their <em>next</em> upgrade comes to you.</p><div id="dPipeline"></div></div>
<div id="dCoachCard" class="table-card" style="margin-bottom:18px;display:none;border-color:rgba(240,197,109,.35)"><h2 style="margin:0 0 4px">Coach your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">The fastest way to grow your own income is helping the people below you take their next step. Here's who in <em>your</em> team could use a nudge today — updated live from the blockchain.</p><div id="dCoach"></div></div> <div id="dCoachCard" class="table-card" style="margin-bottom:18px;display:none;border-color:rgba(240,197,109,.35)"><h2 style="margin:0 0 4px">Coach your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">The fastest way to grow your own income is helping the people below you take their next step. Here's who in <em>your</em> team could use a nudge today — updated live from the blockchain.</p><div id="dCoach"></div></div>
<div class="table-card" style="margin-bottom:18px;border-color:rgba(120,160,255,.35)"><h2 style="margin:0 0 4px">Messages <span id="msgBell" class="hidden" style="font-size:14px;background:var(--gold);color:#132;border-radius:20px;padding:2px 10px;vertical-align:middle;font-weight:800">🔔 0</span></h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Wallet-verified messaging along your matrix lines — your upline and your team can reach you here, and you can reach them. No email needed; your wallet is your identity. <span class="micro">Messages are member-to-member; the team admin can review them for abuse.</span></p><div id="dMsg"><div class="empty">Loading…</div></div></div> <div class="table-card" style="margin-bottom:18px;border-color:rgba(120,160,255,.35)"><h2 style="margin:0 0 4px">Messages <span id="msgBell" class="hidden" style="font-size:14px;background:var(--gold);color:#132;border-radius:20px;padding:2px 10px;vertical-align:middle;font-weight:800">🔔 0</span></h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Wallet-verified messaging along your matrix lines — your upline and your team can reach you here, and you can reach them. No email needed; your wallet is your identity. <span class="micro">Messages are member-to-member; the team admin can review them for abuse.</span></p><div id="dMsg"><div class="empty">Loading…</div></div></div>
+30 -1
View File
@@ -13,6 +13,7 @@
const tp=document.getElementById('tabPitch'),td=document.getElementById('tabDash'); const tp=document.getElementById('tabPitch'),td=document.getElementById('tabDash');
if(tp)tp.classList.toggle('on',name==='pitch'); if(tp)tp.classList.toggle('on',name==='pitch');
if(td)td.classList.toggle('on',name==='dash'); if(td)td.classList.toggle('on',name==='dash');
if(name==='dash'&&treeUI.mode==='pyramid')setTimeout(applyMatrixZoom,30); // fit once the matrix is visible
} }
document.querySelectorAll('.mp-tab').forEach(t=>t.addEventListener('click',()=>{setTab(t.dataset.tab);window.scrollTo({top:0,behavior:'smooth'});})); document.querySelectorAll('.mp-tab').forEach(t=>t.addEventListener('click',()=>{setTab(t.dataset.tab);window.scrollTo({top:0,behavior:'smooth'});}));
function renderPitch(d){ function renderPitch(d){
@@ -34,7 +35,33 @@
function pathId(){const m=location.pathname.match(/^\/my\/(\d+)$/);if(m)return m[1];const q=new URLSearchParams(location.search).get('id');if(q&&/^\d+$/.test(q))return q;try{return localStorage.getItem('ctb.myId')||''}catch(e){return ''}} function pathId(){const m=location.pathname.match(/^\/my\/(\d+)$/);if(m)return m[1];const q=new URLSearchParams(location.search).get('id');if(q&&/^\d+$/.test(q))return q;try{return localStorage.getItem('ctb.myId')||''}catch(e){return ''}}
const treeUI={byId:{},parent:{},homeId:0,rootId:0,mode:'pyramid'}; const treeUI={byId:{},parent:{},homeId:0,rootId:0,mode:'pyramid',zoom:null,_applied:1};
// Zoom the matrix so it fits small screens. treeUI.zoom: null = auto-fit (shrink
// to fit width, never enlarge), or a manual factor. Uses CSS `zoom` so the layout
// reflows cleanly (no leftover box, centering + scroll keep working).
function applyMatrixZoom(){
const bar=document.getElementById('dZoom');
const mtp=document.querySelector('#dTree .mtp');
if(!mtp){if(bar)bar.classList.add('hidden');return}
mtp.style.zoom='1';
const availW=mtp.clientWidth;
if(!availW){if(bar)bar.classList.add('hidden');return} // in a hidden tab — apply when it opens
if(bar)bar.classList.remove('hidden');
const natW=mtp.scrollWidth||1;
let z=treeUI.zoom;
if(z==null)z=Math.min(1,availW/natW); // Fit / default: shrink only
z=Math.max(.4,Math.min(1.6,z));
mtp.style.zoom=String(z);
treeUI._applied=z;
const lbl=document.getElementById('zLbl');if(lbl)lbl.textContent=Math.round(z*100)+'%';
}
(function wireZoom(){
const out=document.getElementById('zOut'),inn=document.getElementById('zIn'),fit=document.getElementById('zFit');
if(out)out.addEventListener('click',()=>{treeUI.zoom=Math.max(.4,(treeUI._applied||1)-0.15);applyMatrixZoom()});
if(inn)inn.addEventListener('click',()=>{treeUI.zoom=Math.min(1.6,(treeUI._applied||1)+0.15);applyMatrixZoom()});
if(fit)fit.addEventListener('click',()=>{treeUI.zoom=null;applyMatrixZoom()});
let t;window.addEventListener('resize',()=>{clearTimeout(t);t=setTimeout(()=>{if(treeUI.mode==='pyramid')applyMatrixZoom()},160)});
})();
function indexTree(n,par){if(!n)return;treeUI.byId[n.id]=n;if(par)treeUI.parent[n.id]=par.id;indexTree(n.left,n);indexTree(n.right,n)} function indexTree(n,par){if(!n)return;treeUI.byId[n.id]=n;if(par)treeUI.parent[n.id]=par.id;indexTree(n.left,n);indexTree(n.right,n)}
function isSpill(n){return !!(n&&treeUI.parent[n.id]&&n.referrerId&&n.referrerId!==treeUI.parent[n.id])} function isSpill(n){return !!(n&&treeUI.parent[n.id]&&n.referrerId&&n.referrerId!==treeUI.parent[n.id])}
function card(n,focus){ function card(n,focus){
@@ -58,10 +85,12 @@
nav.innerHTML=path.length>1?`<span class="micro" style="margin:0">Viewing:</span> `+path.map((id,i)=>i===path.length-1?`<strong class="mt-id">#${id}</strong>`:`<button class="btn btn-secondary btn-sm" data-tid="${id}">#${id}</button>`).join(' <span style="color:var(--muted)">›</span> ')+` <button class="btn btn-secondary btn-sm" data-tid="${treeUI.parent[treeUI.rootId]}">↑ Up one</button>`:''; nav.innerHTML=path.length>1?`<span class="micro" style="margin:0">Viewing:</span> `+path.map((id,i)=>i===path.length-1?`<strong class="mt-id">#${id}</strong>`:`<button class="btn btn-secondary btn-sm" data-tid="${id}">#${id}</button>`).join(' <span style="color:var(--muted)">›</span> ')+` <button class="btn btn-secondary btn-sm" data-tid="${treeUI.parent[treeUI.rootId]}">↑ Up one</button>`:'';
const wire=el=>el.querySelectorAll('[data-tid]').forEach(b=>b.addEventListener('click',()=>{const t=Number(b.dataset.tid);if(t&&treeUI.byId[t]){treeUI.rootId=t;renderTree()}})); const wire=el=>el.querySelectorAll('[data-tid]').forEach(b=>b.addEventListener('click',()=>{const t=Number(b.dataset.tid);if(t&&treeUI.byId[t]){treeUI.rootId=t;renderTree()}}));
wire(out);wire(nav); wire(out);wire(nav);
applyMatrixZoom();
} }
function renderList(){ function renderList(){
const out=document.getElementById('dTree'); const out=document.getElementById('dTree');
document.getElementById('dTreeNav').innerHTML=''; document.getElementById('dTreeNav').innerHTML='';
const zb=document.getElementById('dZoom');if(zb)zb.classList.add('hidden');
const home=treeUI.byId[treeUI.homeId]; const home=treeUI.byId[treeUI.homeId];
if(!home){out.innerHTML='<div class="empty">Team view is still indexing — check back in a few minutes.</div>';return} if(!home){out.innerHTML='<div class="empty">Team view is still indexing — check back in a few minutes.</div>';return}
const node=(n,depth)=>{ const node=(n,depth)=>{
+2
View File
@@ -135,6 +135,8 @@ a{color:inherit}.wrap{width:min(1160px,calc(100% - 32px));margin:auto}.nav{heigh
/* Admin matrix pyramid view */ /* Admin matrix pyramid view */
.mtp{display:flex;flex-direction:column;gap:14px;overflow-x:auto;padding:6px 2px 10px} .mtp{display:flex;flex-direction:column;gap:14px;overflow-x:auto;padding:6px 2px 10px}
.mt-zoom{display:inline-flex;gap:4px;align-items:center;background:rgba(13,34,54,.6);border:1px solid #2a4a66;border-radius:9px;padding:3px 4px}
.mt-zbtn{min-width:30px;padding-left:0;padding-right:0;font-weight:800;font-size:16px;line-height:1}
.mtp-row{display:flex;justify-content:center;gap:8px;min-width:max-content;margin:0 auto} .mtp-row{display:flex;justify-content:center;gap:8px;min-width:max-content;margin:0 auto}
.mtp-card{appearance:none;font:inherit;cursor:pointer;text-align:center;background:#0d2236;border:1px solid #2a4a66;border-radius:12px;padding:9px 10px;min-width:104px;color:var(--text);display:flex;flex-direction:column;gap:2px;align-items:center;transition:.15s ease} .mtp-card{appearance:none;font:inherit;cursor:pointer;text-align:center;background:#0d2236;border:1px solid #2a4a66;border-radius:12px;padding:9px 10px;min-width:104px;color:var(--text);display:flex;flex-direction:column;gap:2px;align-items:center;transition:.15s ease}
.mtp-card:hover{border-color:var(--teal);transform:translateY(-1px)} .mtp-card:hover{border-color:var(--teal);transform:translateY(-1px)}