phase-1: optimize code
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
<script>
|
||||
let { ctx, collapsible = false } = $props();
|
||||
|
||||
let expanded = $state(!collapsible); // collapsed by default when collapsible=true
|
||||
|
||||
const cards = $derived.by(() => {
|
||||
const b = ctx?.benchmarks ?? {};
|
||||
return [
|
||||
{
|
||||
label: '10Y Yield',
|
||||
value: ctx?.riskFreeRate != null ? ctx.riskFreeRate.toFixed(2) + '%' : '—',
|
||||
tip: 'US 10-year Treasury yield — the risk-free rate benchmark. Higher = tighter conditions for stocks and bonds.',
|
||||
},
|
||||
{
|
||||
label: 'VIX',
|
||||
value: ctx?.vixLevel?.toFixed(1) ?? '—',
|
||||
tip: 'CBOE Volatility Index — measures expected market volatility. Above 20 = elevated fear; above 30 = high stress.',
|
||||
},
|
||||
{
|
||||
label: 'S&P 500',
|
||||
value: ctx?.sp500Price?.toLocaleString() ?? '—',
|
||||
tip: 'Live S&P 500 index price — broad US large-cap benchmark.',
|
||||
},
|
||||
{
|
||||
label: 'S&P P/E',
|
||||
value: b.marketPE != null ? b.marketPE.toFixed(1) + 'x' : '—',
|
||||
tip: 'Trailing P/E ratio of SPY. Used to set the INFLATED mode P/E gate (S&P P/E × 1.5 in normal rates).',
|
||||
},
|
||||
{
|
||||
label: 'Tech P/E',
|
||||
value: b.techPE != null ? b.techPE.toFixed(1) + 'x' : '—',
|
||||
tip: 'Trailing P/E of XLK (tech sector ETF). Sets the tech-sector gate in INFLATED mode (XLK P/E × 1.3).',
|
||||
},
|
||||
{
|
||||
label: 'REIT Yield',
|
||||
value: b.reitYield != null ? b.reitYield.toFixed(2) + '%' : '—',
|
||||
tip: 'Dividend yield of XLRE (real estate ETF). Used as the REIT minimum yield gate in INFLATED mode.',
|
||||
},
|
||||
{
|
||||
label: 'IG Spread',
|
||||
value: b.igSpread != null ? b.igSpread.toFixed(2) + '%' : '—',
|
||||
tip: 'Investment-grade bond spread (LQD yield − 10Y yield). Sets the bond minimum spread gate in INFLATED mode.',
|
||||
},
|
||||
{
|
||||
label: 'Rate Regime',
|
||||
value: ctx?.rateRegime ?? '—',
|
||||
tip: 'HIGH (>4.5%) compresses P/E gates and tightens bond/REIT requirements. NORMAL uses looser INFLATED gates.',
|
||||
},
|
||||
{
|
||||
label: 'Volatility',
|
||||
value: ctx?.volatilityRegime ?? '—',
|
||||
tip: 'Derived from VIX level — LOW (<15), NORMAL (15–25), HIGH (>25). Informational; not currently gating scores.',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="ctx-wrap">
|
||||
{#if collapsible}
|
||||
<button class="ctx-toggle" onclick={() => expanded = !expanded}>
|
||||
<span class="ctx-toggle-label">Market Context</span>
|
||||
<span class="ctx-toggle-chevron">{expanded ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if expanded}
|
||||
<div class="grid">
|
||||
{#each cards as c}
|
||||
<div class="card">
|
||||
<div class="label-row">
|
||||
<span class="label">{c.label}</span>
|
||||
<span class="tip-wrap">
|
||||
<span class="tip-anchor">?</span>
|
||||
<span class="tip-box">{c.tip}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="value">{c.value}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ctx-wrap { margin-bottom: 20px; }
|
||||
|
||||
/* ── Collapsible toggle ─────────────────────────────────────────── */
|
||||
.ctx-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ctx-toggle-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.ctx-toggle-chevron {
|
||||
font-size: 9px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* ── Cards grid ────────────────────────────────────────────────── */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card { background: #1e293b; border-radius: 8px; padding: 12px 14px; }
|
||||
|
||||
.label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* ── Tooltip ──────────────────────────────────────────────────── */
|
||||
.tip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tip-anchor {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
color: #475569;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.tip-box {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 220px;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.tip-box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: #334155;
|
||||
}
|
||||
|
||||
.tip-wrap:hover .tip-box { display: block; }
|
||||
|
||||
.value { font-size: 17px; font-weight: 700; color: #f1f5f9; margin-top: 4px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user