ui: tighten dashboard copy and fix layout issues

This commit is contained in:
k
2026-06-14 21:58:56 +08:00
parent 4c3c8c6f87
commit 8534d90589
19 changed files with 1036 additions and 345 deletions
+50 -8
View File
@@ -1,6 +1,6 @@
'use client'
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import type { TrumpPost, Candle } from '@/types'
import { useDashboardStore } from '@/store/dashboard'
@@ -33,6 +33,16 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
// subscribeVisibleTimeRangeChange callback (when the user pans / zooms).
const repositionMacroRef = useRef<(() => void) | null>(null)
const fittedRef = useRef(false)
// Chart creation is async (dynamic import). If the candles fetch wins the
// race (common — SWR serves them instantly from cache), the data effect
// runs while seriesRef is still null and bails; nothing re-triggers it
// afterwards, so the chart stays EMPTY and the 2s live-tick effect then
// paints a single lone candle via series.update(). This state flips when
// the chart actually exists so the data effect re-runs.
const [chartReady, setChartReady] = useState(false)
// The live-tick effect must never run before setData() — updating an empty
// series creates a one-candle chart.
const dataLoadedRef = useRef(false)
const postsRef = useRef(posts)
postsRef.current = posts
const selectedPostIdRef = useRef(externalSelectedId)
@@ -111,7 +121,11 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
const chart = createChart(containerRef.current, {
width: containerRef.current.clientWidth,
height: 360,
// Follow the container, don't hardcode: .chart-wrap is 360px on
// desktop but 300/240px behind !important media queries. A fixed
// 360 overflows those and overflow:hidden cuts off the bottom of
// the chart — lowest candles, markers, and the whole time axis.
height: containerRef.current.clientHeight || 360,
layout: {
background: { color: colors.background },
textColor: colors.textColor,
@@ -145,6 +159,16 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
seriesRef.current = series
// Breathing room above/below the candles. Markers render ~10px outside
// the bar's high/low; with the default tight bottom margin a crash
// candle at the bottom of the autoscaled range gets its marker dots
// clipped by the pane edge.
series.priceScale().applyOptions({
scaleMargins: { top: 0.12, bottom: 0.16 },
})
setChartReady(true)
// Re-project macro signal time → pixel X on every visible-range
// change (pan / zoom / data update / resize). Without this the line
// is set ONCE in the data effect and then stays stuck at a fixed
@@ -226,7 +250,10 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
ro = new ResizeObserver(() => {
if (containerRef.current && !destroyed) {
chart.applyOptions({ width: containerRef.current.clientWidth })
chart.applyOptions({
width: containerRef.current.clientWidth,
height: containerRef.current.clientHeight || 360,
})
// Width change → new pixel coordinates → re-project the macro
// marker too, otherwise it'd drift relative to the candles on
// window resize / sidebar toggle.
@@ -269,6 +296,8 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
ro?.disconnect()
themeObserver?.disconnect()
fittedRef.current = false
dataLoadedRef.current = false
setChartReady(false)
if (chartRef.current) {
// @ts-expect-error lightweight-charts type
chartRef.current.remove()
@@ -291,7 +320,10 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
useEffect(() => {
const series = seriesRef.current
const chart = chartRef.current
if (!series || !chart || candles.length === 0) return
if (!series || !chart || candles.length === 0) {
dataLoadedRef.current = false
return
}
const sorted = [...candles].sort((a, b) => a.time - b.time)
@@ -303,6 +335,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
low: c.low,
close: c.close,
})))
dataLoadedRef.current = true
const minTime = sorted[0].time
const maxTime = sorted[sorted.length - 1].time
@@ -313,6 +346,13 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
return t >= minTime && t <= maxTime
})
// Markers show actionable signals only. Hold/filtered posts vastly
// outnumber signals and their gray dots pile into unreadable clusters;
// they stay browsable in the Recent-signals list instead.
const markerPosts = visible.filter(
(p) => p.signal === 'buy' || p.signal === 'short' || p.signal === 'sell',
)
const bucketByTf: Record<string, number> = {
'5m': 300, '15m': 900, '1h': 3600, '4h': 14400, '1d': 86400, '1w': 604800,
}
@@ -320,7 +360,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
const bucketSecs = bucketByTf[timeframeRef.current.toLowerCase()] ?? candleSpacing
const bucketMap = new Map<number, typeof visible>()
for (const p of visible) {
for (const p of markerPosts) {
const pt = Math.floor(new Date(p.published_at).getTime() / 1000)
const bucket = Math.floor(pt / bucketSecs) * bucketSecs
if (!bucketMap.has(bucket)) bucketMap.set(bucket, [])
@@ -415,7 +455,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
chart.timeScale().fitContent()
fittedRef.current = true
}
}, [candles, posts, externalSelectedId, asset])
}, [candles, posts, externalSelectedId, asset, chartReady])
useEffect(() => { fittedRef.current = false }, [timeframe, asset])
@@ -426,7 +466,9 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
// the live tick exceeds them.
useEffect(() => {
const series = seriesRef.current as any
if (!series) return
// dataLoadedRef: update() on a series that hasn't had setData() yet
// creates a chart with a single lone candle.
if (!series || !dataLoadedRef.current) return
const live = livePrices[asset]
if (live == null || !candles.length) return
const last = candles[candles.length - 1]
@@ -442,7 +484,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', position: 'relative' }}
style={{ width: '100%', height: '100%', borderRadius: 'var(--r-sm)', overflow: 'hidden', cursor: 'crosshair', position: 'relative' }}
/>
)
}