diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 24415d8a912..134a4b6f75e 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3311,7 +3311,11 @@ const SidebarHostStats = memo(function SidebarHostStats() { } const { Component } = HOST_STATS_VARIANTS[style]; - return ; + return ( +
+ +
+ ); }); const SidebarChromeFooter = memo(function SidebarChromeFooter() { @@ -3329,10 +3333,12 @@ const SidebarChromeFooter = memo(function SidebarChromeFooter() { - + {/* Desktop: 1.5× a project row (h-18 = 72px vs the 48px project rows), Settings squished to its + content, and the host-stats readout stretched to the full height. */} + diff --git a/apps/web/src/components/host-stats/VariantBadge.tsx b/apps/web/src/components/host-stats/VariantBadge.tsx deleted file mode 100644 index faf878d608e..00000000000 --- a/apps/web/src/components/host-stats/VariantBadge.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { cn } from "../../lib/utils"; -import { formatFooterGigabytes, hostStatsDetail, type HostStatsVariantProps } from "./types"; - -export function VariantBadge({ stats, history }: HostStatsVariantProps) { - void history; - const detail = hostStatsDetail(stats); - - const cpu = Math.max(0, Math.min(100, stats.cpuPercent)); - const memPercent = - stats.memTotalBytes > 0 ? (stats.memUsedBytes / stats.memTotalBytes) * 100 : 0; - - const cpuStatus = cpu >= 85 ? "destructive" : cpu >= 60 ? "warning" : "success"; - const memStatus = memPercent >= 85 ? "destructive" : memPercent >= 60 ? "warning" : "success"; - - const statusPriority = { destructive: 3, warning: 2, success: 1 }; - const overallStatus = - statusPriority[cpuStatus] > statusPriority[memStatus] ? cpuStatus : memStatus; - - const containerColorClasses = { - success: "bg-success/10 border-success/20", - warning: "bg-warning/10 border-warning/20", - destructive: "bg-destructive/10 border-destructive/20", - }[overallStatus]; - - const dotColorClasses = { - success: "bg-success", - warning: "bg-warning animate-pulse", - destructive: "bg-destructive animate-pulse", - }[overallStatus]; - - return ( -
-
-
- CPU - {Math.round(cpu)}% - · - MEM - {formatFooterGigabytes(stats.memUsedBytes)}G -
-
- ); -} diff --git a/apps/web/src/components/host-stats/VariantBars.tsx b/apps/web/src/components/host-stats/VariantBars.tsx deleted file mode 100644 index 867f2037230..00000000000 --- a/apps/web/src/components/host-stats/VariantBars.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { cn } from "../../lib/utils"; -import { formatFooterGigabytes, hostStatsDetail, type HostStatsVariantProps } from "./types"; - -/** Map a 0–100 load into the app's status tokens: <60 success, 60–85 warning, >85 destructive. */ -function loadTone(percent: number): { fill: string; text: string } { - if (percent > 85) return { fill: "bg-destructive", text: "text-destructive-foreground" }; - if (percent >= 60) return { fill: "bg-warning", text: "text-warning-foreground" }; - return { fill: "bg-success", text: "text-success-foreground" }; -} - -function clampPercent(value: number): number { - if (!Number.isFinite(value)) return 0; - return Math.min(100, Math.max(0, value)); -} - -interface MeterRowProps { - readonly label: string; - readonly percent: number; - readonly value: string; -} - -function MeterRow({ label, percent, value }: MeterRowProps) { - const tone = loadTone(percent); - return ( -
- - {label} - - - - - {value} -
- ); -} - -/** - * Two slim stacked meter bars — CPU on top, MEM below — with a color-ramped - * fill and a right-aligned tabular value, all on a shared three-column grid so - * tracks and figures line up. Fills glide on each 5s refresh. - */ -export function VariantBars({ stats }: HostStatsVariantProps) { - const cpuPercent = clampPercent(stats.cpuPercent); - const memPercent = - stats.memTotalBytes > 0 ? clampPercent((stats.memUsedBytes / stats.memTotalBytes) * 100) : 0; - - const cpuValue = `${Math.round(cpuPercent)}%`; - const memValue = `${formatFooterGigabytes(stats.memUsedBytes)}/${formatFooterGigabytes(stats.memTotalBytes)}G`; - const detail = hostStatsDetail(stats); - - return ( -
- - -
- ); -} diff --git a/apps/web/src/components/host-stats/VariantEqualizer.tsx b/apps/web/src/components/host-stats/VariantEqualizer.tsx deleted file mode 100644 index cd99d0c2f7a..00000000000 --- a/apps/web/src/components/host-stats/VariantEqualizer.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { formatFooterGigabytes, hostStatsDetail, type HostStatsVariantProps } from "./types"; - -import { cn } from "../../lib/utils"; - -const MAX_BARS = 24; -const BAR_WIDTH = 2; -const GAP = 1; -const TRACK_WIDTH = MAX_BARS * (BAR_WIDTH + GAP) - GAP; - -function clampCpu(value: number): number { - return Math.max(0, Math.min(100, value)); -} - -function loadTone(cpu: number): "success" | "warning" | "destructive" { - if (cpu > 85) return "destructive"; - if (cpu >= 60) return "warning"; - return "success"; -} - -export function VariantEqualizer({ stats, history }: HostStatsVariantProps) { - const detail = hostStatsDetail(stats); - const currentCpu = clampCpu(stats.cpuPercent); - const currentTone = loadTone(currentCpu); - - const bars = - history.length > 0 - ? history - : [ - { - at: 0, - cpuPercent: stats.cpuPercent, - memUsedBytes: stats.memUsedBytes, - memTotalBytes: stats.memTotalBytes, - }, - ]; - - return ( -
-
- {Array.from({ length: MAX_BARS }, (_, slot) => { - const dataIndex = slot - (MAX_BARS - bars.length); - if (dataIndex < 0) { - return
; - } - const sample = bars[dataIndex]; - if (!sample) return null; - const cpu = clampCpu(sample.cpuPercent); - const tone = loadTone(cpu); - const opacity = bars.length === 1 ? 1 : 0.35 + (dataIndex / (bars.length - 1)) * 0.65; - return ( -
- ); - })} -
- -
- - {Math.round(currentCpu)}% - - - {formatFooterGigabytes(stats.memUsedBytes)} - / - {formatFooterGigabytes(stats.memTotalBytes)}G - -
-
- ); -} diff --git a/apps/web/src/components/host-stats/VariantRings.tsx b/apps/web/src/components/host-stats/VariantRings.tsx deleted file mode 100644 index 82aaa08f17a..00000000000 --- a/apps/web/src/components/host-stats/VariantRings.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { formatFooterGigabytes, hostStatsDetail, type HostStatsVariantProps } from "./types"; -import { cn } from "../../lib/utils"; - -// Two mini donut gauges (CPU + MEM): muted track circle with a round-capped -// progress arc color-ramped by load, starting at 12 o'clock, with the value -// and a tiny label beside it. Refreshes every 5s; the arc glides via a -// stroke-dashoffset transition. -const RING_RADIUS = 9; -const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS; - -function loadColorClass(pct: number): string { - if (pct >= 85) return "text-destructive"; - if (pct >= 60) return "text-warning"; - return "text-success"; -} - -interface RingGaugeProps { - readonly pct: number; - readonly colorClass: string; - readonly value: string; - readonly label: string; -} - -function RingGauge({ pct, colorClass, value, label }: RingGaugeProps) { - const offset = RING_CIRCUMFERENCE * (1 - pct / 100); - return ( -
- -
- {value} - - {label} - -
-
- ); -} - -export function VariantRings({ stats }: HostStatsVariantProps) { - const detail = hostStatsDetail(stats); - const cpuPct = Math.min(100, Math.max(0, stats.cpuPercent)); - const memPct = - stats.memTotalBytes > 0 - ? Math.min(100, Math.max(0, (stats.memUsedBytes / stats.memTotalBytes) * 100)) - : 0; - - return ( -
- -
- ); -} diff --git a/apps/web/src/components/host-stats/VariantSegments.tsx b/apps/web/src/components/host-stats/VariantSegments.tsx index 012acf4c4b6..fbada264a83 100644 --- a/apps/web/src/components/host-stats/VariantSegments.tsx +++ b/apps/web/src/components/host-stats/VariantSegments.tsx @@ -30,7 +30,7 @@ function SegmentMeter({ percent, peakPercent }: SegmentMeterProps) { @@ -57,19 +57,23 @@ export function VariantSegments({ stats, history }: HostStatsVariantProps) { return (
- CPU + + CPU + - + {cpuPercent.toFixed(1)}% - MEM + + MEM + - + {formatFooterGigabytes(stats.memUsedBytes)}/{formatFooterGigabytes(stats.memTotalBytes)}G
diff --git a/apps/web/src/components/host-stats/VariantSignature.tsx b/apps/web/src/components/host-stats/VariantSignature.tsx deleted file mode 100644 index c691f13c107..00000000000 --- a/apps/web/src/components/host-stats/VariantSignature.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { cn } from "../../lib/utils"; -import { formatFooterGigabytes, hostStatsDetail, type HostStatsVariantProps } from "./types"; - -/** - * "Signature" — a two-row precision-fader readout. - * - * Each metric is a hairline capacity track with a solid status-colored dot - * riding it, trailing a translucent wake of the same hue, so severity is - * encoded twice (position + color). A muted tick marks the recent peak from - * `history`. Figures stay in neutral text tokens — color lives only in the - * marks — so the widget sits quietly at idle and lights up under load. - */ - -type Tone = "success" | "warning" | "destructive"; - -function clampPercent(value: number): number { - if (!Number.isFinite(value)) return 0; - return Math.min(100, Math.max(0, value)); -} - -function toneFor(percent: number): Tone { - if (percent > 85) return "destructive"; - if (percent >= 60) return "warning"; - return "success"; -} - -const WAKE_CLASS: Record = { - success: "bg-success/25", - warning: "bg-warning/30", - destructive: "bg-destructive/30", -}; - -const DOT_CLASS: Record = { - success: "bg-success", - warning: "bg-warning", - destructive: "bg-destructive", -}; - -interface MeterRowProps { - readonly label: string; - readonly value: string; - readonly percent: number; - readonly peakPercent: number | null; -} - -function MeterRow({ label, value, percent, peakPercent }: MeterRowProps) { - const tone = toneFor(percent); - // Only draw the peak-hold tick when it sits clear of the dot. - const showPeak = peakPercent !== null && peakPercent > percent + 3; - - return ( - <> - - {label} - -
-
-
- {showPeak ? ( -
- ) : null} -
-
- - {value} - - - ); -} - -export function VariantSignature({ stats, history }: HostStatsVariantProps) { - const detail = hostStatsDetail(stats); - - const cpuPercent = clampPercent(stats.cpuPercent); - const memPercent = - stats.memTotalBytes > 0 ? clampPercent((stats.memUsedBytes / stats.memTotalBytes) * 100) : 0; - - const hasWindow = history.length > 1; - const cpuPeak = hasWindow - ? Math.max(...history.map((sample) => clampPercent(sample.cpuPercent))) - : null; - const memPeak = hasWindow - ? Math.max( - ...history.map((sample) => - sample.memTotalBytes > 0 - ? clampPercent((sample.memUsedBytes / sample.memTotalBytes) * 100) - : 0, - ), - ) - : null; - - return ( -
- - -
- ); -} diff --git a/apps/web/src/components/host-stats/VariantSparkline.tsx b/apps/web/src/components/host-stats/VariantSparkline.tsx deleted file mode 100644 index 08f14d663fe..00000000000 --- a/apps/web/src/components/host-stats/VariantSparkline.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { cn } from "../../lib/utils"; -import { formatFooterGigabytes, hostStatsDetail, type HostStatsVariantProps } from "./types"; - -const SPARK_WIDTH = 56; -const SPARK_HEIGHT = 20; - -function clampPercent(value: number): number { - return Math.min(100, Math.max(0, value)); -} - -/** Load-ramp color class: <60% success, 60–85% warning, >85% destructive. */ -function rampClass(cpuPercent: number): string { - if (cpuPercent > 85) return "text-destructive"; - if (cpuPercent >= 60) return "text-warning"; - return "text-success"; -} - -/** Oldest→newest polyline + closed area points for the CPU sparkline. */ -function buildSparkPoints(cpuSamples: readonly number[]): { line: string; area: string } { - const samples = cpuSamples.length >= 2 ? cpuSamples : [cpuSamples[0] ?? 0, cpuSamples[0] ?? 0]; - const step = SPARK_WIDTH / (samples.length - 1); - const coords = samples.map((cpu, index) => { - const x = index * step; - const y = SPARK_HEIGHT - (cpu / 100) * SPARK_HEIGHT; - return `${x.toFixed(1)},${y.toFixed(1)}`; - }); - const line = coords.join(" "); - const area = `0,${SPARK_HEIGHT} ${line} ${SPARK_WIDTH},${SPARK_HEIGHT}`; - return { line, area }; -} - -// Live CPU sparkline hero readout: a colored trend line for recent load next -// to a bold current-value figure, with memory tucked in small below it. -export function VariantSparkline({ stats, history }: HostStatsVariantProps) { - const cpuPercent = clampPercent(stats.cpuPercent); - const cpuSamples = history.map((sample) => clampPercent(sample.cpuPercent)); - const { line, area } = buildSparkPoints(cpuSamples.length > 0 ? cpuSamples : [cpuPercent]); - const color = rampClass(cpuPercent); - const detail = hostStatsDetail(stats); - const cpuLabel = `${cpuPercent.toFixed(1)}%`; - const memLabel = `${formatFooterGigabytes(stats.memUsedBytes)}/${formatFooterGigabytes(stats.memTotalBytes)}G`; - - return ( -
- -
- - {cpuLabel} - - {memLabel} -
-
- ); -} diff --git a/apps/web/src/components/host-stats/variants.ts b/apps/web/src/components/host-stats/variants.ts index 117ebe2a28d..9f91f588b47 100644 --- a/apps/web/src/components/host-stats/variants.ts +++ b/apps/web/src/components/host-stats/variants.ts @@ -2,14 +2,8 @@ import type { ComponentType } from "react"; import type { SidebarHostStatsStyle } from "@t3tools/contracts/settings"; import type { HostStatsVariantProps } from "./types"; -import { VariantBadge } from "./VariantBadge"; -import { VariantBars } from "./VariantBars"; import { VariantClassic } from "./VariantClassic"; -import { VariantEqualizer } from "./VariantEqualizer"; -import { VariantRings } from "./VariantRings"; import { VariantSegments } from "./VariantSegments"; -import { VariantSignature } from "./VariantSignature"; -import { VariantSparkline } from "./VariantSparkline"; export interface HostStatsVariantEntry { /** Human label for the Settings → Features style picker. */ @@ -17,17 +11,29 @@ export interface HostStatsVariantEntry { readonly Component: ComponentType; } -// Redesign candidates for the sidebar server-load readout, selectable from -// Settings → Features → Sidebar → "Server load style". +const CLASSIC: HostStatsVariantEntry = { + label: "Classic (plain text)", + Component: VariantClassic, +}; +const SEGMENTS: HostStatsVariantEntry = { label: "Segments", Component: VariantSegments }; + +// "segments" won the PR #39 redesign bake-off. The retired candidate ids stay +// decodable (see SidebarHostStatsStyle in contracts) and render as the winner. export const HOST_STATS_VARIANTS: Readonly< Record > = { - classic: { label: "Classic (plain text)", Component: VariantClassic }, - signature: { label: "Signature", Component: VariantSignature }, - bars: { label: "Bars", Component: VariantBars }, - sparkline: { label: "Sparkline", Component: VariantSparkline }, - segments: { label: "Segments", Component: VariantSegments }, - rings: { label: "Rings", Component: VariantRings }, - equalizer: { label: "Equalizer", Component: VariantEqualizer }, - badge: { label: "Badge", Component: VariantBadge }, + classic: CLASSIC, + signature: SEGMENTS, + bars: SEGMENTS, + sparkline: SEGMENTS, + segments: SEGMENTS, + rings: SEGMENTS, + equalizer: SEGMENTS, + badge: SEGMENTS, }; + +/** The styles actually offered in the picker (retired ids are hidden). */ +export const HOST_STATS_PICKER_STYLES: readonly SidebarHostStatsStyle[] = [ + "segments", + "classic", +]; diff --git a/apps/web/src/components/settings/FeaturesSettings.tsx b/apps/web/src/components/settings/FeaturesSettings.tsx index bae3336a0f6..df39ca427fd 100644 --- a/apps/web/src/components/settings/FeaturesSettings.tsx +++ b/apps/web/src/components/settings/FeaturesSettings.tsx @@ -13,7 +13,7 @@ import { type SidebarHostStatsStyle, } from "@t3tools/contracts/settings"; -import { HOST_STATS_VARIANTS } from "../host-stats/variants"; +import { HOST_STATS_PICKER_STYLES, HOST_STATS_VARIANTS } from "../host-stats/variants"; import { useIsMobile } from "../../hooks/useMediaQuery"; import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings"; @@ -167,9 +167,9 @@ export function FeaturesSettingsPanel() { {HOST_STATS_VARIANTS[settings.sidebarHostStatsStyle].label} - {Object.entries(HOST_STATS_VARIANTS).map(([value, entry]) => ( + {HOST_STATS_PICKER_STYLES.map((value) => ( - {entry.label} + {HOST_STATS_VARIANTS[value].label} ))} diff --git a/docs/host-stats-variants/comparison.png b/docs/host-stats-variants/comparison.png deleted file mode 100644 index 2dfd149f92f..00000000000 Binary files a/docs/host-stats-variants/comparison.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-badge.png b/docs/host-stats-variants/dark-badge.png deleted file mode 100644 index a3edd913bdf..00000000000 Binary files a/docs/host-stats-variants/dark-badge.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-bars.png b/docs/host-stats-variants/dark-bars.png deleted file mode 100644 index dd9589daf36..00000000000 Binary files a/docs/host-stats-variants/dark-bars.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-classic.png b/docs/host-stats-variants/dark-classic.png deleted file mode 100644 index fc83ca926b2..00000000000 Binary files a/docs/host-stats-variants/dark-classic.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-equalizer.png b/docs/host-stats-variants/dark-equalizer.png deleted file mode 100644 index 0bd41f570a7..00000000000 Binary files a/docs/host-stats-variants/dark-equalizer.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-full.png b/docs/host-stats-variants/dark-full.png deleted file mode 100644 index 466f60b5570..00000000000 Binary files a/docs/host-stats-variants/dark-full.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-rings.png b/docs/host-stats-variants/dark-rings.png deleted file mode 100644 index 405a17aed4a..00000000000 Binary files a/docs/host-stats-variants/dark-rings.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-segments.png b/docs/host-stats-variants/dark-segments.png deleted file mode 100644 index f77b37074c5..00000000000 Binary files a/docs/host-stats-variants/dark-segments.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-signature.png b/docs/host-stats-variants/dark-signature.png deleted file mode 100644 index 4da6b740ba4..00000000000 Binary files a/docs/host-stats-variants/dark-signature.png and /dev/null differ diff --git a/docs/host-stats-variants/dark-sparkline.png b/docs/host-stats-variants/dark-sparkline.png deleted file mode 100644 index e266ad0e45a..00000000000 Binary files a/docs/host-stats-variants/dark-sparkline.png and /dev/null differ diff --git a/docs/host-stats-variants/light-badge.png b/docs/host-stats-variants/light-badge.png deleted file mode 100644 index d57f7ca9ade..00000000000 Binary files a/docs/host-stats-variants/light-badge.png and /dev/null differ diff --git a/docs/host-stats-variants/light-bars.png b/docs/host-stats-variants/light-bars.png deleted file mode 100644 index 6d6ea76adda..00000000000 Binary files a/docs/host-stats-variants/light-bars.png and /dev/null differ diff --git a/docs/host-stats-variants/light-classic.png b/docs/host-stats-variants/light-classic.png deleted file mode 100644 index 8818a2f2211..00000000000 Binary files a/docs/host-stats-variants/light-classic.png and /dev/null differ diff --git a/docs/host-stats-variants/light-equalizer.png b/docs/host-stats-variants/light-equalizer.png deleted file mode 100644 index dc6132480ca..00000000000 Binary files a/docs/host-stats-variants/light-equalizer.png and /dev/null differ diff --git a/docs/host-stats-variants/light-full.png b/docs/host-stats-variants/light-full.png deleted file mode 100644 index 5615673db0b..00000000000 Binary files a/docs/host-stats-variants/light-full.png and /dev/null differ diff --git a/docs/host-stats-variants/light-rings.png b/docs/host-stats-variants/light-rings.png deleted file mode 100644 index a0e7da58845..00000000000 Binary files a/docs/host-stats-variants/light-rings.png and /dev/null differ diff --git a/docs/host-stats-variants/light-segments.png b/docs/host-stats-variants/light-segments.png deleted file mode 100644 index 1bbab7c59e8..00000000000 Binary files a/docs/host-stats-variants/light-segments.png and /dev/null differ diff --git a/docs/host-stats-variants/light-signature.png b/docs/host-stats-variants/light-signature.png deleted file mode 100644 index 97544a26cb5..00000000000 Binary files a/docs/host-stats-variants/light-signature.png and /dev/null differ diff --git a/docs/host-stats-variants/light-sparkline.png b/docs/host-stats-variants/light-sparkline.png deleted file mode 100644 index 3a6b8ef1f67..00000000000 Binary files a/docs/host-stats-variants/light-sparkline.png and /dev/null differ diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 40cb8d66bda..0d815b51c33 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -34,8 +34,10 @@ export const SidebarChatListView = Schema.Literals(["grouped", "flat"]); export type SidebarChatListView = typeof SidebarChatListView.Type; export const DEFAULT_SIDEBAR_CHAT_LIST_VIEW: SidebarChatListView = "grouped"; -// Visual style of the sidebar-footer server-load readout. "classic" is the -// original plain-text CPU/MEM line; the rest are richer redesign candidates. +// Visual style of the sidebar-footer server-load readout. "segments" won the +// PR #39 redesign bake-off; "classic" is the original plain-text CPU/MEM line. +// The remaining literals are retired candidates kept only so settings stored +// while the bake-off ran still decode — the UI renders them as "segments". export const SidebarHostStatsStyle = Schema.Literals([ "classic", "signature", @@ -47,7 +49,7 @@ export const SidebarHostStatsStyle = Schema.Literals([ "badge", ]); export type SidebarHostStatsStyle = typeof SidebarHostStatsStyle.Type; -export const DEFAULT_SIDEBAR_HOST_STATS_STYLE: SidebarHostStatsStyle = "classic"; +export const DEFAULT_SIDEBAR_HOST_STATS_STYLE: SidebarHostStatsStyle = "segments"; export const SidebarProjectGroupingMode = Schema.Literals([ "repository",