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:
k
2026-05-27 11:25:59 +08:00
parent ee3648c4cb
commit d01adc4790
23 changed files with 1793 additions and 592 deletions
+24 -8
View File
@@ -7,6 +7,7 @@ import { useDashboardStore } from '@/store/dashboard'
import { getUserPublic, setHlApiKey, subscribe } from '@/lib/api'
import { signRequest } from '@/lib/signedRequest'
import { isUserRejection, walletErrorLabel } from '@/lib/walletError'
import { getFirstReadyConnector, walletConnectErrorLabel } from '@/lib/walletConnect'
// Action names must match backend/app/api/{user,subscribe}.py
const ACTION_SET_API_KEY = 'set_hl_api_key'
@@ -28,7 +29,7 @@ function fmtHold(s: number) {
export default function BotPanel({ performance }: Props) {
const { isSubscribed, hlApiKeySet, hlApiKeyMasked, botReadiness, setBotReadiness, setHlApiKeySet, setSubscribed } = useDashboardStore()
const { address, isConnected } = useAccount()
const { connect, connectors } = useConnect()
const { connectAsync, connectors } = useConnect()
const { signMessageAsync } = useSignMessage()
const [mounted, setMounted] = useState(false)
@@ -37,6 +38,7 @@ export default function BotPanel({ performance }: Props) {
const [errorMsg, setErrorMsg] = useState('')
const [subState, setSubState] = useState<'idle' | 'signing' | 'saving' | 'error'>('idle')
const [subError, setSubError] = useState('')
const [connectError, setConnectError] = useState('')
useEffect(() => { setMounted(true) }, [])
@@ -127,9 +129,18 @@ export default function BotPanel({ performance }: Props) {
: saveState === 'success' ? '✓ Saved'
: 'Save key'
function handleConnectWallet() {
const connector = connectors[0]
if (connector) connect({ connector })
async function handleConnectWallet() {
setConnectError('')
try {
const connector = await getFirstReadyConnector(connectors)
if (!connector) {
setConnectError('No wallet connector is available right now.')
return
}
await connectAsync({ connector })
} catch (err: unknown) {
setConnectError(walletConnectErrorLabel(err))
}
}
return (
@@ -169,10 +180,15 @@ export default function BotPanel({ performance }: Props) {
<div className="bot-cta">
{(!mounted || !isConnected) && (
<button className="btn amber" style={{ width: '100%' }}
onClick={handleConnectWallet}>
Connect wallet
</button>
<>
<button className="btn amber" style={{ width: '100%' }}
onClick={() => { void handleConnectWallet() }}>
Connect wallet
</button>
{connectError && (
<p style={{ fontSize: 11, color: 'var(--down)', marginTop: 6, textAlign: 'center' }}>{connectError}</p>
)}
</>
)}
{mounted && isConnected && !isSubscribed && (
<div style={{ width: '100%' }}>
+122 -2
View File
@@ -17,6 +17,9 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
const containerRef = useRef<HTMLDivElement>(null)
const chartRef = useRef<unknown>(null)
const seriesRef = useRef<unknown>(null)
const macroPriceLineRef = useRef<unknown>(null)
const macroVerticalRef = useRef<HTMLDivElement | null>(null)
const macroLabelRef = useRef<HTMLDivElement | null>(null)
const fittedRef = useRef(false)
const postsRef = useRef(posts)
postsRef.current = posts
@@ -37,6 +40,8 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
textColor: isDark ? '#666666' : '#888888',
gridColor: isDark ? '#1e1e1e' : '#f0ede8',
borderColor: isDark ? '#2a2a2a' : '#e8e4de',
macroAccent: isDark ? '#f59e0b' : '#b45309',
macroAccentSoft: isDark ? 'rgba(245,158,11,0.24)' : 'rgba(180,83,9,0.18)',
}
}
@@ -51,6 +56,42 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
if (destroyed || !containerRef.current) return
const colors = getChartColors()
const vertical = document.createElement('div')
vertical.style.position = 'absolute'
vertical.style.top = '0'
vertical.style.bottom = '0'
vertical.style.width = '0'
vertical.style.borderLeft = `2px dashed ${colors.macroAccent}`
vertical.style.opacity = '0'
vertical.style.pointerEvents = 'none'
vertical.style.zIndex = '2'
vertical.style.transition = 'left 160ms ease, opacity 120ms ease'
containerRef.current.appendChild(vertical)
macroVerticalRef.current = vertical
const label = document.createElement('div')
label.style.position = 'absolute'
label.style.top = '14px'
label.style.left = '0'
label.style.padding = '5px 8px'
label.style.borderRadius = '999px'
label.style.border = `1px solid ${colors.macroAccentSoft}`
label.style.background = colors.background
label.style.boxShadow = '0 6px 24px rgba(15, 23, 42, 0.08)'
label.style.color = colors.macroAccent
label.style.fontSize = '11px'
label.style.fontWeight = '700'
label.style.letterSpacing = '0.04em'
label.style.textTransform = 'uppercase'
label.style.whiteSpace = 'nowrap'
label.style.pointerEvents = 'none'
label.style.opacity = '0'
label.style.transform = 'translateX(-50%)'
label.style.zIndex = '3'
label.style.transition = 'left 160ms ease, opacity 120ms ease'
containerRef.current.appendChild(label)
macroLabelRef.current = label
const chart = createChart(containerRef.current, {
width: containerRef.current.clientWidth,
height: 360,
@@ -144,6 +185,14 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
rightPriceScale: { borderColor: colors.borderColor },
timeScale: { borderColor: colors.borderColor },
})
if (macroVerticalRef.current) {
macroVerticalRef.current.style.borderLeft = `2px dashed ${colors.macroAccent}`
}
if (macroLabelRef.current) {
macroLabelRef.current.style.color = colors.macroAccent
macroLabelRef.current.style.borderColor = colors.macroAccentSoft
macroLabelRef.current.style.background = colors.background
}
})
themeObserver.observe(document.documentElement, {
attributes: true,
@@ -162,6 +211,15 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
chartRef.current = null
seriesRef.current = null
}
if (macroVerticalRef.current?.parentNode) {
macroVerticalRef.current.parentNode.removeChild(macroVerticalRef.current)
}
if (macroLabelRef.current?.parentNode) {
macroLabelRef.current.parentNode.removeChild(macroLabelRef.current)
}
macroVerticalRef.current = null
macroLabelRef.current = null
macroPriceLineRef.current = null
}
}, [])
@@ -232,12 +290,74 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
// @ts-expect-error lightweight-charts type
series.setMarkers(markers)
// Highlight the most recent visible Macro Vibes reversal signal on BTC.
const visibleMacro = asset === 'BTC'
? visible
.filter((p) =>
((p.source || '') === 'btc_bottom_reversal' || (p.source || '') === 'funding_reversal') &&
(p.signal === 'buy' || p.signal === 'short'),
)
.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime())
: []
const macroSignal = visibleMacro[0] ?? null
const colors = getChartColors()
const priceSeries = series as any
if (macroPriceLineRef.current && priceSeries?.removePriceLine) {
try { priceSeries.removePriceLine(macroPriceLineRef.current as any) } catch {}
macroPriceLineRef.current = null
}
if (macroSignal && chart && macroVerticalRef.current) {
const pt = Math.floor(new Date(macroSignal.published_at).getTime() / 1000)
const bucketTime = Math.floor(pt / bucketSecs) * bucketSecs
// Only draw the horizontal reference when we have a true signal-entry
// price from the backend. Falling back to a bucket close made the line
// drift across timeframe changes and looked like a bug because it was.
const priceAtSignal =
macroSignal.price_impact?.asset === 'BTC' && macroSignal.price_impact?.price_at_post
? macroSignal.price_impact.price_at_post
: null
if (priceAtSignal != null && priceSeries?.createPriceLine) {
macroPriceLineRef.current = priceSeries.createPriceLine({
price: priceAtSignal,
color: colors.macroAccent,
lineWidth: 2,
lineStyle: 2,
axisLabelVisible: false,
title: (macroSignal.source || '') === 'btc_bottom_reversal' ? 'Macro bottom' : 'Funding reversal',
})
}
const x = (chart as any).timeScale?.().timeToCoordinate?.(bucketTime)
if (typeof x === 'number' && Number.isFinite(x)) {
macroVerticalRef.current.style.left = `${x}px`
macroVerticalRef.current.style.opacity = '1'
if (macroLabelRef.current) {
macroLabelRef.current.textContent =
(macroSignal.source || '') === 'btc_bottom_reversal'
? 'Macro bottom'
: 'Funding reversal'
const clamped = Math.max(76, Math.min((containerRef.current?.clientWidth ?? x) - 76, x))
macroLabelRef.current.style.left = `${clamped}px`
macroLabelRef.current.style.opacity = '1'
}
} else {
macroVerticalRef.current.style.opacity = '0'
if (macroLabelRef.current) macroLabelRef.current.style.opacity = '0'
}
} else if (macroVerticalRef.current) {
macroVerticalRef.current.style.opacity = '0'
if (macroLabelRef.current) macroLabelRef.current.style.opacity = '0'
}
if (!fittedRef.current) {
// @ts-expect-error lightweight-charts type
chart.timeScale().fitContent()
fittedRef.current = true
}
}, [candles, posts, externalSelectedId])
}, [candles, posts, externalSelectedId, asset])
useEffect(() => { fittedRef.current = false }, [timeframe])
@@ -264,7 +384,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
return (
<div
ref={containerRef}
style={{ width: '100%', height: 360, borderRadius: 'var(--r-sm)', overflow: 'hidden', cursor: 'crosshair' }}
style={{ width: '100%', height: 360, borderRadius: 'var(--r-sm)', overflow: 'hidden', cursor: 'crosshair', position: 'relative' }}
/>
)
}