feat: refine widget interactions
This commit is contained in:
1 parent
a8a7545f40
commit
842e86012d
107 files changed
+3144
-1233
No files matched your search
@@ -9,7 +9,7 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { AveragesSection } from "./averages";
|
||||
import { combineSpeedtestDashboards } from "./helpers";
|
||||
import { combineSpeedtestDashboards, getCompactSections } from "./helpers";
|
||||
import { LatestResultSection } from "./latest-result";
|
||||
import { RecentResultsSection } from "./recent-results";
|
||||
|
||||
@@ -48,6 +48,11 @@ export default function SpeedtestTrackerWidget({
|
||||
const averages = options.showStats && combined.stats && (
|
||||
<AveragesSection stats={combined.stats} width={displayMode === "advanced" ? advancedSectionWidth : width} />
|
||||
);
|
||||
const compactSections = getCompactSections(height, {
|
||||
latest: Boolean(latest),
|
||||
chart: hasChart,
|
||||
averages: Boolean(averages),
|
||||
});
|
||||
|
||||
if (displayMode === "advanced") {
|
||||
return (
|
||||
@@ -85,13 +90,13 @@ export default function SpeedtestTrackerWidget({
|
||||
|
||||
return (
|
||||
<Stack h="100%" gap="sm" p="xs" style={{ overflow: "hidden" }}>
|
||||
{hasStatSection && (
|
||||
{(compactSections.latest || compactSections.averages) && (
|
||||
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>
|
||||
{latest && <div style={{ flex: 1, minHeight: 0 }}>{latest}</div>}
|
||||
{averages && height >= 170 && <div style={{ flex: 1, minHeight: 0 }}>{averages}</div>}
|
||||
{compactSections.latest && <div style={{ flex: 1, minHeight: 0 }}>{latest}</div>}
|
||||
{compactSections.averages && <div style={{ flex: 1, minHeight: 0 }}>{averages}</div>}
|
||||
</Stack>
|
||||
)}
|
||||
{hasChart && (
|
||||
{compactSections.chart && (
|
||||
<div style={{ flex: 2, minHeight: 0 }}>
|
||||
<RecentResultsSection results={recentFiltered} showPingGraph={options.showPingGraph} />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { SpeedtestTrackerDashboardData, SpeedtestTrackerStats } from "@homarr/integrations/types";
|
||||
|
||||
import { combineSpeedtestDashboards, mergeStats } from "./helpers";
|
||||
import { combineSpeedtestDashboards, getCompactSections, mergeStats } from "./helpers";
|
||||
|
||||
const stats = (average: number, totalResults: number): SpeedtestTrackerStats => ({
|
||||
ping: { avg: average, min: average, max: average },
|
||||
@@ -26,3 +26,16 @@ describe("speedtest dashboard aggregation", () => {
|
||||
expect(combineSpeedtestDashboards(dashboards).latestResult?.id).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCompactSections", () => {
|
||||
const available = { latest: true, chart: true, averages: true };
|
||||
|
||||
it("prioritizes the latest result in a short widget", () => {
|
||||
expect(getCompactSections(180, available)).toEqual({ latest: true, chart: false, averages: false });
|
||||
});
|
||||
|
||||
it("reveals history before averages as height grows", () => {
|
||||
expect(getCompactSections(300, available)).toEqual({ latest: true, chart: true, averages: false });
|
||||
expect(getCompactSections(400, available)).toEqual({ latest: true, chart: true, averages: true });
|
||||
});
|
||||
});
|
||||
@@ -75,3 +75,21 @@ export const formatStatsSpeed = (band: SpeedtestTrackerStats["download"]): strin
|
||||
if (band.avg_bits != null) return formatBitsPerSec(band.avg_bits);
|
||||
return formatBitsPerSec(band.avg * 8);
|
||||
};
|
||||
|
||||
type CompactSection = "latest" | "chart" | "averages";
|
||||
|
||||
export const getCompactSections = (
|
||||
height: number,
|
||||
available: Record<CompactSection, boolean>,
|
||||
): Record<CompactSection, boolean> => {
|
||||
const budget = height < 240 ? 1 : height < 360 ? 2 : 3;
|
||||
const visible = new Set(
|
||||
(["latest", "chart", "averages"] as const).filter((section) => available[section]).slice(0, budget),
|
||||
);
|
||||
|
||||
return {
|
||||
latest: visible.has("latest"),
|
||||
chart: visible.has("chart"),
|
||||
averages: visible.has("averages"),
|
||||
};
|
||||
};
|
||||
@@ -26,7 +26,7 @@ export function LatestResultSection({ result, width = 500 }: { result: Speedtest
|
||||
<Stack gap={6} h="100%">
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<SectionLabel>{t("latestResult")}</SectionLabel>
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text size="xs" c="dimmed" truncate title={timestamp} style={{ minWidth: 0 }}>
|
||||
{timestamp}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Text } from "@mantine/core";
|
||||
|
||||
export function SectionLabel({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Text size="xs" fw={600} c="dimmed" tt="uppercase" style={{ letterSpacing: "0.05em" }}>
|
||||
<Text size="xs" fw={600} c="dimmed">
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Card, Flex, Text } from "@mantine/core";
|
||||
import { Box, Flex, Text, Tooltip, VisuallyHidden } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
@@ -21,34 +21,37 @@ export function SpeedStatCard({ icon: Icon, color, value, label, compact = false
|
||||
const hideLabel = height > 0 && height <= 38;
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
p={compact ? "xs" : "sm"}
|
||||
radius={board.itemRadius}
|
||||
bg={`var(--mantine-color-${color}-light)`}
|
||||
h="100%"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Flex
|
||||
<Tooltip label={`${label}: ${value}`} withArrow>
|
||||
<Box
|
||||
ref={ref}
|
||||
p={compact ? "xs" : "sm"}
|
||||
bg={`var(--mantine-color-${color}-light)`}
|
||||
h="100%"
|
||||
w="100%"
|
||||
align="center"
|
||||
justify="center"
|
||||
direction={isWide ? "row" : "column"}
|
||||
gap={isWide ? 8 : 4}
|
||||
style={{ flex: 1, borderRadius: `var(--mantine-radius-${board.itemRadius})`, minWidth: 0 }}
|
||||
>
|
||||
<Icon size={compact ? 16 : 20} color={`var(--mantine-color-${color}-5)`} style={{ flexShrink: 0 }} />
|
||||
<Flex direction="column" align={isWide ? "flex-start" : "center"} gap={0}>
|
||||
<Text size={compact ? "sm" : "md"} fw={700} ta="center" lh={1.1}>
|
||||
{value}
|
||||
</Text>
|
||||
{!hideLabel && (
|
||||
<Text size="xs" c="dimmed" ta="center" lh={1.3}>
|
||||
{label}
|
||||
<Flex
|
||||
h="100%"
|
||||
w="100%"
|
||||
align="center"
|
||||
justify="center"
|
||||
direction={isWide ? "row" : "column"}
|
||||
gap={isWide ? 8 : 4}
|
||||
style={{ minWidth: 0 }}
|
||||
>
|
||||
<Icon size={compact ? 16 : 20} color={`var(--mantine-color-${color}-5)`} style={{ flexShrink: 0 }} />
|
||||
<Flex direction="column" align={isWide ? "flex-start" : "center"} gap={0} style={{ minWidth: 0 }}>
|
||||
<Text size={compact ? "sm" : "md"} fw={700} ta="center" lh={1.1} truncate w="100%">
|
||||
{value}
|
||||
</Text>
|
||||
)}
|
||||
{!hideLabel && (
|
||||
<Text size="xs" c="dimmed" ta="center" lh={1.3} truncate w="100%">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
{hideLabel && <VisuallyHidden>{label}</VisuallyHidden>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user