fix: balance app and health widget scaling

This commit is contained in:
ajnart committed 2026-09-09 14:31:14 +02:00
1 parent 3aac106156
commit 3f2f26bea7
10 files changed
+63 -12

No files matched your search

+2
View File
@@ -14,6 +14,8 @@ import { appWidget } from ".";
The App widget opens an application or website and can show whether Homarr can reach it. Configure the target and status
check from the tile's edit actions.
App icons, labels, and spacing adapt to the visible tile size. On small tiles, labels use at most two lines and shrink with the tile to leave room for the icon.
### Adding the widget
<AddingWidget />
@@ -25,6 +25,8 @@ This widget displays information about the health and status of your system(s),
CPU usage, CPU temperature, memory usage, and GPU utilization are shown as compact gauges with threshold colors and their original units.
Gauges switch to a smaller layout when the visible widget is narrow or short, including when fitting more columns or resizing the browser. Gauge labels stay proportional as the board scales.
Advanced view is a complete health workspace: it shows every available CPU, temperature, memory, GPU, filesystem, update, reboot, and system-information section even when compact visibility switches hide some of them on the board. Proxmox clusters likewise show uptime, CPU, memory, and every node, VM, container, and storage section. Storage-volume filters, units, and permissions remain in effect.
### Screenshots
+9 -1
View File
@@ -1,8 +1,16 @@
.appText {
min-width: 0;
min-height: 0;
flex-shrink: 1;
overflow-wrap: anywhere;
overflow: hidden;
}
.appIcon {
max-height: 100%;
max-width: 100%;
overflow: auto;
flex: 1 !important;
flex: 1;
object-fit: contain;
scale: 0.8;
transition: scale 0.2s ease-in-out;
+28 -10
View File
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react";
import { Fragment, Suspense } from "react";
import { Box, Flex, rem, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
import { Box, Flex, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
import { IconMinus } from "@tabler/icons-react";
import combineClasses from "clsx";
@@ -21,7 +21,13 @@ import classes from "./app.module.css";
import { PingDot } from "./ping/ping-dot";
import { PingIndicator } from "./ping/ping-indicator";
export default function AppWidget({ options, isEditMode, height, width }: WidgetComponentProps<"app">) {
export default function AppWidget({
options,
isEditMode,
height,
width,
displayScale = 1,
}: WidgetComponentProps<"app">) {
const tCommon = useI18n("common");
const settings = useSettings();
const board = useRequiredBoard();
@@ -33,7 +39,13 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
if (isInitialWidgetQueryPending(appQuery)) return <WidgetQueryLoadingState />;
if (!app) return <WidgetEmptyState />;
const isTiny = height < 100 || width < 100;
// Readable board tokens stay fixed on screen, but must yield space to the icon
// when the tile itself becomes small. Below 100px, scale the whole composition.
let scale = 1;
if (Number.isFinite(displayScale) && displayScale > 0) scale = displayScale;
const contentScale = Math.min(1 / Math.min(scale, 1), Math.min(width, height) / 100);
const textSize = `${14 * contentScale}px`;
const spacing = 12 * contentScale;
const isColumnLayout = options.layout.startsWith("column");
return (
@@ -44,22 +56,26 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
enabled={options.descriptionDisplayMode === "tooltip" && Boolean(app.description) && !isEditMode}
>
<Flex
p={isTiny ? 4 : "sm"}
className={combineClasses("app-flex-wrapper", app.name, app.id, href && classes.appWithUrl)}
h="100%"
w="100%"
direction={options.layout}
justify="center"
align="center"
gap={isColumnLayout ? 0 : "sm"}
style={{ padding: spacing, gap: isColumnLayout ? 0 : spacing / 2 }}
onContextMenu={isEditMode ? (e) => e.preventDefault() : undefined}
>
<Stack gap={0}>
<Stack
gap={0}
className={classes.appText}
style={{ maxWidth: isColumnLayout ? "100%" : "50%", maxHeight: isColumnLayout ? "50%" : "100%" }}
>
{options.showTitle && (
<Text
className="app-title"
fw={700}
size={isTiny ? rem(8) : "sm"}
lineClamp={2}
style={{ fontSize: textSize }}
ta={isColumnLayout ? "center" : undefined}
>
{app.name}
@@ -68,10 +84,10 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
{options.descriptionDisplayMode === "normal" && (
<Text
className="app-description"
size={isTiny ? rem(8) : "sm"}
style={{ fontSize: textSize }}
ta={isColumnLayout ? "center" : undefined}
c="dimmed"
lineClamp={4}
lineClamp={2}
>
{app.description?.split("\n").map((line, index) => (
<Fragment key={index}>
@@ -88,9 +104,11 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
alt={app.name}
className={combineClasses(classes.appIcon, "app-icon")}
style={{
flex: isColumnLayout ? undefined : "0 0 50%",
height: "100%",
width: "100%",
minWidth: "20%",
minWidth: 0,
minHeight: 0,
maxWidth: isColumnLayout ? undefined : "50%",
}}
/>
@@ -5,6 +5,7 @@ import { IconCpu } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
import classes from "../system-health.module.css";
export const CpuRing = ({
cpuUtilization,
@@ -17,6 +18,7 @@ export const CpuRing = ({
}) => {
return (
<GaugeChart
classNames={{ label: classes.gaugeLabel }}
className="health-monitoring-cpu"
aria-label={ariaLabel}
roundCaps
@@ -5,6 +5,7 @@ import { IconCpu } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
import classes from "../system-health.module.css";
const CPU_TEMPERATURE_MIN_CELSIUS = 0;
const CPU_TEMPERATURE_MAX_CELSIUS = 100;
@@ -31,6 +32,7 @@ export const CpuTempRing = ({
return (
<GaugeChart
classNames={{ label: classes.gaugeLabel }}
className="health-monitoring-cpu-temperature"
aria-label={ariaLabel}
roundCaps
@@ -5,6 +5,7 @@ import { IconDeviceDesktop } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
import classes from "../system-health.module.css";
interface GpuRingProps {
gpu: {
@@ -43,6 +44,7 @@ export const GpuRing = ({ gpu, isTiny, fahrenheit, ariaLabel }: GpuRingProps) =>
multiline
>
<GaugeChart
classNames={{ label: classes.gaugeLabel }}
className={`health-monitoring-gpu health-monitoring-gpu-${gpu.gpuId}`}
aria-label={ariaLabel}
roundCaps
@@ -6,6 +6,7 @@ import { useByteFormatter } from "@homarr/settings";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
import classes from "../system-health.module.css";
export const MemoryRing = ({
available,
@@ -24,6 +25,7 @@ export const MemoryRing = ({
return (
<Tooltip label={`${memoryUsage.memUsed.percent}%`}>
<GaugeChart
classNames={{ label: classes.gaugeLabel }}
className="health-monitoring-memory"
aria-label={ariaLabel}
roundCaps
@@ -33,3 +33,12 @@
transition: none;
}
}
/* Gauge labels live inside an SVG foreignObject. The SVG already scales its
* coordinate system to the compensated gauge size; compensating again makes
* the label grow as the board shrinks. */
.gaugeLabel {
--mantine-scale: 1;
--board-canvas-ui-scale: 1;
--mantine-font-size-xs: 0.75rem;
}
@@ -61,6 +61,8 @@ export const SystemHealthMonitoring = ({
options,
integrationIds,
width,
height,
displayScale = 1,
displayMode,
withScrollArea = true,
}: WidgetComponentProps<"healthMonitoring"> & { withScrollArea?: boolean }) => {
@@ -75,7 +77,9 @@ export const SystemHealthMonitoring = ({
const { formatBytes, formatBytesPair } = useByteFormatter();
const isAdvanced = displayMode === "advanced";
const isTiny = !isAdvanced && width < 256;
let scale = 1;
if (!isAdvanced && Number.isFinite(displayScale) && displayScale > 0) scale = displayScale;
const isTiny = !isAdvanced && (width * scale < 256 || height * scale < 160);
const showCpu = isAdvanced || options.cpu;
const showMemory = isAdvanced || options.memory;
const showGpu = isAdvanced || options.gpu;