Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)}
.hcard-hint b{color:var(--orange);font-weight:700}
.hcard-hint-ok{color:var(--green)}
.lot-badge{font-size:.5rem;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;padding:2px 6px;background:var(--s3);border:1px solid var(--bd2);color:var(--mu2);font-family:var(--mono)}
.hcard-spot-muted{color:var(--mu2)}

/* Wide-card layout: when <=2 visible open lots and viewport >=720px,
render each card as a full-width horizontal row with 4 columns. */
@media (min-width:720px){
.holdings-grid--wide{grid-template-columns:1fr}
.holdings-grid--wide .hcard{display:grid;grid-template-columns:minmax(180px,1fr) minmax(140px,1fr) minmax(220px,1.4fr) minmax(260px,1.4fr);align-items:stretch}
.holdings-grid--wide .hcard-hd{border-bottom:none;border-right:1px solid var(--bd);flex-direction:column;align-items:flex-start;gap:8px;justify-content:center}
.holdings-grid--wide .hcard-hero{border-right:1px solid var(--bd);display:flex;flex-direction:column;justify-content:center;padding:13px}
.holdings-grid--wide .hcard-spot{border-top:none;border-right:1px solid var(--bd);padding:13px;display:flex;flex-direction:column;justify-content:center}
.holdings-grid--wide .hcard-stats{border-top:none;grid-template-columns:1fr 1fr 1fr;align-items:center}
}

/* MERGE LOTS */
.btn-merge{font-size:.62rem;font-family:var(--mono);font-weight:700;padding:3px 10px;border-radius:5px;cursor:pointer;border:1px solid var(--ob);color:var(--orange);background:var(--od);transition:all .15s;white-space:nowrap;letter-spacing:.5px;text-transform:uppercase}
Expand Down
14 changes: 11 additions & 3 deletions src/js/06-render-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,16 @@ function rTable(displayRows, streams, lots) {
: '';
// Live spot, unrealized P&L vs net cost, breakeven hint
const spot = livePrices[a];
let spotBlock = '';
if (spot) {
let spotBlock;
if (!spot) {
spotBlock = '<div class="hcard-spot hcard-spot-placeholder">'
+ '<div class="hcard-spot-row">'
+ '<span class="hcard-spot-lbl">Spot</span>'
+ '<span class="hcard-spot-val hcard-spot-muted">&mdash;</span>'
+ '</div>'
+ '<div class="hcard-hint">spot unavailable</div>'
+ '</div>';
} else {
const pnlPerToken = spot - nc;
const pnlTotal = pnlPerToken * lot.size;
const pnlPct = nc > 0 ? (pnlPerToken / nc * 100) : 0;
Expand Down Expand Up @@ -377,7 +385,7 @@ function rTable(displayRows, streams, lots) {
+ '<div class="sec-ttl"><span class="dot dg"></span>Holdings</div>'
+ '<span style="font-size:.6rem;color:var(--mu);font-family:var(--mono)">' + openLotCount + ' open lot' + (openLotCount !== 1 ? 's' : '') + '</span>'
+ '</div>'
+ '<div class="holdings-grid">' + cardsHtml + '</div>'
+ '<div class="holdings-grid' + (openLotCount <= 2 ? ' holdings-grid--wide' : '') + '">' + cardsHtml + '</div>'
+ (mergesHtml ? '<div class="holdings-merges">' + mergesHtml + '</div>' : '')
+ '</div>';
} else {
Expand Down
100 changes: 100 additions & 0 deletions test/integration/holding-card-wide.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const test = require('node:test');
const assert = require('node:assert');
const { setupJsdom } = require('../helpers/setupJsdom');

// Issue #52: Wide-card layout for holding cards at low lot counts.
// JS-side contract: when the number of *visible* open lots is <= 2, the
// .holdings-grid element gains the class `holdings-grid--wide`. The CSS
// activates the wide row layout at >= 720px viewport width via @media.

const ethHolding = {
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',
};

function btcHolding(id, n) {
return { id, asset: 'BTC', type: 'HOLDING', date: '2026-0' + n + '-01',
expiry: '', dte: null, strike: 60000 + id, size: 0.1, premium: 0,
outcome: 'OPEN', closeCost: 0, platform: 'SPOT' };
}

function getGrid(window) {
const g = window.document.querySelector('.holdings-grid');
assert.ok(g, 'expected a .holdings-grid element');
return g;
}

test('1 visible open lot: holdings-grid has --wide class', (t) => {
const { window, teardown } = setupJsdom({ trades: [ethHolding] });
t.after(teardown);
const grid = getGrid(window);
assert.ok(grid.classList.contains('holdings-grid--wide'),
'grid should have holdings-grid--wide class with 1 visible lot');
});

test('2 visible open lots: holdings-grid has --wide class', (t) => {
const { window, teardown } = setupJsdom({
trades: [ethHolding, btcHolding(2, 2)],
});
t.after(teardown);
const grid = getGrid(window);
assert.ok(grid.classList.contains('holdings-grid--wide'),
'grid should have holdings-grid--wide class with 2 visible lots');
});

test('3 visible open lots: holdings-grid does NOT have --wide class', (t) => {
const { window, teardown } = setupJsdom({
trades: [ethHolding, btcHolding(2, 2), btcHolding(3, 3)],
});
t.after(teardown);
const grid = getGrid(window);
assert.ok(!grid.classList.contains('holdings-grid--wide'),
'grid should NOT have holdings-grid--wide class with 3+ visible lots');
});

test('asset filter honored: BTC filter w/ 1 BTC lot among many → wide', (t) => {
const { window, teardown } = setupJsdom({
trades: [ethHolding, btcHolding(2, 2), btcHolding(3, 3),
{ ...ethHolding, id: 4, date: '2026-02-01' }],
});
t.after(teardown);
// Filter to BTC: should reduce visible lots to 2 → wide class applies.
window.setFilter('BTC');
const grid = getGrid(window);
assert.ok(grid.classList.contains('holdings-grid--wide'),
'asset filter should narrow visible-lot count for the threshold');
});

test('missing spot: card renders stable Spot — placeholder', (t) => {
// No livePrices stub → spot is undefined.
const { window, teardown } = setupJsdom({ trades: [ethHolding] });
t.after(teardown);
const card = window.document.querySelector('.hcard');
const spot = card.querySelector('.hcard-spot');
assert.ok(spot, 'spot block should render even when price missing');
assert.match(spot.textContent, /Spot\s*—/,
'placeholder should show "Spot —"');
assert.match(spot.textContent, /spot unavailable/i,
'placeholder sub-line should read "spot unavailable"');
});

test('wide layout preserves edit btn, lot badge, merge btn', (t) => {
// 2 ETH lots: edit btn (HOLDING), lot badge (>1 lot), merge btn (>=2 open).
const { window, teardown } = setupJsdom({
trades: [
ethHolding,
{ ...ethHolding, id: 2, date: '2026-02-01', strike: 3200 },
],
});
t.after(teardown);
const grid = getGrid(window);
assert.ok(grid.classList.contains('holdings-grid--wide'),
'precondition: 2 lots → wide');
const cards = window.document.querySelectorAll('.hcard');
assert.strictEqual(cards.length, 2);
assert.ok(cards[0].querySelector('.hcard-edit'), 'edit button present');
assert.ok(cards[0].querySelector('.lot-badge'), 'lot badge present');
assert.ok(window.document.querySelector('.btn-merge'),
'merge button present');
});
Loading