feat(ui): redesign dashboard in app-style shell
This commit is contained in:
@@ -728,6 +728,7 @@ The `docs/` folder contains dashboard screenshots referenced by this README. The
|
|||||||
| File | Description |
|
| File | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `docs/dashboard.png` | Full operator dashboard - hero image at the top of this README |
|
| `docs/dashboard.png` | Full operator dashboard - hero image at the top of this README |
|
||||||
|
| `docs/design/modrinth-app-final-concept.png` | Final app-style design reference used for the current shell |
|
||||||
| `docs/boot.png` | Boot sequence animation |
|
| `docs/boot.png` | Boot sequence animation |
|
||||||
| `docs/map.png` | Worldview map with marker types and flight arcs |
|
| `docs/map.png` | Worldview map with marker types and flight arcs |
|
||||||
| `docs/globe.png` | 3D WebGL globe view with atmosphere glow and markers |
|
| `docs/globe.png` | 3D WebGL globe view with atmosphere glow and markers |
|
||||||
|
|||||||
BIN
dashboard/public/assets/app-shell-texture.png
Normal file
BIN
dashboard/public/assets/app-shell-texture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -498,6 +498,100 @@ body[data-view="markets"] .lp-macro,body[data-view="markets"] .lp-ideas{max-widt
|
|||||||
.map-container{min-height:420px}
|
.map-container{min-height:420px}
|
||||||
.map-legend{left:8px;right:8px;bottom:8px}
|
.map-legend{left:8px;right:8px;bottom:8px}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FINAL APP-SHELL FIDELITY PASS */
|
||||||
|
body::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background-image:linear-gradient(180deg,rgba(10,13,18,.72),rgba(10,13,18,.88)),url('./assets/app-shell-texture.png');background-size:auto,420px 420px;background-blend-mode:normal,soft-light;opacity:.5}
|
||||||
|
#main.app-root{grid-template-columns:150px minmax(0,1fr);gap:0;background:linear-gradient(180deg,#171a20,#11141a)}
|
||||||
|
.app-sidebar{width:150px;padding:28px 20px 24px;background:linear-gradient(180deg,#151a21,#10151c);border-right:1px solid rgba(124,138,154,.24);box-shadow:inset -1px 0 rgba(255,255,255,.03)}
|
||||||
|
.rail-live{height:34px;min-width:86px;padding:0 13px;border-radius:999px;background:rgba(255,255,255,.035);display:flex;align-items:center;justify-content:center;gap:8px;color:var(--app-muted);font-weight:800;font-size:12px}
|
||||||
|
.app-nav{position:relative;gap:18px;margin-top:6px}
|
||||||
|
.nav-glider{position:absolute;left:50%;top:0;width:86px;height:86px;border-radius:999px;background:radial-gradient(circle at 50% 46%,rgba(27,217,106,.42),rgba(27,217,106,.2) 48%,rgba(27,217,106,0) 72%);box-shadow:0 0 34px rgba(27,217,106,.34),inset 0 0 0 1px rgba(27,217,106,.22);transform:translate(-50%,0);transition:transform .58s cubic-bezier(.2,.9,.2,1),opacity .28s ease;pointer-events:none}
|
||||||
|
.nav-glider::after{content:'';position:absolute;inset:13px;border-radius:999px;border:2px solid rgba(27,217,106,.8);box-shadow:0 0 22px rgba(27,217,106,.42)}
|
||||||
|
.nav-item{position:relative;z-index:1;width:96px;height:92px;border-radius:28px;background:transparent;color:#aeb7c4;gap:8px;transition:transform .24s ease,color .24s ease,filter .24s ease}
|
||||||
|
.nav-item:hover{background:transparent;color:#f3f7fa;transform:translateY(-2px)}
|
||||||
|
.nav-item.active{background:transparent;color:#fff;filter:drop-shadow(0 12px 28px rgba(27,217,106,.18))}
|
||||||
|
.nav-icon{width:46px;height:46px;border:0;border-radius:16px;display:grid;place-items:center;background:rgba(255,255,255,.035);box-shadow:inset 0 0 0 2px currentColor;transition:background .24s ease,box-shadow .24s ease,transform .24s ease}
|
||||||
|
.nav-icon svg{width:26px;height:26px;fill:none;stroke:currentColor;stroke-width:2.05;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.nav-item.active .nav-icon{background:rgba(27,217,106,.18);box-shadow:inset 0 0 0 2px var(--app-green),0 0 0 10px rgba(27,217,106,.04);transform:scale(1.04)}
|
||||||
|
.nav-item small{font-size:11px;font-weight:800;line-height:1;color:currentColor;text-shadow:0 1px 14px rgba(0,0,0,.35)}
|
||||||
|
.sidebar-status{width:96px;border-top-color:rgba(124,138,154,.28);font-size:12px}
|
||||||
|
.app-shell{margin:14px 16px 14px 0;border-radius:32px;background:#181c23;border-color:#35404c;box-shadow:0 24px 80px rgba(0,0,0,.34),inset 0 1px rgba(255,255,255,.035)}
|
||||||
|
.topbar{padding:30px 34px 18px;grid-template-columns:minmax(280px,420px) minmax(280px,1fr) auto;background:linear-gradient(180deg,#20242c,#1e222a);gap:26px}
|
||||||
|
.brand{font-size:28px;letter-spacing:-.01em}
|
||||||
|
.view-subtitle{max-width:420px;color:#97a2b2}
|
||||||
|
.regime-chip{display:none}
|
||||||
|
.app-search{height:56px;border-radius:18px;background:#1a1f27;border-color:#38424e;font-size:15px;box-shadow:inset 0 1px rgba(255,255,255,.03)}
|
||||||
|
.top-right{grid-column:auto;justify-content:flex-end;align-content:flex-start;gap:10px}
|
||||||
|
.theme-switch,.meta-pill,.guide-btn,.alert-badge,.perf-pill{min-height:43px;border-radius:20px;border-color:#38424e;background:#1d222b;box-shadow:inset 0 1px rgba(255,255,255,.025)}
|
||||||
|
.theme-switch{padding:5px}
|
||||||
|
.theme-btn{min-width:52px;padding:8px 12px;font-size:12px}
|
||||||
|
.theme-btn.active{box-shadow:0 7px 20px rgba(27,217,106,.22)}
|
||||||
|
.alert-badge{background:rgba(255,95,111,.13);border-color:rgba(255,95,111,.38)}
|
||||||
|
.grid{padding:20px 24px 24px;grid-template-columns:300px minmax(0,1fr);gap:18px;background:#181c23}
|
||||||
|
.top-metric-row{grid-column:1/-1;display:grid;grid-template-columns:repeat(6,minmax(130px,1fr));gap:14px}
|
||||||
|
.top-metric-card{position:relative;overflow:hidden;min-height:86px;padding:16px 18px;border:1px solid #333d49;border-radius:19px;background:linear-gradient(145deg,#252b34,#1d232c);box-shadow:inset 0 1px rgba(255,255,255,.035)}
|
||||||
|
.top-metric-card::after{content:'';position:absolute;inset:auto -20% -65% -20%;height:80%;background:radial-gradient(circle,rgba(27,217,106,.13),transparent 62%);opacity:0;transition:opacity .3s ease}
|
||||||
|
.top-metric-card:hover::after{opacity:1}
|
||||||
|
.tm-icon{width:38px;height:38px;border-radius:14px;display:grid;place-items:center;background:rgba(27,217,106,.11);color:var(--app-green);margin-bottom:8px}
|
||||||
|
.tm-icon svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.tm-label{font-size:10px;font-weight:900;letter-spacing:.06em;text-transform:uppercase;color:#98a4b3}
|
||||||
|
.tm-value{margin-top:2px;font-size:20px;font-weight:900;color:#f3f6f8;letter-spacing:-.02em}
|
||||||
|
.tm-sub{font-size:11px;color:#8e99a8}
|
||||||
|
.g-panel,.map-region-bar,.map-container,.glossary-panel{border-color:#333d49;background:linear-gradient(180deg,#252a33,#202630)}
|
||||||
|
.g-panel{border-radius:20px;padding:18px;box-shadow:inset 0 1px rgba(255,255,255,.035)}
|
||||||
|
.map-container{border-radius:22px;background:#0c1118;box-shadow:inset 0 1px rgba(255,255,255,.04),0 18px 48px rgba(0,0,0,.22)}
|
||||||
|
.map-region-bar{border-radius:20px;padding:16px}
|
||||||
|
.layer-item,.site-row,.econ-row,.src-item,.mc,.signal-row,.sm,.idea-card,.ic,.tk-card{background:#202630;border-color:#303946;transition:transform .22s ease,border-color .22s ease,background .22s ease,box-shadow .22s ease}
|
||||||
|
.layer-item:hover,.signal-row:hover,.ic:hover,.tk-card.clickable:hover,.mc:hover{transform:translateY(-2px);border-color:rgba(27,217,106,.32);box-shadow:0 12px 32px rgba(0,0,0,.14)}
|
||||||
|
.layer-count,.site-val,.eval,.sm .smv{font-weight:900;color:var(--app-green)}
|
||||||
|
.sec-head h3{font-size:18px;letter-spacing:-.01em}
|
||||||
|
.badge{background:#1c222b;border-color:#33404c}
|
||||||
|
.right-actions .terminal-output{min-height:72px}
|
||||||
|
.sm .smb span,.mc .mbar span{background:linear-gradient(90deg,#1bd96a,#7ee787)}
|
||||||
|
.regime-chip .blink,.sidebar-status-dot{animation:status-pulse 2.2s ease-in-out infinite}
|
||||||
|
.map-container::after{content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(27,217,106,.035),transparent);transform:translateX(-120%);animation:surface-sweep 8s ease-in-out infinite}
|
||||||
|
body.view-transitioning .grid{animation:view-shift .42s cubic-bezier(.2,.9,.2,1)}
|
||||||
|
body.theme-transitioning .app-shell{animation:theme-wash .42s ease}
|
||||||
|
.motion-stagger{animation:panel-rise .52s cubic-bezier(.2,.9,.2,1) both}
|
||||||
|
.alert-badge,.delta-badge.new{animation:soft-alert 2.4s ease-in-out infinite}
|
||||||
|
@keyframes status-pulse{0%,100%{box-shadow:0 0 0 0 rgba(27,217,106,.32)}50%{box-shadow:0 0 0 7px rgba(27,217,106,0)}}
|
||||||
|
@keyframes surface-sweep{0%,72%{transform:translateX(-120%)}100%{transform:translateX(120%)}}
|
||||||
|
@keyframes panel-rise{from{opacity:0;transform:translateY(16px) scale(.985);filter:blur(4px)}to{opacity:1;transform:none;filter:none}}
|
||||||
|
@keyframes view-shift{from{opacity:.18;transform:translateX(18px) scale(.985);filter:blur(8px)}to{opacity:1;transform:none;filter:none}}
|
||||||
|
@keyframes theme-wash{0%{filter:brightness(.75) saturate(.75)}100%{filter:none}}
|
||||||
|
@keyframes soft-alert{0%,100%{box-shadow:inset 0 1px rgba(255,255,255,.025)}50%{box-shadow:0 0 0 5px rgba(255,95,111,.05),inset 0 1px rgba(255,255,255,.025)}}
|
||||||
|
body.low-perf .motion-stagger,body.low-perf .alert-badge,body.low-perf .delta-badge.new,body.low-perf .map-container::after,body.low-perf .regime-chip .blink,body.low-perf .sidebar-status-dot{animation:none!important}
|
||||||
|
body.low-perf .nav-glider,body.low-perf .nav-item,body.low-perf .layer-item,body.low-perf .signal-row,body.low-perf .ic,body.low-perf .tk-card,body.low-perf .mc{transition:none!important}
|
||||||
|
|
||||||
|
@media(min-width:1320px){
|
||||||
|
.grid{grid-template-columns:300px minmax(0,1fr) 380px}
|
||||||
|
#rightRail{grid-column:auto}
|
||||||
|
}
|
||||||
|
@media(max-width:1240px){
|
||||||
|
.topbar{grid-template-columns:1fr;gap:14px}
|
||||||
|
.top-right{justify-content:flex-start}
|
||||||
|
.top-metric-row{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||||
|
}
|
||||||
|
@media(max-width:760px){
|
||||||
|
body::before{background-size:auto,300px 300px}
|
||||||
|
#main.app-root{display:block;padding:0 0 88px}
|
||||||
|
.app-sidebar{width:auto;height:78px;padding:8px 12px}
|
||||||
|
.rail-live,.sidebar-status{display:none}
|
||||||
|
.app-nav{margin:0;gap:6px}
|
||||||
|
.nav-glider{left:0;width:58px;height:58px}
|
||||||
|
.nav-glider::after{inset:8px}
|
||||||
|
.nav-item{width:56px;height:58px;border-radius:19px}
|
||||||
|
.nav-icon{width:30px;height:30px;border-radius:10px}
|
||||||
|
.nav-icon svg{width:18px;height:18px}
|
||||||
|
.nav-item small{display:none}
|
||||||
|
.app-shell{margin:10px;border-radius:24px}
|
||||||
|
.topbar{padding:22px}
|
||||||
|
.top-metric-row{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
|
||||||
|
.top-metric-card{min-height:76px;padding:13px}
|
||||||
|
}
|
||||||
|
@media(prefers-reduced-motion:reduce){
|
||||||
|
*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body data-theme="dark" data-view="home">
|
<body data-theme="dark" data-view="home">
|
||||||
@@ -510,14 +604,15 @@ body[data-view="markets"] .lp-macro,body[data-view="markets"] .lp-ideas{max-widt
|
|||||||
<div class="bg-grid" id="bgGrid"></div>
|
<div class="bg-grid" id="bgGrid"></div>
|
||||||
<div id="main" class="app-root">
|
<div id="main" class="app-root">
|
||||||
<aside class="app-sidebar" aria-label="Primary views">
|
<aside class="app-sidebar" aria-label="Primary views">
|
||||||
<div class="app-brand-mark"><span>IT</span></div>
|
<div class="rail-live"><span class="sidebar-status-dot"></span><span>Live</span></div>
|
||||||
<nav class="app-nav" id="appNav">
|
<nav class="app-nav" id="appNav">
|
||||||
<button class="nav-item active" data-view-target="home" onclick="setAppView('home')" title="Home"><span>H</span><small>Home</small></button>
|
<div class="nav-glider" id="navGlider" aria-hidden="true"></div>
|
||||||
<button class="nav-item" data-view-target="worldview" onclick="setAppView('worldview')" title="Worldview"><span>W</span><small>World</small></button>
|
<button class="nav-item active" data-view-target="home" onclick="setAppView('home')" title="Home" aria-label="Home"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/><path d="M9 21v-7h6v7"/></svg></span><small>Home</small></button>
|
||||||
<button class="nav-item" data-view-target="sources" onclick="setAppView('sources')" title="Sources"><span>S</span><small>Sources</small></button>
|
<button class="nav-item" data-view-target="worldview" onclick="setAppView('worldview')" title="Worldview" aria-label="Worldview"><span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3c2.8 2.6 4.2 5.6 4.2 9S14.8 18.4 12 21"/><path d="M12 3c-2.8 2.6-4.2 5.6-4.2 9S9.2 18.4 12 21"/></svg></span><small>Worldview</small></button>
|
||||||
<button class="nav-item" data-view-target="signals" onclick="setAppView('signals')" title="Signals"><span>I</span><small>Signals</small></button>
|
<button class="nav-item" data-view-target="sources" onclick="setAppView('sources')" title="Sources" aria-label="Sources"><span class="nav-icon"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="7" ry="3"/><path d="M5 5v6c0 1.7 3.1 3 7 3s7-1.3 7-3V5"/><path d="M5 11v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6"/></svg></span><small>Sources</small></button>
|
||||||
<button class="nav-item" data-view-target="markets" onclick="setAppView('markets')" title="Markets"><span>M</span><small>Markets</small></button>
|
<button class="nav-item" data-view-target="signals" onclick="setAppView('signals')" title="Signals" aria-label="Signals"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M3 13h4l3-8 4 14 3-6h4"/><path d="M4 20h16"/></svg></span><small>Signals</small></button>
|
||||||
<button class="nav-item" data-view-target="ops" onclick="setAppView('ops')" title="Ops"><span>O</span><small>Ops</small></button>
|
<button class="nav-item" data-view-target="markets" onclick="setAppView('markets')" title="Markets" aria-label="Markets"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 19V5"/><path d="M4 19h17"/><path d="M8 16v-5"/><path d="M13 16V8"/><path d="M18 16v-9"/></svg></span><small>Markets</small></button>
|
||||||
|
<button class="nav-item" data-view-target="ops" onclick="setAppView('ops')" title="Ops" aria-label="Ops"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 5h16v14H4z"/><path d="m8 9 3 3-3 3"/><path d="M13 15h4"/></svg></span><small>Ops</small></button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="sidebar-status">
|
<div class="sidebar-status">
|
||||||
<span class="sidebar-status-dot"></span>
|
<span class="sidebar-status-dot"></span>
|
||||||
@@ -527,6 +622,7 @@ body[data-view="markets"] .lp-macro,body[data-view="markets"] .lp-ideas{max-widt
|
|||||||
<main class="app-shell">
|
<main class="app-shell">
|
||||||
<div class="topbar" id="topbar"></div>
|
<div class="topbar" id="topbar"></div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
<div class="top-metric-row" id="topMetricRow"></div>
|
||||||
<div class="col" id="leftRail"></div>
|
<div class="col" id="leftRail"></div>
|
||||||
<div class="col" id="centerCol">
|
<div class="col" id="centerCol">
|
||||||
<div class="map-region-bar" id="mapRegionBar"></div>
|
<div class="map-region-bar" id="mapRegionBar"></div>
|
||||||
@@ -641,10 +737,80 @@ function applyTheme(pref = themePreference){
|
|||||||
function setTheme(pref){
|
function setTheme(pref){
|
||||||
themePreference = pref;
|
themePreference = pref;
|
||||||
localStorage.setItem('intelligence_terminal_theme', pref);
|
localStorage.setItem('intelligence_terminal_theme', pref);
|
||||||
|
document.body.classList.add('theme-transitioning');
|
||||||
|
window.setTimeout(() => document.body.classList.remove('theme-transitioning'), 480);
|
||||||
applyTheme(pref);
|
applyTheme(pref);
|
||||||
renderTopbar();
|
renderTopbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function iconSvg(name){
|
||||||
|
const icons = {
|
||||||
|
sweep:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>',
|
||||||
|
delta:'<svg viewBox="0 0 24 24"><path d="M4 15l5-5 4 4 7-8"/><path d="M16 6h4v4"/></svg>',
|
||||||
|
signals:'<svg viewBox="0 0 24 24"><path d="M3 13h4l3-8 4 14 3-6h4"/></svg>',
|
||||||
|
regions:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3c2.8 2.6 4.2 5.6 4.2 9S14.8 18.4 12 21"/><path d="M12 3c-2.8 2.6-4.2 5.6-4.2 9S9.2 18.4 12 21"/></svg>',
|
||||||
|
alert:'<svg viewBox="0 0 24 24"><path d="M12 3 5 6v5c0 4.7 3 8.3 7 10 4-1.7 7-5.3 7-10V6z"/><path d="M12 8v5"/><path d="M12 16h.01"/></svg>',
|
||||||
|
status:'<svg viewBox="0 0 24 24"><path d="M12 3 5 6v6c0 4.2 2.8 7.1 7 9 4.2-1.9 7-4.8 7-9V6z"/><path d="m8.5 12 2.2 2.2 4.8-5"/></svg>'
|
||||||
|
};
|
||||||
|
return icons[name] || icons.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTopMetrics(){
|
||||||
|
const el = document.getElementById('topMetricRow');
|
||||||
|
if(!el) return;
|
||||||
|
const delta = D.delta?.summary;
|
||||||
|
const signalCount = (D.tSignals||[]).length;
|
||||||
|
const activeRegions = ['world','americas','europe','middleEast','asiaPacific','africa'].length - (D.meta.sourcesQueried === 0 ? 1 : 0);
|
||||||
|
const highAlert = (D.tg?.urgent?.length || 0) > 0 || (D.noaa?.totalAlerts || 0) > 0 || (D.acled?.totalFatalities || 0) > 0;
|
||||||
|
const sourceState = D.meta.sourcesQueried ? `${D.meta.sourcesOk}/${D.meta.sourcesQueried}` : '0/0';
|
||||||
|
const cards = [
|
||||||
|
{icon:'sweep',label:'Sweep',value:`${(D.meta.totalDurationMs/1000).toFixed(1)}s`,sub:'Last run'},
|
||||||
|
{icon:'delta',label:'Delta',value:delta?.direction ? delta.direction.replace('-', ' ') : 'Baseline',sub:'Change'},
|
||||||
|
{icon:'signals',label:'Signals',value:signalCount || (D.tg?.urgent?.length || 0),sub:'New'},
|
||||||
|
{icon:'regions',label:'Regions',value:activeRegions,sub:'Active'},
|
||||||
|
{icon:'alert',label:'Alert posture',value:highAlert ? 'High alert' : 'Normal',sub:highAlert ? 'Elevated' : 'Stable',tone:highAlert?'danger':''},
|
||||||
|
{icon:'status',label:'System status',value:D.meta.sourcesFailed ? 'Degraded' : 'Operational',sub:`Sources ${sourceState}`,tone:D.meta.sourcesFailed?'warn':'ok'}
|
||||||
|
];
|
||||||
|
el.innerHTML = cards.map(card => `
|
||||||
|
<div class="top-metric-card ${card.tone||''}">
|
||||||
|
<div class="tm-icon">${iconSvg(card.icon)}</div>
|
||||||
|
<div class="tm-label">${card.label}</div>
|
||||||
|
<div class="tm-value">${card.value}</div>
|
||||||
|
<div class="tm-sub">${card.sub}</div>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionNavGlider(){
|
||||||
|
const nav = document.getElementById('appNav');
|
||||||
|
const glider = document.getElementById('navGlider');
|
||||||
|
const active = nav?.querySelector('.nav-item.active');
|
||||||
|
if(!nav || !glider || !active) return;
|
||||||
|
const navRect = nav.getBoundingClientRect();
|
||||||
|
const activeRect = active.getBoundingClientRect();
|
||||||
|
if(isMobileLayout()){
|
||||||
|
const x = activeRect.left - navRect.left + (activeRect.width - glider.offsetWidth) / 2;
|
||||||
|
glider.style.transform = `translate(${x}px,0)`;
|
||||||
|
} else {
|
||||||
|
const y = activeRect.top - navRect.top + (activeRect.height - glider.offsetHeight) / 2;
|
||||||
|
glider.style.transform = `translate(-50%,${y}px)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldAnimateUi(){
|
||||||
|
return !lowPerfMode && !(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPanelEntrances(){
|
||||||
|
if(!shouldAnimateUi()) return;
|
||||||
|
const items = [...document.querySelectorAll('.top-metric-card,.g-panel,.map-region-bar,.map-container')];
|
||||||
|
items.forEach((el,i) => {
|
||||||
|
el.classList.remove('motion-stagger');
|
||||||
|
el.style.animationDelay = `${Math.min(i * 36, 420)}ms`;
|
||||||
|
void el.offsetWidth;
|
||||||
|
el.classList.add('motion-stagger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderAppNav(){
|
function renderAppNav(){
|
||||||
document.body.dataset.view = currentView;
|
document.body.dataset.view = currentView;
|
||||||
document.querySelectorAll('.nav-item[data-view-target]').forEach(btn => {
|
document.querySelectorAll('.nav-item[data-view-target]').forEach(btn => {
|
||||||
@@ -652,14 +818,25 @@ function renderAppNav(){
|
|||||||
});
|
});
|
||||||
const status = document.getElementById('sidebarStatus');
|
const status = document.getElementById('sidebarStatus');
|
||||||
if(status) status.textContent = currentView === 'home' ? 'Live' : appViews[currentView].title;
|
if(status) status.textContent = currentView === 'home' ? 'Live' : appViews[currentView].title;
|
||||||
|
requestAnimationFrame(positionNavGlider);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAppView(view){
|
function setAppView(view){
|
||||||
if(!appViews[view]) return;
|
if(!appViews[view]) return;
|
||||||
|
const changed = currentView !== view;
|
||||||
currentView = view;
|
currentView = view;
|
||||||
localStorage.setItem('intelligence_terminal_view', view);
|
localStorage.setItem('intelligence_terminal_view', view);
|
||||||
|
if(changed && shouldAnimateUi()){
|
||||||
|
document.body.classList.add('view-transitioning');
|
||||||
|
window.setTimeout(() => document.body.classList.remove('view-transitioning'), 460);
|
||||||
|
}
|
||||||
renderAppNav();
|
renderAppNav();
|
||||||
renderTopbar();
|
renderTopbar();
|
||||||
|
renderTopMetrics();
|
||||||
|
renderLeftRail();
|
||||||
|
renderLower();
|
||||||
|
renderRight();
|
||||||
|
playPanelEntrances();
|
||||||
refreshMapViewport(true);
|
refreshMapViewport(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,8 +1045,8 @@ function renderTopbar(){
|
|||||||
const deltaLabel = direction === 'risk-off' ? '▲ '+t('dashboard.riskOff','RISK-OFF') : direction === 'risk-on' ? '▼ '+t('dashboard.riskOn','RISK-ON') : '◆ '+t('dashboard.mixed','MIXED');
|
const deltaLabel = direction === 'risk-off' ? '▲ '+t('dashboard.riskOff','RISK-OFF') : direction === 'risk-on' ? '▼ '+t('dashboard.riskOn','RISK-ON') : '◆ '+t('dashboard.mixed','MIXED');
|
||||||
document.getElementById('topbar').innerHTML=`
|
document.getElementById('topbar').innerHTML=`
|
||||||
<div class="top-left">
|
<div class="top-left">
|
||||||
<span class="brand">${view.title}</span>
|
<span class="brand">Intelligence Terminal</span>
|
||||||
<span class="view-subtitle">${view.subtitle}</span>
|
<span class="view-subtitle">${view.title} / ${view.subtitle}</span>
|
||||||
<span class="regime-chip"><span class="blink"></span>Operator dashboard</span>
|
<span class="regime-chip"><span class="blink"></span>Operator dashboard</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-search"><span>Search sources, signals, regions, markets...</span></div>
|
<div class="app-search"><span>Search sources, signals, regions, markets...</span></div>
|
||||||
@@ -2132,16 +2309,19 @@ function syncResponsiveLayout(force=false){
|
|||||||
if(force || lastResponsiveMobile === null || mobileNow !== lastResponsiveMobile){
|
if(force || lastResponsiveMobile === null || mobileNow !== lastResponsiveMobile){
|
||||||
lastResponsiveMobile = mobileNow;
|
lastResponsiveMobile = mobileNow;
|
||||||
renderTopbar();
|
renderTopbar();
|
||||||
|
renderTopMetrics();
|
||||||
renderLeftRail();
|
renderLeftRail();
|
||||||
renderLower();
|
renderLower();
|
||||||
renderRight();
|
renderRight();
|
||||||
|
playPanelEntrances();
|
||||||
}
|
}
|
||||||
refreshMapViewport(force && !isFlat);
|
refreshMapViewport(force && !isFlat);
|
||||||
|
positionNavGlider();
|
||||||
}
|
}
|
||||||
|
|
||||||
// === REINIT (for live updates without boot sequence) ===
|
// === REINIT (for live updates without boot sequence) ===
|
||||||
function reinit(){
|
function reinit(){
|
||||||
renderTopbar();renderLeftRail();renderLower();renderRight();
|
renderTopbar();renderTopMetrics();renderLeftRail();renderLower();renderRight();playPanelEntrances();
|
||||||
plotMarkers();
|
plotMarkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2182,7 +2362,7 @@ let booted = false;
|
|||||||
function init(){
|
function init(){
|
||||||
applyTheme(themePreference);
|
applyTheme(themePreference);
|
||||||
renderAppNav();
|
renderAppNav();
|
||||||
renderTopbar();renderLeftRail();renderLower();renderRight();
|
renderTopbar();renderTopMetrics();renderLeftRail();renderLower();renderRight();playPanelEntrances();
|
||||||
renderGlossary();
|
renderGlossary();
|
||||||
initMap();
|
initMap();
|
||||||
if (!booted) { runBoot(); booted = true; }
|
if (!booted) { runBoot(); booted = true; }
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 154 KiB |
BIN
docs/design/modrinth-app-final-concept.png
Normal file
BIN
docs/design/modrinth-app-final-concept.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user