diff --git a/src/css/styles.css b/src/css/styles.css index 386124b..81a03a8 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -286,8 +286,14 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)} .ppnl-tabs{display:flex;gap:4px} .ppnl-tab{font-family:var(--mono);font-size:.65rem;letter-spacing:1px;cursor:pointer;background:transparent;border:1px solid var(--bd);color:var(--mu);padding:4px 10px;transition:all .15s} .ppnl-tab.active{border-color:var(--green);color:var(--green)} -.ppnl-cards{padding:16px 20px 20px;display:grid;grid-template-columns:repeat(3,minmax(0,340px));gap:12px;justify-content:center} +.ppnl-layout{padding:16px 20px 20px;display:grid;grid-template-columns:1.4fr 1fr;gap:14px;align-items:stretch} .ppnl-card{background:var(--s2);border:1px solid var(--bd);border-radius:10px;padding:14px 16px;position:relative} +.ppnl-trio{display:flex;flex-direction:column;gap:10px} +.ppnl-trio .ppnl-card{flex:1;display:flex;flex-direction:column;justify-content:center} +.ppnl-hero{background:linear-gradient(135deg,var(--gd),transparent 65%),var(--s2);border-color:var(--gb);padding:24px 28px;display:flex;flex-direction:column;justify-content:center} +.ppnl-hero .ppnl-lbl{font-size:.65rem;margin-bottom:14px} +.ppnl-hero .ppnl-main{font-size:2.2rem;letter-spacing:-1px;color:var(--green)} +.ppnl-hero .ppnl-sub{font-size:.72rem;margin-top:8px} /* Generic styled-popover tooltip. Opt in by adding class="has-tip" + data-tip="…" */ .has-tip{position:relative;cursor:help} @@ -308,8 +314,7 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)} .ppnl-mtbl tr:last-child td{border-bottom:none} .ppnl-mtbl .rate-hi{color:var(--green)} .ppnl-mtbl .rate-lo{color:var(--red)} -@media(max-width:600px){.ppnl-cards{grid-template-columns:1fr 1fr}} -@media(max-width:460px){.ppnl-cards{grid-template-columns:1fr}} +@media(max-width:600px){.ppnl-layout{grid-template-columns:1fr}.ppnl-hero .ppnl-main{font-size:1.8rem}} /* EMPTY */ .empty{text-align:center;padding:64px 24px;display:flex;flex-direction:column;align-items:center;gap:16px} diff --git a/src/js/07-render-charts.js b/src/js/07-render-charts.js index f28bd3f..7b8824a 100644 --- a/src/js/07-render-charts.js +++ b/src/js/07-render-charts.js @@ -445,33 +445,36 @@ function rCharts(displayRows, lots) { if (sPpnlTab === 'total') { const s = calcStats(displayRows); - function card(label, main, sub, tip) { + function tile(extraClass, label, main, sub, tip) { const tipAttr = tip ? ' data-tip="' + tip.replace(/"/g, '"') + '"' : ''; - return '
' + + const cls = 'ppnl-card' + (extraClass ? ' ' + extraClass : '') + (tip ? ' has-tip' : ''); + return '
' + '
' + label + (tip ? ' ' : '') + '
' + '
' + main + '
' + (sub ? '
' + sub + '
' : '') + '
'; } - el.className = 'ppnl-cards'; - el.innerHTML = [ - card('Total Premium Collected', + el.className = 'ppnl-layout'; + el.innerHTML = + tile('ppnl-hero', + 'Total Premium Collected', s.totalCount > 0 ? '$' + fmt(s.totalPrem) : dash, s.totalCount > 0 ? pos(s.settled) + ' settled' + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '', - 'Sum of every option premium collected (gross of buy-to-close costs). Includes settled and open positions.'), - card('Total Notional', - s.totalNotional > 0 ? '$' + fmt(s.totalNotional) : dash, - s.totalCount > 0 ? pos(s.totalCount) + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '', - 'Total Notional = Σ strike × size across every option (settled and open). The capital you would tie up if every put were assigned at strike.'), - card('Portfolio APR', - s.portfolioAPR !== null ? s.portfolioAPR.toFixed(1) + '%' : dash, - s.settled > 0 ? 'notional-weighted · ' + s.settled + ' settled' : '', - 'Notional-weighted average APR of settled options. Per-option APR = (netPrem / collateral) / DTE × 365. Open options excluded.'), - card('Return Rate', - s.returnRate !== null ? s.returnRate.toFixed(1) + '%' : dash, - s.settled > 0 ? s.otmCount + ' / ' + s.settled + ' exp OTM' : '', - 'Share of settled options that expired OTM (premium kept, no assignment/call-away). Open options excluded.'), - ].join(''); + 'Sum of every option premium collected (gross of buy-to-close costs). Includes settled and open positions.') + + '
' + + tile('', 'Total Notional', + s.totalNotional > 0 ? '$' + fmt(s.totalNotional) : dash, + s.totalCount > 0 ? pos(s.totalCount) + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '', + 'Total Notional = Σ strike × size across every option (settled and open). The capital you would tie up if every put were assigned at strike.') + + tile('', 'Portfolio APR', + s.portfolioAPR !== null ? s.portfolioAPR.toFixed(1) + '%' : dash, + s.settled > 0 ? 'notional-weighted · ' + s.settled + ' settled' : '', + 'Notional-weighted average APR of settled options. Per-option APR = (netPrem / collateral) / DTE × 365. Open options excluded.') + + tile('', 'Return Rate', + s.returnRate !== null ? s.returnRate.toFixed(1) + '%' : dash, + s.settled > 0 ? s.otmCount + ' / ' + s.settled + ' exp OTM' : '', + 'Share of settled options that expired OTM (premium kept, no assignment/call-away). Open options excluded.') + + '
'; } else { // Group trades by month — OPEN trades by open date, settled by expiry date diff --git a/test/integration/pnl-tiles.test.js b/test/integration/pnl-tiles.test.js index 7476ef1..5037a7b 100644 --- a/test/integration/pnl-tiles.test.js +++ b/test/integration/pnl-tiles.test.js @@ -72,6 +72,35 @@ test('Return Rate tile has tooltip + ⓘ glyph', (t) => { assertHasTooltip(findCard(window, /Return Rate/i), /OTM|expired/i); }); +test('Total tab uses hero + supporting trio layout (issue #42)', (t) => { + // Total Premium Collected is the hero (focal point); the other three live in a trio container. + const trades = [ + { id: 1, asset: 'BTC', type: 'PUT', date: '2026-01-01', expiry: '2026-01-15', + dte: 14, strike: 50000, size: 0.1, premium: 120, outcome: 'EXPIRED', + closeCost: 0, platform: 'RYSK' }, + ]; + const { window, teardown } = setupJsdom({ trades }); + t.after(teardown); + + const hero = window.document.querySelector('.ppnl-hero'); + assert.ok(hero, 'expected a .ppnl-hero element on the Total tab'); + const heroLbl = hero.querySelector('.ppnl-lbl'); + assert.match(heroLbl.textContent, /Total Premium Collected/i, + `hero should be Total Premium Collected, got "${heroLbl.textContent}"`); + + const trio = window.document.querySelector('.ppnl-trio'); + assert.ok(trio, 'expected a .ppnl-trio container'); + const trioLabels = Array.from(trio.querySelectorAll('.ppnl-lbl')).map(l => l.textContent.trim()); + assert.strictEqual(trioLabels.length, 3, `trio should hold 3 tiles, got ${trioLabels.length}`); + assert.ok(trioLabels.some(l => /Total Notional/i.test(l))); + assert.ok(trioLabels.some(l => /Portfolio APR/i.test(l))); + assert.ok(trioLabels.some(l => /Return Rate/i.test(l))); + + // Hero retains its tooltip + ⓘ glyph affordance. + assert.ok(hero.classList.contains('has-tip'), 'hero should keep has-tip styling'); + assert.ok(hero.querySelector('.ppnl-tip-ico'), 'hero should keep the ⓘ glyph'); +}); + test('Total tab no longer renders Realised/Unrealised/Total P&L cards (issue #40)', (t) => { // Per #40, the hero band is the canonical Realised/Unrealised/Total surface. // The Total tab must not duplicate them.