diff --git a/public/admin/assets/css/intel.css b/public/admin/assets/css/intel.css index 4c6d97d..18b8175 100644 --- a/public/admin/assets/css/intel.css +++ b/public/admin/assets/css/intel.css @@ -75,10 +75,45 @@ /* ── graph view ── */ +/* on the graph page we flex the page chrome top-to-bottom so the layout + grows to fill the viewport. each ancestor in the chain needs min-height:0 + so the flex child can actually shrink/stretch. */ + +body.page-graph { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +body.page-graph main.content { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + padding: 12px; +} + +body.page-graph #intel-content { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +body.page-graph #intel-graph-layout { + flex: 1; + min-height: 0; +} + +body.page-graph #intel-graph-svg-wrap, +body.page-graph #graph-info { + height: auto; /* let flex sizing win */ +} + #intel-graph-layout { display: flex; - gap: 14px; + gap: 12px; } #intel-graph-svg-wrap { @@ -88,7 +123,7 @@ border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; - height: 600px; + height: 600px; /* fallback for any other page that might reuse this */ } #intel-graph-svg { @@ -498,90 +533,187 @@ color: var(--muted-dark); } -.signal-card-detail { - display: none; - padding: 0 16px 16px; - border-top: 1px solid var(--border-light); - padding-top: 14px; -} - -.signal-card.expanded .signal-card-detail { display: block; } - -.signal-detail-section { - margin-bottom: 12px; -} - .signal-detail-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted-dark); font-weight: 600; - margin-bottom: 5px; + margin-bottom: 6px; } -.signal-detail-section ul { - padding-left: 16px; - margin: 0; - font-size: 12px; - color: var(--muted); - line-height: 1.7; + +/* ── signal detail modal ─────────────────────────────────────────────────── */ +/* wider than the default modal (920px) and split into two columns so the + summary/drivers/risks live alongside the reference event list. + fixed height gives it a predictable aspect ratio and lets each column + own its own scroll. */ + +.signal-modal { + width: 920px; + max-width: 95vw; + height: 620px; + max-height: 92vh; + padding: 0; /* reset default modal padding — we manage it per section */ + overflow: hidden; /* body is the one that scrolls */ + display: flex; + flex-direction: column; } -.signal-detail-section p { +.signal-modal-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 22px 28px 4px; +} + +.signal-modal-company { + font-size: 20px; + font-weight: 700; + letter-spacing: -.01em; + color: var(--foreground); + line-height: 1.1; +} + +.signal-modal-ticker { font-size: 12px; color: var(--muted); + margin-top: 4px; + font-weight: 500; + letter-spacing: .04em; +} + +.signal-modal-head .signal-badge { + font-size: 13px; + padding: 6px 14px; +} + +.signal-modal-tags { + display: flex; + gap: 6px; + flex-wrap: wrap; + padding: 10px 28px 18px; + border-bottom: 1px solid var(--border); +} + + +.signal-modal-body { + flex: 1; + display: grid; + grid-template-columns: 1.2fr 1fr; + min-height: 0; /* required so the columns inside can scroll */ +} + +.signal-modal-col { + padding: 20px 24px; + overflow-y: auto; + min-height: 0; +} + +.signal-modal-col-main { + border-right: 1px solid var(--border); + background: var(--bg-card); +} + +.signal-modal-col-refs { + background: var(--bg-subtle); +} + + +.signal-modal-section { + margin-bottom: 18px; +} + +.signal-modal-section:last-child { margin-bottom: 0; } + +.signal-modal-section p, +#signal-modal-summary { + font-size: 13px; + color: var(--foreground); line-height: 1.6; margin: 0; } -.signal-regen-btn { - margin-top: 10px; - font-size: 12px; - padding: 5px 12px; - background: transparent; - border-color: var(--border); +.signal-modal-section ul { + padding-left: 18px; + margin: 0; + font-size: 13px; color: var(--muted); + line-height: 1.7; } -.signal-regen-btn:hover { color: var(--foreground); background: var(--bg-subtle); } +.signal-modal-meta-row { + display: flex; + gap: 18px; + color: var(--muted-dark); + font-size: 11.5px; + padding-top: 8px; + border-top: 1px dashed var(--border); +} + + +.signal-modal .modal-footer { + margin: 0; + padding: 14px 24px; + border-top: 1px solid var(--border); + flex-shrink: 0; +} + + +/* responsive — stack the two columns on narrow screens */ +@media (max-width: 720px) { + .signal-modal-body { + grid-template-columns: 1fr; + } + .signal-modal-col-main { + border-right: none; + border-bottom: 1px solid var(--border); + } +} .signal-ref-event { border-left: 2px solid var(--border); - padding: 2px 0 6px 8px; - margin-bottom: 8px; + padding: 4px 0 10px 10px; + margin-bottom: 12px; } +.signal-ref-event:last-child { margin-bottom: 0; } + .signal-ref-event-title { - font-size: 12px; + font-size: 13px; color: var(--foreground); - font-weight: 500; + font-weight: 600; display: flex; justify-content: space-between; gap: 8px; align-items: baseline; + line-height: 1.3; } .signal-ref-articles { - padding-left: 14px; - margin: 4px 0 0; - font-size: 11.5px; - color: var(--muted); - line-height: 1.55; + list-style: none; + padding-left: 0; + margin: 6px 0 0; + font-size: 12px; + line-height: 1.5; } .signal-ref-articles li { - margin-bottom: 2px; + padding: 4px 0; + border-bottom: 1px solid var(--border-light); } +.signal-ref-articles li:last-child { border-bottom: none; } + .signal-ref-articles a { - color: var(--muted); + color: var(--accent); text-decoration: none; - border-bottom: 1px dashed var(--border); + display: block; } -.signal-ref-articles a:hover { color: var(--foreground); } +.signal-ref-articles a:hover { text-decoration: underline; } .signal-ref-source { margin-left: 6px; diff --git a/public/admin/assets/js/intel-signals.js b/public/admin/assets/js/intel-signals.js index c326dd3..3770685 100644 --- a/public/admin/assets/js/intel-signals.js +++ b/public/admin/assets/js/intel-signals.js @@ -1,6 +1,12 @@ -// intelligence → signals (trade signal cards) +// intelligence → signals (trade signal cards + detail popover) +// cards are glance-only; clicking opens a modal with the full breakdown. // depends on: app.js, intel-shared.js +// local lookup of signal rows keyed by company_id — so the modal can +// render without re-fetching. references are still lazy-loaded per open. +const signalsById = new Map(); + + async function loadSignals() { let data; try { @@ -9,6 +15,9 @@ async function loadSignals() { data = []; } + signalsById.clear(); + (data || []).forEach(s => signalsById.set(s.company_id, s)); + const grid = document.getElementById("intel-signals-grid"); const empty = document.getElementById("intel-signals-empty"); @@ -22,21 +31,11 @@ async function loadSignals() { grid.innerHTML = data.map(s => { const firstSentence = (s.summary || "").split(/\.\s+/)[0].replace(/\.$/, "") + "."; - const eventTs = s.latest_event_date ? s.latest_event_date.slice(0, 10) : null; - - let drivers = []; - let risks = []; - let predIds = []; - try { drivers = JSON.parse(s.key_drivers || "[]"); } catch (_) {} - try { risks = JSON.parse(s.risk_factors || "[]"); } catch (_) {} - try { predIds = JSON.parse(s.supporting_prediction_ids || "[]"); } catch (_) {} - - const driverItems = drivers.map(d => `
${escapeHtml(s.summary || "—")}
-${predIds.length} predictions · window: 90 days
-Loading…
-Loading…
`; + + document.getElementById("signalOverlay").classList.add("open"); + + loadSignalReferences(companyId); +} + + +function closeSignalModal() { + document.getElementById("signalOverlay").classList.remove("open"); + currentSignal = null; } async function loadSignalReferences(companyId) { - const container = document.getElementById(`signal-refs-${companyId}`); - if (!container || container.dataset.loaded === "1") return; - - container.dataset.loaded = "1"; + const container = document.getElementById("signal-modal-refs"); let data; try { data = await api(`/admin/api/intelligence/signals/${companyId}/references`); } catch (_) { - container.innerHTML = ` -Failed to load references
- `; - container.dataset.loaded = "0"; + container.innerHTML = `Failed to load references
`; return; } + // guard against stale responses if the user closed/switched modals mid-request + if (!currentSignal || currentSignal.company_id !== companyId) return; + const events = data?.events || []; if (!events.length) { - container.innerHTML = ` -No linked events
- `; + container.innerHTML = `No linked events
`; return; } - const blocks = events.map(ev => { + container.innerHTML = events.map(ev => { const dateStr = (ev.event_date || ev.created_at || "").slice(0, 10); const articles = (ev.articles || []).map(a => { - const src = a.source ? `${escapeHtml(a.source)}` : ""; - const pd = a.pub_date ? `${escapeHtml(a.pub_date.slice(0, 10))}` : ""; + const src = a.source ? `${escapeHtml(a.source)}` : ""; + const pd = a.pub_date ? `${escapeHtml(a.pub_date.slice(0, 10))}` : ""; return `