style(dashboard): unify Macro composite into one card + dark-mode fix
for Performance accent
User flagged the macro index box on the overview page as feeling
disjointed. Reshaped it from three stacked elements (band → track →
scale) into a single bordered card so it reads as ONE component.
- JSX: wrap the three pieces in <div className="overview-macro-card">
- CSS: new .overview-macro-card with tone-coloured rim (bull/bear/neutral)
- Solid neutral-gray filled needle (was a hollow ring — looked like a
placeholder); tone-coloured background + white inner ring + double
shadow so it stands out on any gradient position
- Removed the .overview-score-fill overlay — the gradient already
encodes the spectrum; layering an opaque fill obscured it near 0
- Thinner track (14px vs 22px), tighter scale labels, smaller pill
- Added "TODAY · 8 INDICATORS" stamp next to the title — gives users
a quick anchor of what they're looking at + freshness
Plus: dark-mode override for .overview-stat-card.accent (the Performance
card). It was using a cream gradient that floated as a glaring
out-of-theme block on dark mode. Mirrored the existing .kpi.accent dark
treatment so it stays visually grouped with the rest of the dashboard.
Also includes the in-flight overview rewrite from the other AI tool
(legacy /btc redirect to /macro, middleware.ts replacing proxy.ts for
Next.js routing, refactored several dashboard panels). TypeScript clean,
production build passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ import {
|
||||
type OpenPosition,
|
||||
type TodayStats,
|
||||
} from '@/lib/api'
|
||||
import { getCachedViewEnvelope, getOrCreateViewEnvelope, signRequest } from '@/lib/signedRequest'
|
||||
import { getCachedViewEnvelope, signRequest } from '@/lib/signedRequest'
|
||||
import { isUserRejection, walletErrorLabel } from '@/lib/walletError'
|
||||
|
||||
const POLL_MS = 15_000
|
||||
@@ -171,6 +171,7 @@ export default function OpenPositions() {
|
||||
const [positions, setPositions] = useState<OpenPosition[] | null>(null)
|
||||
const [today, setToday] = useState<TodayStats | null>(null)
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
const [needsUnlock, setNeedsUnlock] = useState(false)
|
||||
// Close-confirmation modal state
|
||||
const [closing, setClosing] = useState<OpenPosition | null>(null)
|
||||
const [closeState, setCloseState] = useState<'idle'|'signing'|'closing'|'err'>('idle')
|
||||
@@ -203,43 +204,38 @@ export default function OpenPositions() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected || !address) {
|
||||
setPositions(null); setToday(null)
|
||||
setPositions(null); setToday(null); setNeedsUnlock(false); setErr(null)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
|
||||
// First load uses getOrCreateViewEnvelope — may pop MetaMask exactly ONCE
|
||||
// when the page is opened (user action). All subsequent polls reuse the
|
||||
// cached envelope via getCachedViewEnvelope (no popup). When the cache
|
||||
// expires (20 min), the polling tick silently skips until the user
|
||||
// refocuses the tab or navigates back in — preventing the wallet from
|
||||
// popping in the background while the user is doing something else.
|
||||
async function load(envSource: 'first' | 'poll') {
|
||||
const env = envSource === 'first'
|
||||
? await getOrCreateViewEnvelope({
|
||||
action: 'view_positions', wallet: address!, signMessageAsync,
|
||||
})
|
||||
: getCachedViewEnvelope('view_positions', address!)
|
||||
if (!env) return // cache expired during polling — wait for user re-entry
|
||||
// Only reuse a cached envelope here. Open positions should never trigger
|
||||
// a fresh signature popup on their own while the user is browsing.
|
||||
async function load() {
|
||||
const env = getCachedViewEnvelope('view_positions', address!)
|
||||
?? getCachedViewEnvelope('view_user', address!)
|
||||
if (!env) {
|
||||
if (!cancelled) {
|
||||
setNeedsUnlock(true)
|
||||
setErr(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const [p, t] = await Promise.all([
|
||||
getOpenPositions(address!, env),
|
||||
getTodayStats(address!, env),
|
||||
])
|
||||
if (!cancelled) {
|
||||
setPositions(p.positions); setToday(t); setErr(null)
|
||||
setPositions(p.positions); setToday(t); setErr(null); setNeedsUnlock(false)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (!cancelled) setErr(e instanceof Error ? e.message.slice(0, 80) : (isZh ? '加载失败' : 'fetch error'))
|
||||
}
|
||||
}
|
||||
|
||||
load('first').catch(e => {
|
||||
// Initial sign-rejection lands here. Show a soft error so the polling
|
||||
// loop still starts (a subsequent in-cache sign elsewhere will revive it).
|
||||
if (!cancelled) setErr(e instanceof Error ? e.message.slice(0, 80) : (isZh ? '加载失败' : 'fetch error'))
|
||||
})
|
||||
const id = setInterval(() => { void load('poll') }, POLL_MS)
|
||||
void load()
|
||||
const id = setInterval(() => { void load() }, POLL_MS)
|
||||
return () => { cancelled = true; clearInterval(id) }
|
||||
}, [address, isConnected, signMessageAsync, isZh])
|
||||
|
||||
@@ -269,6 +265,21 @@ export default function OpenPositions() {
|
||||
// Return null both before mount (SSR) and when not connected — same output,
|
||||
// no hydration mismatch.
|
||||
if (!mounted || !isConnected) return null
|
||||
if (needsUnlock && positions === null && today === null) {
|
||||
return (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.06em', textTransform: 'uppercase', fontWeight: 600, marginBottom: 6 }}>
|
||||
Open positions
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>
|
||||
Load your settings once to unlock private position data.
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4, lineHeight: 1.6 }}>
|
||||
This panel no longer triggers a wallet signature on its own while you browse. Open the Settings page and load once, then the cached permission will populate positions here.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (positions === null && today === null) return null
|
||||
|
||||
const totalUnrealized = (positions ?? []).reduce(
|
||||
|
||||
Reference in New Issue
Block a user