WORKSPACE
{NAV.map(([id,ic,l,b]) => (
))}
YOUR PROJECT
Ledgerline rebuild
SPRINT 04 · W22
62% complete · 3 sprints to go
JC
Jane Cooper
CTO · LEDGERLINE
);
};
const PortalTopbar = () => (
);
// ─── DASHBOARD ─────────────────────────────────────────────
const PortalDashboard = ({ setView }) => (
Welcome back, Jane.
Here's where Ledgerline stands today — May 14, 2026.
{/* Big timeline */}
● ON TRACK · LAUNCH JUNE 28
Ledgerline rebuild · Sprint 04 of 8
{/* Timeline bars */}
{['Discovery','Design','Frontend','API + Data','Auth','Payments','QA + Polish','Launch'].map((s,i) => (
))}
{/* Stat row */}
{/* Two-col */}
setView('files')}>
{[
{t:'Auth flow — final mockups',k:'Design',w:'2 days waiting',c:'#ff8a3a'},
{t:'Brand mark v3',k:'Brand',w:'5 days waiting',c:'#7c9bff'},
].map((r,i) => (
{r.t}
{r.k.toUpperCase()}
{r.w}
))}
{TEAM.slice(0,4).map((t,i) => (
))}
{/* Activity */}
{[
['HA','#d4ff3a','pushed','Frontend dashboard refactor','25m ago'],
['SR','#ff8a3a','uploaded','Auth flow — final mockups','2h ago'],
['AQ','#7c9bff','merged','PR #142 · Webhook reliability','3h ago'],
['HA','#d4ff3a','scheduled','Demo for Friday, 4pm','yesterday'],
['BK','#a78bfa','commented','on Brand mark v3','2d ago'],
].map(([w,c,a,t,d],i) => (
))}
{[
['Thu, May 16','Sprint demo · 4:00 pm','#d4ff3a','demo'],
['Fri, May 17','Auth flow review · 11:00 am','#ff8a3a','review'],
['Mon, May 20','Sprint 05 kickoff · 10:00 am','#7c9bff','kickoff'],
['Wed, May 22','CMS handoff milestone','#a78bfa','milestone'],
].map(([d,n,c,k],i) => (
))}
);
const PortalStat = ({ label, value, sub, hot, ok }) => {
const c = hot ? 'var(--warn)' : ok ? 'var(--ok)' : 'var(--ink-3)';
return (
);
};
// ─── PROJECT DETAIL ────────────────────────────────────────
const PortalProject = ({ id }) => {
const sprints = [
{n:'S1',name:'Discovery',w:'W18',status:'done',pts:24},
{n:'S2',name:'Design system',w:'W19',status:'done',pts:32},
{n:'S3',name:'Frontend',w:'W20',status:'done',pts:34},
{n:'S4',name:'API + Data',w:'W21',status:'done',pts:30},
{n:'S5',name:'Auth',w:'W22',status:'active',pts:32,progress:62},
{n:'S6',name:'Payments',w:'W23',status:'next',pts:28},
{n:'S7',name:'QA + Polish',w:'W24',status:'planned',pts:22},
{n:'S8',name:'Launch',w:'W25',status:'planned',pts:18},
];
return (
Ledgerline rebuild
SP04 · Sprint 04 of 8 · 220 total story points · Launch June 28
{/* Sprint board */}
{sprints.map(sp => (
{sp.n}
{sp.w}
{sp.name}
{sp.status}
{sp.pts}P
))}
{/* Current sprint detail */}
● SPRINT 04 · ACTIVE · DAY 6 OF 10
32 / 32 PTS COMMITTED
Auth & sessions
{[
['DONE','Done',[
{t:'OAuth provider integration',pts:8,w:'AQ',c:'#7c9bff'},
{t:'Session token refresh logic',pts:5,w:'AQ',c:'#7c9bff'},
{t:'Login + signup flows (front)',pts:6,w:'NM',c:'#7ee787'},
]],
['DOING','In progress',[
{t:'Multi-factor auth flow',pts:8,w:'AQ',c:'#7c9bff'},
{t:'Account settings UI',pts:3,w:'NM',c:'#7ee787'},
]],
['TODO','To do',[
{t:'Password reset emails',pts:2,w:'AQ',c:'#7c9bff'},
]],
].map(([k,l,items]) => (
{l.toUpperCase()} · {items.length}
{items.map(it => (
{k==='DONE' ? Icon.check() : k==='DOING' ? '◐' : '○'}
{it.t}
{it.w}
{it.pts}P
))}
))}
BURNDOWN
D1
D5
D10
RISKS
{[
['low','OAuth provider rate limits','#7ee787'],
['med','MFA carrier compatibility','#ffb43a'],
['low','Email deliverability','#7ee787'],
].map(([l,t,c]) => (
{t}
{l}
))}
NEXT DEMO
Friday, May 17 · 4:00 PM
Auth & sessions walkthrough. Bring your team.
);
};
// ─── FILES ─────────────────────────────────────────────────
const PortalFiles = () => {
const groups = [
{label:'Awaiting your approval', tone:'warn', items:[
['Auth flow — final mockups','Figma','SR','2 days','review'],
['Brand mark v3','PDF','SR','5 days','review'],
]},
{label:'Sprint 04 — Auth', tone:'normal', items:[
['OAuth flow diagram','PDF','AQ','today','final'],
['Session model','Doc','AQ','today','final'],
['MFA wireframes','Figma','SR','2 days','draft'],
]},
{label:'Sprint 03 — Frontend', tone:'normal', items:[
['Dashboard.tsx','Code','HA','1 wk','final'],
['Components export','Storybook','NM','1 wk','final'],
['Lighthouse report','PDF','HA','1 wk','final'],
]},
{label:'Sprint 02 — Design system', tone:'normal', items:[
['Tokens.json','JSON','SR','3 wks','final'],
['Brand guide','PDF','SR','3 wks','final'],
['Component library','Figma','SR','3 wks','final'],
['Type specimens','PDF','SR','3 wks','final'],
]},
];
return (
Files & deliverables
Every artifact, every revision, in one place.
{groups.map((g, gi) => (
{g.label}
{g.items.length} FILES
{g.tone==='warn' && }
{g.items.map(([n,type,by,when,status],i) => (
0 ? '1px solid var(--line)' : 'none'}}>
{type.slice(0,3).toUpperCase()}
{n}
{type}
{by}
{when} ago
{status}
{status==='review' ? (
) : (
)}
))}
))}
);
};
// ─── MESSAGES ──────────────────────────────────────────────
const PortalMessages = () => {
const threads = [
{id:'t1',name:'Hassan A.',role:'Engineering lead',c:'#d4ff3a',last:'Just pushed the new dashboard branch — preview link in #demo',when:'12m',unread:1,active:true},
{id:'t2',name:'Sana R.',role:'Design lead',c:'#ff8a3a',last:'New auth mockups are ready for review whenever you have a minute',when:'2h',unread:1},
{id:'t3',name:'Amir Q.',role:'Backend',c:'#7c9bff',last:'Webhook retry logic merged. Watching the production logs.',when:'4h',unread:1},
{id:'t4',name:'#general',role:'Team channel',c:'#a78bfa',last:'Hassan A.: nice — also worth running the perf tests',when:'yesterday',unread:0},
{id:'t5',name:'Bilal K.',role:'AI engineer',c:'#a78bfa',last:'Got the eval pipeline set up if you want a walkthrough later',when:'2d',unread:0},
];
const messages = [
{who:'Hassan A.',c:'#d4ff3a',self:false,t:'Morning Jane. Quick heads up — I just pushed the new dashboard branch.',w:'09:34'},
{who:'Hassan A.',c:'#d4ff3a',self:false,t:'Preview is at preview-ledgerline-frontend-r4f.atoms.dev. Same login as staging.',w:'09:34'},
{who:'Jane',c:'#7c9bff',self:true,t:'Amazing, taking a look now',w:'09:38'},
{who:'Jane',c:'#7c9bff',self:true,t:'The metric cards are gorgeous. Question — is the % delta calculated against the same period last month, or trailing 30d?',w:'09:41'},
{who:'Hassan A.',c:'#d4ff3a',self:false,t:'Trailing 30 by default. We can swap to same-period-last-month with a toggle if you prefer — it\'s a 5-min change.',w:'09:42'},
{who:'Hassan A.',c:'#d4ff3a',self:false,t:'Curious what your accounting team is used to seeing.',w:'09:42'},
{who:'Jane',c:'#7c9bff',self:true,t:'They\'re used to same-period-last-month. Let\'s add the toggle and default to that.',w:'09:45'},
];
return (
{/* Thread list */}
{/* Conversation */}
HA
Hassan A.
● ONLINE · ENGINEERING LEAD
TODAY
{messages.map((m,i) => (
{(!m.self || (i > 0 && messages[i-1].self !== m.self)) && (
{m.who.split(' ').map(w=>w[0]).join('').slice(0,2)}
)}
))}
Hassan is typing…
);
};
// ─── BILLING ───────────────────────────────────────────────
const PortalBilling = () => (
Billing & invoices
● PROJECT ON ACCOUNT
Ledgerline rebuild · Studio tier
$31,200}/>
PAYMENT METHOD
VISA
•••• •••• •••• 4242
EXP 12/28
{/* Invoices */}
Invoices
{['Invoice','Date','Description','Amount','Status',''].map(h => (
{h}
))}
{[
['INV-0042','May 1, 2026','Sprint 04 milestone',7800,'paid'],
['INV-0041','Apr 17, 2026','Sprint 03 milestone',7800,'paid'],
['INV-0040','Apr 3, 2026','Sprint 02 milestone',7800,'paid'],
['INV-0039','Mar 20, 2026','Sprint 01 milestone',7800,'paid'],
['INV-0038','Mar 1, 2026','Project kickoff · 50%',31200,'paid'],
['INV-0043','May 22, 2026','CMS handoff milestone',7800,'upcoming'],
['INV-0044','Jun 28, 2026','Launch · final 50%',23400,'upcoming'],
].map(([n,d,desc,amt,s],i) => (
0 ? '1px solid var(--line)' : 'none'}}>
{n}
{d}
{desc}
${amt.toLocaleString()}
{s}
))}
);
window.Portal = Portal;