mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-11 17:44:50 +00:00
style(app): merge hero + daily-activity heatmap into one split card
Per feedback, the hero (number + KPIs) and the git-style daily-activity heatmap now share a single card, split left/right with a divider. ActivityHeatmap gains a 'bare' mode (renders head+grid without its own card wrapper). Efficiency scorecard + fuel ring move to a row below.
This commit is contained in:
parent
7ad1b76a8c
commit
9bc06e74ad
3 changed files with 63 additions and 39 deletions
|
|
@ -70,7 +70,7 @@ function buildHeatmapDays(daily: DailyHistoryEntry[], now: Date): HeatmapDay[] {
|
|||
})
|
||||
}
|
||||
|
||||
export function ActivityHeatmap({ daily }: { daily: DailyHistoryEntry[] }) {
|
||||
export function ActivityHeatmap({ daily, bare = false }: { daily: DailyHistoryEntry[]; bare?: boolean }) {
|
||||
const days = useMemo(() => buildHeatmapDays(daily, new Date()), [daily])
|
||||
const activeDays = days.filter(day => !day.isFuture && day.cost > 0).length
|
||||
const [tip, setTip] = useState<{ day: HeatmapDay; x: number; y: number } | null>(null)
|
||||
|
|
@ -95,41 +95,42 @@ export function ActivityHeatmap({ daily }: { daily: DailyHistoryEntry[] }) {
|
|||
setTipPosition({ left, top })
|
||||
}, [tip])
|
||||
|
||||
return (
|
||||
<div className="ov-card ov-panel ov-heatmap-panel">
|
||||
<div className="ov-panel-head">
|
||||
<h3>Daily activity</h3>
|
||||
<span className="r ov-active-days">{activeDays} active days</span>
|
||||
</div>
|
||||
<div className="ov-panel-body">
|
||||
<div className="ov-heatmap-scroll">
|
||||
<div className="ov-heatmap" role="grid" aria-label="Daily activity contribution heatmap">
|
||||
<div className="ov-heatmap-labels" aria-hidden="true">
|
||||
{WEEKDAYS.map((weekday, index) => (
|
||||
<span key={weekday}>{index === 1 || index === 3 || index === 5 ? weekday : ''}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="ov-heatmap-cells">
|
||||
{days.map(day => (
|
||||
<button
|
||||
type="button"
|
||||
role="gridcell"
|
||||
key={day.date}
|
||||
className={`ov-heat-cell heat-level-${day.level}${day.isFuture ? ' future' : ''}`}
|
||||
aria-label={`${formatDate(day.date)}: ${day.isFuture ? 'future day' : `${formatUsd(day.cost)}, ${day.calls} calls`}`}
|
||||
data-date={day.date}
|
||||
data-cost={day.cost}
|
||||
data-active={!day.isFuture && day.cost > 0 ? 'true' : 'false'}
|
||||
onMouseEnter={event => setTip({ day, x: event.clientX, y: event.clientY })}
|
||||
onMouseMove={event => setTip({ day, x: event.clientX, y: event.clientY })}
|
||||
onMouseLeave={() => setTip(null)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
const head = (
|
||||
<div className={bare ? 'ov-activity-head' : 'ov-panel-head'}>
|
||||
{bare ? <span className="ov-label">Daily activity</span> : <h3>Daily activity</h3>}
|
||||
<span className="r ov-active-days">{activeDays} active days</span>
|
||||
</div>
|
||||
)
|
||||
const grid = (
|
||||
<div className="ov-heatmap-scroll">
|
||||
<div className="ov-heatmap" role="grid" aria-label="Daily activity contribution heatmap">
|
||||
<div className="ov-heatmap-labels" aria-hidden="true">
|
||||
{WEEKDAYS.map((weekday, index) => (
|
||||
<span key={weekday}>{index === 1 || index === 3 || index === 5 ? weekday : ''}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="ov-heatmap-cells">
|
||||
{days.map(day => (
|
||||
<button
|
||||
type="button"
|
||||
role="gridcell"
|
||||
key={day.date}
|
||||
className={`ov-heat-cell heat-level-${day.level}${day.isFuture ? ' future' : ''}`}
|
||||
aria-label={`${formatDate(day.date)}: ${day.isFuture ? 'future day' : `${formatUsd(day.cost)}, ${day.calls} calls`}`}
|
||||
data-date={day.date}
|
||||
data-cost={day.cost}
|
||||
data-active={!day.isFuture && day.cost > 0 ? 'true' : 'false'}
|
||||
onMouseEnter={event => setTip({ day, x: event.clientX, y: event.clientY })}
|
||||
onMouseMove={event => setTip({ day, x: event.clientX, y: event.clientY })}
|
||||
onMouseLeave={() => setTip(null)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{tip && createPortal(
|
||||
</div>
|
||||
)
|
||||
const tooltip = tip
|
||||
? createPortal(
|
||||
<div
|
||||
ref={tipRef}
|
||||
className={`chart-tip${tipPosition ? ' on' : ''}`}
|
||||
|
|
@ -141,7 +142,17 @@ export function ActivityHeatmap({ daily }: { daily: DailyHistoryEntry[] }) {
|
|||
<div className="chart-tip-s">{tip.day.isFuture ? 'No activity yet' : `${tip.day.calls} calls`}</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
)
|
||||
: null
|
||||
|
||||
if (bare) {
|
||||
return <div className="ov-heatmap-bare">{head}{grid}{tooltip}</div>
|
||||
}
|
||||
return (
|
||||
<div className="ov-card ov-panel ov-heatmap-panel">
|
||||
{head}
|
||||
<div className="ov-panel-body">{grid}</div>
|
||||
{tooltip}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -574,8 +574,8 @@ export function OverviewContent({
|
|||
const anomalies = deriveAnomalies(data, now)
|
||||
return (
|
||||
<div className="ov-dashboard">
|
||||
<div className="ov-hero-row">
|
||||
<div className="ov-card ov-hero">
|
||||
<div className="ov-card ov-hero-split">
|
||||
<div className="ov-hero-main">
|
||||
<div className="ov-hero-top"><span className="ov-label">{data.current.label}</span><span className="ov-streak"><b>{streakDays(data.history.daily, now)}</b>-day streak</span></div>
|
||||
<CountUp value={data.current.cost} />
|
||||
<div className="ov-hero-sub">{data.current.calls.toLocaleString('en-US')} calls · {data.current.sessions.toLocaleString('en-US')} sessions</div>
|
||||
|
|
@ -585,12 +585,14 @@ export function OverviewContent({
|
|||
<div className="ov-hero-kpi ov-hero-kpi-saved"><span>Saved</span><strong>{formatUsd(saved)}</strong><small>from {applied} applied fixes</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<ActivityHeatmap daily={data.history.daily} bare />
|
||||
</div>
|
||||
|
||||
<div className="ov-hero-secondary">
|
||||
<EfficiencyScorecard current={data.current} />
|
||||
<FuelRing status={plans.data} onNavigate={onNavigate} />
|
||||
</div>
|
||||
|
||||
<ActivityHeatmap daily={data.history.daily} />
|
||||
|
||||
<div className="ov-coach">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="3 17 9 11 13 15 21 7"/><polyline points="15 7 21 7 21 13"/></svg>
|
||||
<div className="ov-coach-tx">
|
||||
|
|
|
|||
|
|
@ -223,6 +223,17 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); }
|
|||
.ov-hero-kpi small { color: var(--mut2); font-size: 10px; margin-top: 1px; }
|
||||
.ov-hero-kpi-accent > strong { color: var(--accent); }
|
||||
.ov-hero-kpi-saved > strong { color: var(--ok); }
|
||||
/* Hero + daily-activity heatmap share one split card. */
|
||||
.ov-hero-split { display: grid; grid-template-columns: minmax(270px, 330px) minmax(0, 1fr); gap: 22px; padding: 16px 18px; align-items: stretch; }
|
||||
.ov-hero-main { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
||||
.ov-hero-split .ov-heatmap-bare { display: flex; flex-direction: column; justify-content: center; gap: 9px; min-width: 0; border-left: 1px solid var(--line2); padding-left: 22px; }
|
||||
.ov-activity-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.ov-hero-secondary { display: grid; grid-template-columns: minmax(0, 1fr) 196px; gap: 12px; }
|
||||
@media (max-width: 900px) {
|
||||
.ov-hero-split { grid-template-columns: 1fr; }
|
||||
.ov-hero-split .ov-heatmap-bare { border-left: 0; padding-left: 0; border-top: 1px solid var(--line2); padding-top: 12px; }
|
||||
.ov-hero-secondary { grid-template-columns: 1fr; }
|
||||
}
|
||||
.ov-hero-sub .neutral { color: var(--mut); font-weight: 560; }
|
||||
.ov-efficiency { display: flex; min-width: 0; flex-direction: column; padding: 14px 16px; }
|
||||
.ov-efficiency-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue