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

+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;