UI Phase 1: Statistik 50/50, Footer rechts, Linksammler bis Footer, Bandbreiten-Graph oben rechts, Stat-Karten + Hilfe-Untermenues
Sichtbarkeits-/Layout-Politur (User-Wunsch, Quick-Wins-Paket vor den groesseren Reworks von Einstellungen-Tab und Account-Verwaltung): - Statistik: Bandbreitenverlauf und Hoster-Statistik jetzt 50/50 statt 40/60 (grid-template-columns 1fr 1fr). - Footer: die Zaehler (Pakete/Links/Session/Gesamt/Hoster/Speed/ETA) wandern nach unten-RECHTS, die Aktions-Buttons (Ein-/Ausklappen, Leeren, Clipboard) nach links. - Linksammler: das Textfeld fuellt jetzt die volle Hoehe bis zum Footer (collector-view: grid-row 1fr, textarea flex:1) statt bei 220px zu enden. - Downloads-Tab: neuer Bandbreiten-Graph oben rechts (DownloadSpeedSparkline) neben der Suche. Eigener 250ms-Timer mit geglaettetem Verlauf: bei kurzem Einbruch auf 0 faellt die Kurve nicht hart ab, sondern gleitet weich runter (asymmetrisches Easing: schnell hoch 0.45, langsam runter 0.12) und korrigiert sich wieder; die angezeigte Zahl bleibt der echte aktuelle Wert. Erste Version - Optik wird nach JDownloader-Screenshot nachgezogen. - Stat-Karten professioneller: Wert in Zahl + Einheit getrennt (Einheit dezent), ruhige Leerzustaende (idle '0'/'—' gedaempft statt nacktem grossen '0'), kompaktere Karten, dezentere Eyebrow-Labels. - Hoster-Balken: flacher Akzent statt Neon-Verlauf (kein Gradient). - Hilfe-Menue umstrukturiert mit Hover-Untermenues: 'Logs oeffnen' (Haupt-, Audit-, Rename-, Session-, Trace-Log), 'Remote-Support' (Support-Bundle + Support-Trace), 'Diagnose' (Debug-Setup + Debug-Token); 'Letzte Fehler anzeigen' und 'Suche Aktualisierungen' bleiben direkt. Reine Renderer-Aenderung (App.tsx + styles.css). 808 Tests gruen, tsc=6 Baseline, Build ok.
This commit is contained in:
parent
73f871bfcb
commit
4c09c9a2f3
@ -1253,6 +1253,31 @@ function getDebridLinkKeyStatusDisplay(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitStatValue(value: string): { num: string; unit: string; idle: boolean } {
|
||||||
|
const v = (value ?? "").trim();
|
||||||
|
if (v === "" || v === "--" || v === "—") return { num: "—", unit: "", idle: true };
|
||||||
|
const match = v.match(/^(-?[\d.,]+)\s*(.*)$/);
|
||||||
|
if (!match) return { num: v, unit: "", idle: false };
|
||||||
|
const num = match[1];
|
||||||
|
const unit = (match[2] || "").trim();
|
||||||
|
const idle = /^0([.,]0+)?$/.test(num);
|
||||||
|
return { num, unit, idle };
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatValueView({ value, compact, danger }: { value: string; compact?: boolean; danger?: boolean }): ReactElement {
|
||||||
|
const { num, unit, idle } = splitStatValue(value);
|
||||||
|
const cls = `stat-value${compact ? " stat-value-compact" : ""}${danger ? " danger" : ""}${idle ? " stat-idle" : ""}`;
|
||||||
|
if (compact) {
|
||||||
|
return <span className={cls}>{idle ? "—" : value}</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className={cls}>
|
||||||
|
<span className="stat-num">{num}</span>
|
||||||
|
{unit ? <span className="stat-unit">{unit}</span> : null}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface BandwidthChartProps {
|
interface BandwidthChartProps {
|
||||||
items: Record<string, DownloadItem>;
|
items: Record<string, DownloadItem>;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
@ -1446,6 +1471,106 @@ const BandwidthChart = memo(function BandwidthChart({ items, running, paused, sp
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface DownloadSpeedSparklineProps {
|
||||||
|
items: Record<string, DownloadItem>;
|
||||||
|
running: boolean;
|
||||||
|
paused: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPARKLINE_MAX_SAMPLES = 160;
|
||||||
|
|
||||||
|
const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, running, paused }: DownloadSpeedSparklineProps): ReactElement {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const histRef = useRef<number[]>([]);
|
||||||
|
const displayRef = useRef<number>(0);
|
||||||
|
const itemsRef = useRef(items);
|
||||||
|
const activeRef = useRef(running && !paused);
|
||||||
|
const [liveSpeed, setLiveSpeed] = useState(0);
|
||||||
|
itemsRef.current = items;
|
||||||
|
activeRef.current = running && !paused;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const draw = (): void => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const cssW = canvas.clientWidth;
|
||||||
|
const cssH = canvas.clientHeight;
|
||||||
|
if (cssW <= 0 || cssH <= 0) return;
|
||||||
|
canvas.width = Math.round(cssW * dpr);
|
||||||
|
canvas.height = Math.round(cssH * dpr);
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
ctx.clearRect(0, 0, cssW, cssH);
|
||||||
|
|
||||||
|
const hist = histRef.current;
|
||||||
|
if (hist.length < 2) return;
|
||||||
|
|
||||||
|
const isDark = document.documentElement.getAttribute("data-theme") !== "light";
|
||||||
|
const accent = isDark ? "#f2942d" : "#c2701a";
|
||||||
|
const fill = isDark ? "rgba(242, 148, 45, 0.16)" : "rgba(194, 112, 26, 0.16)";
|
||||||
|
|
||||||
|
let maxV = 0;
|
||||||
|
for (const v of hist) if (v > maxV) maxV = v;
|
||||||
|
maxV = Math.max(maxV, 1024 * 1024);
|
||||||
|
|
||||||
|
const pad = 2;
|
||||||
|
const h = cssH - pad * 2;
|
||||||
|
const step = cssW / (SPARKLINE_MAX_SAMPLES - 1);
|
||||||
|
const startIdx = SPARKLINE_MAX_SAMPLES - hist.length;
|
||||||
|
const px = (i: number): number => (startIdx + i) * step;
|
||||||
|
const py = (v: number): number => pad + h - (v / maxV) * h;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(px(0), py(hist[0]));
|
||||||
|
for (let i = 1; i < hist.length; i += 1) ctx.lineTo(px(i), py(hist[i]));
|
||||||
|
ctx.lineTo(px(hist.length - 1), pad + h);
|
||||||
|
ctx.lineTo(px(0), pad + h);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(px(0), py(hist[0]));
|
||||||
|
for (let i = 1; i < hist.length; i += 1) ctx.lineTo(px(i), py(hist[i]));
|
||||||
|
ctx.strokeStyle = accent;
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const tick = (): void => {
|
||||||
|
let target = 0;
|
||||||
|
if (activeRef.current) {
|
||||||
|
for (const it of Object.values(itemsRef.current)) {
|
||||||
|
if (it.status === "downloading") target += it.speedBps || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cur = displayRef.current;
|
||||||
|
const alpha = target > cur ? 0.45 : 0.12;
|
||||||
|
let next = cur + (target - cur) * alpha;
|
||||||
|
if (next < 1) next = 0;
|
||||||
|
displayRef.current = next;
|
||||||
|
const hist = histRef.current;
|
||||||
|
hist.push(next);
|
||||||
|
if (hist.length > SPARKLINE_MAX_SAMPLES) hist.splice(0, hist.length - SPARKLINE_MAX_SAMPLES);
|
||||||
|
setLiveSpeed(target);
|
||||||
|
draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = window.setInterval(tick, 250);
|
||||||
|
return () => window.clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="speed-sparkline" title="Aktuelle Download-Geschwindigkeit (geglättet)">
|
||||||
|
<canvas ref={canvasRef} className="speed-sparkline-canvas" />
|
||||||
|
<span className="speed-sparkline-value">{liveSpeed > 0 ? formatSpeedMbps(liveSpeed) : "0 B/s"}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
let nextCollectorId = 1;
|
let nextCollectorId = 1;
|
||||||
|
|
||||||
function createScheduleId(): string {
|
function createScheduleId(): string {
|
||||||
@ -4344,37 +4469,52 @@ export function App(): ReactElement {
|
|||||||
</button>
|
</button>
|
||||||
{openMenu === "hilfe" && (
|
{openMenu === "hilfe" && (
|
||||||
<div className="menu-dropdown">
|
<div className="menu-dropdown">
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog().catch(() => {}); }}>
|
<div
|
||||||
<span>Log öffnen</span>
|
className="menu-submenu"
|
||||||
</button>
|
onMouseEnter={() => setOpenSubmenu("hilfe-log")}
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}>
|
onMouseLeave={() => setOpenSubmenu(null)}
|
||||||
<span>Audit-Log öffnen</span>
|
>
|
||||||
</button>
|
<button className="menu-submenu-trigger">Logs öffnen</button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openRenameLog().catch(() => {}); }}>
|
{openSubmenu === "hilfe-log" && (
|
||||||
<span>Rename-Log öffnen</span>
|
<div className="menu-submenu-dropdown">
|
||||||
</button>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog().catch(() => {}); }}><span>Haupt-Log</span></button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}><span>Audit-Log</span></button>
|
||||||
<span>Session-Log öffnen</span>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openRenameLog().catch(() => {}); }}><span>Rename-Log</span></button>
|
||||||
</button>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}><span>Session-Log</span></button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openTraceLog().catch(() => {}); }}>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openTraceLog().catch(() => {}); }}><span>Trace-Log</span></button>
|
||||||
<span>Trace-Log öffnen</span>
|
</div>
|
||||||
</button>
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="menu-submenu"
|
||||||
|
onMouseEnter={() => setOpenSubmenu("hilfe-remote")}
|
||||||
|
onMouseLeave={() => setOpenSubmenu(null)}
|
||||||
|
>
|
||||||
|
<button className="menu-submenu-trigger">Remote-Support</button>
|
||||||
|
{openSubmenu === "hilfe-remote" && (
|
||||||
|
<div className="menu-submenu-dropdown">
|
||||||
|
<button className="menu-dropdown-item" onClick={() => { void onExportSupportBundle(); }}><span>Support-Bundle exportieren</span></button>
|
||||||
|
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}><span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="menu-separator" />
|
<div className="menu-separator" />
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onExportSupportBundle(); }}>
|
|
||||||
<span>Support-Bundle exportieren</span>
|
|
||||||
</button>
|
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}>
|
|
||||||
<span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span>
|
|
||||||
</button>
|
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onShowRecentErrors(); }}>
|
<button className="menu-dropdown-item" onClick={() => { void onShowRecentErrors(); }}>
|
||||||
<span>Letzte Fehler anzeigen</span>
|
<span>Letzte Fehler anzeigen</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onRunDebugSetupCheck(); }}>
|
<div
|
||||||
<span>Debug-Setup prüfen</span>
|
className="menu-submenu"
|
||||||
</button>
|
onMouseEnter={() => setOpenSubmenu("hilfe-diagnose")}
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}>
|
onMouseLeave={() => setOpenSubmenu(null)}
|
||||||
<span>Debug-Token rotieren</span>
|
>
|
||||||
</button>
|
<button className="menu-submenu-trigger">Diagnose</button>
|
||||||
|
{openSubmenu === "hilfe-diagnose" && (
|
||||||
|
<div className="menu-submenu-dropdown">
|
||||||
|
<button className="menu-dropdown-item" onClick={() => { void onRunDebugSetupCheck(); }}><span>Debug-Setup prüfen</span></button>
|
||||||
|
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}><span>Debug-Token rotieren</span></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="menu-separator" />
|
<div className="menu-separator" />
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void onCheckUpdates(); }}>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void onCheckUpdates(); }}>
|
||||||
<span>Suche Aktualisierungen</span>
|
<span>Suche Aktualisierungen</span>
|
||||||
@ -4488,6 +4628,13 @@ export function App(): ReactElement {
|
|||||||
<button className={tab === "history" ? "tab active" : "tab"} onClick={() => setTab("history")}>Verlauf</button>
|
<button className={tab === "history" ? "tab active" : "tab"} onClick={() => setTab("history")}>Verlauf</button>
|
||||||
<button className={tab === "statistics" ? "tab active" : "tab"} onClick={() => setTab("statistics")}>Statistiken</button>
|
<button className={tab === "statistics" ? "tab active" : "tab"} onClick={() => setTab("statistics")}>Statistiken</button>
|
||||||
<div className="tab-actions">
|
<div className="tab-actions">
|
||||||
|
{tab === "downloads" && (
|
||||||
|
<DownloadSpeedSparkline
|
||||||
|
items={snapshot.session.items}
|
||||||
|
running={snapshot.session.running}
|
||||||
|
paused={snapshot.session.paused}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{tab === "downloads" && (
|
{tab === "downloads" && (
|
||||||
<input
|
<input
|
||||||
className="search-input tab-search"
|
className="search-input tab-search"
|
||||||
@ -4502,7 +4649,7 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
<main className="tab-content">
|
<main className="tab-content">
|
||||||
{tab === "collector" && (
|
{tab === "collector" && (
|
||||||
<section className="grid-two">
|
<section className="grid-two collector-view">
|
||||||
<article className="card wide">
|
<article className="card wide">
|
||||||
<div className="collector-header">
|
<div className="collector-header">
|
||||||
<h3>Linksammler</h3>
|
<h3>Linksammler</h3>
|
||||||
@ -4811,7 +4958,7 @@ export function App(): ReactElement {
|
|||||||
<span className="stat-eyebrow">{item.eyebrow}</span>
|
<span className="stat-eyebrow">{item.eyebrow}</span>
|
||||||
<span className="stat-label">{item.label}</span>
|
<span className="stat-label">{item.label}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className={`stat-value${item.danger ? " danger" : ""}${item.compactValue ? " stat-value-compact" : ""}`}>{item.value}</span>
|
<StatValueView value={item.value} compact={item.compactValue} danger={item.danger} />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div key={item.key} className="stat-item">
|
<div key={item.key} className="stat-item">
|
||||||
@ -4819,7 +4966,7 @@ export function App(): ReactElement {
|
|||||||
<span className="stat-eyebrow">{item.eyebrow}</span>
|
<span className="stat-eyebrow">{item.eyebrow}</span>
|
||||||
<span className="stat-label">{item.label}</span>
|
<span className="stat-label">{item.label}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className={`stat-value${item.compactValue ? " stat-value-compact" : ""}`}>{item.value}</span>
|
<StatValueView value={item.value} compact={item.compactValue} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -5974,14 +6121,6 @@ export function App(): ReactElement {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<footer className="status-bar">
|
<footer className="status-bar">
|
||||||
<span>Pakete: {snapshot.stats.totalPackages}</span>
|
|
||||||
<span>Links: {Object.keys(snapshot.session.items).length}</span>
|
|
||||||
<span>Session: {humanSize(snapshot.stats.totalDownloaded)}</span>
|
|
||||||
<span>Gesamt: {humanSize(snapshot.stats.totalDownloadedAllTime)}</span>
|
|
||||||
<span>Hoster: {providerStats.length}</span>
|
|
||||||
<span>{snapshot.speedText}</span>
|
|
||||||
<span>{snapshot.etaText}</span>
|
|
||||||
<span className="footer-spacer" />
|
|
||||||
{totalPackageCount > 0 && (
|
{totalPackageCount > 0 && (
|
||||||
<button className="btn footer-btn" title={allPackagesCollapsed ? "Alle Pakete in der Liste ausklappen und Details anzeigen" : "Alle Pakete in der Liste einklappen und nur die Kopfzeilen anzeigen"} onClick={() => {
|
<button className="btn footer-btn" title={allPackagesCollapsed ? "Alle Pakete in der Liste ausklappen und Details anzeigen" : "Alle Pakete in der Liste einklappen und nur die Kopfzeilen anzeigen"} onClick={() => {
|
||||||
setCollapsedPackages((prev) => {
|
setCollapsedPackages((prev) => {
|
||||||
@ -6014,6 +6153,14 @@ export function App(): ReactElement {
|
|||||||
Clipboard: An
|
Clipboard: An
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<span className="footer-spacer" />
|
||||||
|
<span>Pakete: {snapshot.stats.totalPackages}</span>
|
||||||
|
<span>Links: {Object.keys(snapshot.session.items).length}</span>
|
||||||
|
<span>Session: {humanSize(snapshot.stats.totalDownloaded)}</span>
|
||||||
|
<span>Gesamt: {humanSize(snapshot.stats.totalDownloadedAllTime)}</span>
|
||||||
|
<span>Hoster: {providerStats.length}</span>
|
||||||
|
<span>{snapshot.speedText}</span>
|
||||||
|
<span>{snapshot.etaText}</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{updateInstallProgress && (
|
{updateInstallProgress && (
|
||||||
|
|||||||
@ -536,6 +536,33 @@ body,
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.speed-sparkline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: var(--field);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-sparkline-canvas {
|
||||||
|
width: 116px;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-sparkline-value {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 66px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
background: var(--tab-bg);
|
background: var(--tab-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@ -579,6 +606,20 @@ body,
|
|||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collector-view {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collector-view .card.wide {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collector-view .card textarea {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 120px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
.card h3 {
|
.card h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@ -2531,7 +2572,7 @@ td {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1.5fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@ -2579,10 +2620,10 @@ td {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
min-height: 96px;
|
min-height: 82px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 12px 14px;
|
padding: 11px 14px;
|
||||||
background: var(--field);
|
background: var(--field);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@ -2600,16 +2641,17 @@ td {
|
|||||||
.stat-top {
|
.stat-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 3px;
|
gap: 2px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-eyebrow {
|
.stat-eyebrow {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: 9.5px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.14em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item.stat-item-clickable {
|
.stat-item.stat-item-clickable {
|
||||||
@ -2623,25 +2665,44 @@ td {
|
|||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: clamp(22px, 1.35vw, 30px);
|
display: flex;
|
||||||
font-weight: 700;
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.stat-value-compact {
|
.stat-num {
|
||||||
font-size: clamp(18px, 1.05vw, 24px);
|
font-size: clamp(22px, 1.35vw, 30px);
|
||||||
line-height: 1.2;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.danger {
|
.stat-unit {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.stat-value-compact {
|
||||||
|
font-size: clamp(18px, 1.05vw, 24px);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.stat-idle,
|
||||||
|
.stat-value.stat-idle .stat-num {
|
||||||
|
color: var(--muted);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.danger,
|
||||||
|
.stat-value.danger .stat-num {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2718,7 +2779,7 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bar-fill.completed {
|
.bar-fill.completed {
|
||||||
background: linear-gradient(90deg, #f2942d, #ff7a5c);
|
background: var(--accent, #f2942d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-detail {
|
.provider-detail {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user