diff --git a/src/css/styles.css b/src/css/styles.css index 835cd4d..16835b2 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -246,15 +246,9 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)} .hcard-hero{padding:13px 13px 9px} .hcard-hero-lbl{font-size:.55rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:var(--mu);margin-bottom:4px;font-family:var(--mono)} .hcard-hero-val{font-size:1.3rem;font-weight:700;color:var(--orange);letter-spacing:-.5px;line-height:1;font-family:var(--mono)} -.hcard-hero-sub{font-size:.6rem;color:var(--mu2);margin-top:5px;font-family:var(--mono)} -.hcard-hero-sub span{color:var(--green);font-weight:600} -.hcard-bar-wrap{padding:0 13px 12px} -.hcard-bar-labels{display:flex;justify-content:space-between;font-size:.54rem;color:var(--mu);letter-spacing:.8px;text-transform:uppercase;margin-bottom:5px;font-family:var(--mono)} -.hcard-bar-track{height:3px;background:var(--bd2);overflow:hidden} -.hcard-bar-fill{height:100%;background:linear-gradient(90deg,var(--green),rgba(0,214,143,.5))} -.hcard-stats{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--bd)} +.hcard-stats{display:grid;grid-template-columns:1fr 1fr 1fr;border-top:1px solid var(--bd)} .hcard-stat{padding:9px 13px} -.hcard-stat:first-child{border-right:1px solid var(--bd)} +.hcard-stat + .hcard-stat{border-left:1px solid var(--bd)} .hcard-stat-lbl{font-size:.54rem;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;color:var(--mu);margin-bottom:3px;font-family:var(--mono)} .hcard-stat-val{font-size:.8rem;font-weight:600;color:var(--text2);font-family:var(--mono)} .hcard-stat-val.green{color:var(--green)} diff --git a/src/js/06-render-table.js b/src/js/06-render-table.js index 028ade0..a10725c 100644 --- a/src/js/06-render-table.js +++ b/src/js/06-render-table.js @@ -312,8 +312,7 @@ function rTable(displayRows, streams, lots) { assetLots.forEach(lot => { openLotCount++; const nc = lot.netCost; - const reduction = lot.costBasis - nc; - const reductionPct = lot.costBasis > 0 ? (reduction / lot.costBasis * 100) : 0; + const reductionPct = lot.costBasis > 0 ? ((lot.costBasis - nc) / lot.costBasis * 100) : 0; const lotBadge = totalAssetLots > 1 ? 'Lot ' + lot.lotNum + '' : ''; const holdingTrade = trades.find(t => t.id === lot.tradeIds[0]); const isManualHolding = holdingTrade && holdingTrade.type === 'HOLDING'; @@ -362,16 +361,12 @@ function rTable(displayRows, streams, lots) { + '
' + '
Net Cost / ' + a + '
' + '
$' + fmt(nc) + '
' - + '
basis $' + fmt(lot.costBasis) + ' — saved $' + fmt(reduction) + ' (' + reductionPct.toFixed(1) + '%)
' + '
' + spotBlock - + '
' - + '
Premium reduction' + reductionPct.toFixed(1) + '%
' - + '
' - + '
' + '
' - + '
CC Premiums
$' + fmt(lot.lotPremiums) + '
' + '
Cost Basis
$' + fmt(lot.costBasis) + '
' + + '
CC Premiums
$' + fmt(lot.lotPremiums) + '
' + + '
Premium Reduction %
' + reductionPct.toFixed(1) + '%
' + '
' + ''; }); diff --git a/test/integration/holding-card-trim.test.js b/test/integration/holding-card-trim.test.js new file mode 100644 index 0000000..b9f60f7 --- /dev/null +++ b/test/integration/holding-card-trim.test.js @@ -0,0 +1,70 @@ +const test = require('node:test'); +const assert = require('node:assert'); +const { setupJsdom } = require('../helpers/setupJsdom'); + +// Issue #51: Trim redundant content from holding cards. +// - Hero sub-line ("basis $X — saved $Y") removed. +// - Premium-reduction bar removed. +// - Footer stats: Cost Basis | CC Premiums | Premium Reduction % (3 cols, in order). + +function getHcard(window) { + const card = window.document.querySelector('.hcard'); + assert.ok(card, 'expected a .hcard element on the page'); + return card; +} + +const baseTrades = [ + { id: 1, asset: 'ETH', type: 'HOLDING', date: '2026-01-01', expiry: '', + dte: null, strike: 3000, size: 1, premium: 0, outcome: 'OPEN', + closeCost: 0, platform: 'SPOT' }, + { id: 2, asset: 'ETH', type: 'CALL', date: '2026-01-15', expiry: '2026-01-29', + dte: 14, strike: 3500, size: 1, premium: 60, outcome: 'EXPIRED', + closeCost: 0, platform: 'RYSK' }, +]; + +test('holding card has no hero sub-line', (t) => { + const { window, teardown } = setupJsdom({ trades: baseTrades }); + t.after(teardown); + const card = getHcard(window); + assert.strictEqual(card.querySelector('.hcard-hero-sub'), null, + '.hcard-hero-sub should be removed'); +}); + +test('holding card has no premium-reduction bar', (t) => { + const { window, teardown } = setupJsdom({ trades: baseTrades }); + t.after(teardown); + const card = getHcard(window); + assert.strictEqual(card.querySelector('.hcard-bar-wrap'), null, + '.hcard-bar-wrap should be removed'); +}); + +test('holding card footer has 3 stats: Cost Basis | CC Premiums | Premium Reduction %', (t) => { + const { window, teardown } = setupJsdom({ trades: baseTrades }); + t.after(teardown); + const card = getHcard(window); + + const stats = card.querySelectorAll('.hcard-stats .hcard-stat'); + assert.strictEqual(stats.length, 3, 'footer should have exactly 3 stat cells'); + + const labels = Array.from(stats).map(s => + s.querySelector('.hcard-stat-lbl').textContent.trim()); + assert.deepStrictEqual(labels, ['Cost Basis', 'CC Premiums', 'Premium Reduction %'], + 'stat labels must appear in this order'); + + // Net cost = 3000 - 60 = 2940; reduction = 60/3000 = 2.0%. + const values = Array.from(stats).map(s => + s.querySelector('.hcard-stat-val').textContent.trim()); + assert.match(values[0], /\$3,?000(\.\d+)?$/, `Cost Basis value, got "${values[0]}"`); + assert.match(values[1], /\$60(\.\d+)?$/, `CC Premiums value, got "${values[1]}"`); + assert.match(values[2], /^2\.0%$/, `Reduction % value, got "${values[2]}"`); +}); + +test('Cost Basis appears exactly once on the card', (t) => { + const { window, teardown } = setupJsdom({ trades: baseTrades }); + t.after(teardown); + const card = getHcard(window); + const labels = Array.from(card.querySelectorAll('.hcard-stat-lbl')) + .map(el => el.textContent.trim()); + const cbCount = labels.filter(l => /^Cost Basis$/i.test(l)).length; + assert.strictEqual(cbCount, 1, 'Cost Basis should appear exactly once'); +});