fix(board-v2): icon and tiny text fix (#6704)

This commit is contained in:
Bergasha authored and GitHub committed 2026-08-26 08:44:36 +02:00
1 parent 6548da3772
commit 40e34181e7
48 files changed
+368 -172

No files matched your search

+29
View File
@@ -1,3 +1,4 @@
import type { CSSProperties } from "react";
import type { Icon123, IconProps } from "@tabler/icons-react";
export * from "./src";
@@ -5,3 +6,31 @@ export * from "./src";
export type TablerIcon = typeof Icon123;
export type TablerIconProps = IconProps;
/**
* Icon sizes tied to Mantine's font-size scale instead of raw pixel
* numbers, so icons stay legible under the board's CSS zoom scaling.
* Pass as the `style` prop (not `size`) - tabler icons convert `size` into
* raw SVG width/height attributes, and CSS `var()` isn't guaranteed to
* resolve there (Firefox especially). `style` is real CSS, so it does.
*/
export const iconSizes = {
xs: { width: "var(--mantine-font-size-xs)", height: "var(--mantine-font-size-xs)", flexShrink: 0 },
sm: { width: "var(--mantine-font-size-sm)", height: "var(--mantine-font-size-sm)", flexShrink: 0 },
md: { width: "var(--mantine-font-size-md)", height: "var(--mantine-font-size-md)", flexShrink: 0 },
lg: { width: "var(--mantine-font-size-lg)", height: "var(--mantine-font-size-lg)", flexShrink: 0 },
xl: { width: "var(--mantine-font-size-xl)", height: "var(--mantine-font-size-xl)", flexShrink: 0 },
} as const satisfies Record<string, CSSProperties>;
/**
* For icon/element sizes that don't fit one of `iconSizes`' fixed tokens (e.g. a deliberately
* oversized icon). Same reasoning as `iconSizes`: pass as `style`, not a raw `size` number - a
* bare pixel value never re-scales when the board's zoom level changes, so it renders at a
* fraction of its intended size on any board that isn't at 100% zoom (which is most boards with
* more than a couple of widgets).
*/
export const zoomCompensatedSize = (px: number): CSSProperties => ({
width: `calc(${px}px * var(--board-canvas-ui-scale, 1))`,
height: `calc(${px}px * var(--board-canvas-ui-scale, 1))`,
flexShrink: 0,
});
@@ -5,6 +5,7 @@ import { Text } from "@mantine/core";
import type { AudiobookshelfDashboardData, NavidromeDashboardData } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import classes from "./component.module.css";
import type { AudioStatsBackend, AudioStatsDisplayOptions } from "./shared";
@@ -66,7 +67,7 @@ export function AudioStatsContent({
>
{visibleStats.map(({ optionKey, statKey, value, Icon }) => (
<div key={optionKey} className={`${classes.statTile} ${statTileClassByCompact[compactKey]}`}>
<Icon className={classes.statIcon} size={iconSize} stroke={1.5} />
<Icon className={classes.statIcon} style={zoomCompensatedSize(iconSize)} stroke={1.5} />
<span className={classes.statValue}>{value}</span>
<span className={classes.statLabel}>{t(statKey as never)}</span>
</div>
+2 -1
View File
@@ -7,6 +7,7 @@ import { IconAlertTriangle, IconBell, IconDeviceTv, IconMovie, IconPlugConnected
import { clientApi } from "@homarr/api/client";
import { getIntegrationName } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
@@ -122,7 +123,7 @@ export default function BazarrWidget({
return (
<div key={statKey} className={`${classes.statTile} ${isWarning ? classes.statTileWarning : ""}`}>
<Icon className={classes.statIcon} size={iconSize} stroke={1.5} />
<Icon className={classes.statIcon} style={zoomCompensatedSize(iconSize)} stroke={1.5} />
<span className={`${classes.statValue} ${isWarning ? classes.statValueWarning : ""}`}>
{typeof value === "string" && value.trim() === "" ? "—" : value}
</span>
@@ -35,6 +35,7 @@ import { formatBytes } from "@homarr/common";
import { invariantTechnicalLabels } from "@homarr/definitions";
import { useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import classes from "./component.module.css";
@@ -207,7 +208,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<MetricRow
key="cpu"
icon={<Cpu size={sz.iconSize} />}
icon={<Cpu style={zoomCompensatedSize(sz.iconSize)} />}
label={invariantTechnicalLabels.cpu}
value={formatPercent(s.cpu)}
progress={{ value: s.cpu, color: thresholdColor(s.cpu) }}
@@ -222,7 +223,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<MetricRow
key="mem"
icon={<MemoryStick size={sz.iconSize} />}
icon={<MemoryStick style={zoomCompensatedSize(sz.iconSize)} />}
label={t("metric.memory")}
value={formatPercent(s.memory)}
progress={{ value: s.memory, color: thresholdColor(s.memory) }}
@@ -236,7 +237,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
key: "showDisk",
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<Group key="disk" gap="xs" wrap="nowrap" justify="space-between" style={{ minHeight: sz.rowHeight }}>
<HardDrive size={sz.iconSize} />
<HardDrive style={zoomCompensatedSize(sz.iconSize)} />
<Text size={sz.fontSize} c="dimmed" w={sz.labelMiw} style={{ whiteSpace: "nowrap", flexShrink: 0 }}>
{t("metric.disk")}
</Text>
@@ -251,7 +252,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<MetricRow
key="gpu"
icon={<Monitor size={sz.iconSize} />}
icon={<Monitor style={zoomCompensatedSize(sz.iconSize)} />}
label={invariantTechnicalLabels.gpu}
value={formatPercent(s.gpu)}
progress={{ value: s.gpu, color: thresholdColor(s.gpu) }}
@@ -266,7 +267,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<MetricRow
key="la"
icon={<Activity size={sz.iconSize} />}
icon={<Activity style={zoomCompensatedSize(sz.iconSize)} />}
label={t("metric.loadAvg")}
value={formatLoadAvg(s.loadAvg)}
size={sz}
@@ -280,7 +281,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<MetricRow
key="net"
icon={<Network size={sz.iconSize} />}
icon={<Network style={zoomCompensatedSize(sz.iconSize)} />}
label={t("metric.net")}
value={formatByteRate(s.netBytes)}
size={sz}
@@ -294,7 +295,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<MetricRow
key="temp"
icon={<Thermometer size={sz.iconSize} />}
icon={<Thermometer style={zoomCompensatedSize(sz.iconSize)} />}
label={t("metric.temp")}
value={formatTemp(s.temp, false)}
size={sz}
@@ -311,7 +312,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
return (
<MetricRow
key="bat"
icon={<Icon size={sz.iconSize} />}
icon={<Icon style={zoomCompensatedSize(sz.iconSize)} />}
label={t("metric.battery")}
value={`${s.battery?.[0] ?? 0}%`}
size={sz}
@@ -326,7 +327,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, _t: SystemCardProps["t"], sz: SizeConfig, tCommon: SystemCardProps["tCommon"]) => (
<MetricRow
key="svc"
icon={<Server size={sz.iconSize} />}
icon={<Server style={zoomCompensatedSize(sz.iconSize)} />}
label={tCommon("services")}
value={String(s.services)}
size={sz}
@@ -340,7 +341,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<MetricRow
key="up"
icon={<Activity size={sz.iconSize} />}
icon={<Activity style={zoomCompensatedSize(sz.iconSize)} />}
label={t("metric.uptime")}
value={formatUptime(s.uptime)}
size={sz}
@@ -354,7 +355,7 @@ const metricRenderers: BeszelMetricRenderer[] = [
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
<MetricRow
key="agent"
icon={<Wifi size={sz.iconSize} />}
icon={<Wifi style={zoomCompensatedSize(sz.iconSize)} />}
label={t("metric.agent")}
value={s.agentVersion}
size={sz}
@@ -487,7 +488,7 @@ export default function BeszelSystemGridWidget({
</Group>
<Center h="100%">
<Stack align="center" gap="xs">
<IconServerOff size={28} opacity={0.5} />
<IconServerOff style={zoomCompensatedSize(28)} opacity={0.5} />
<Text size="sm" c="dimmed">
{t("empty.noSystems")}
</Text>
@@ -11,6 +11,7 @@ import { constructBoardPermissions } from "@homarr/auth/shared";
import { useOptionalBoard } from "@homarr/boards/context";
import { showErrorNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { iconSizes, zoomCompensatedSize } from "@homarr/ui";
import classes from "./component.module.css";
@@ -147,7 +148,7 @@ export default function BeszelSystemStatsWidget({
</Group>
<Center h="100%">
<Stack align="center" gap="xs">
<IconServerOff size={28} opacity={0.5} />
<IconServerOff style={zoomCompensatedSize(28)} opacity={0.5} />
<Text size="sm" c="dimmed">
{tBeszel("empty.noSystems")}
</Text>
@@ -161,7 +162,7 @@ export default function BeszelSystemStatsWidget({
return (
<Center h="100%">
<Stack align="center" gap="sm" p="md">
<IconQuestionMark size={40} />
<IconQuestionMark style={zoomCompensatedSize(40)} />
<Text size="sm" c="dimmed" ta="center">
{t("error.systemNotFound")}
</Text>
@@ -199,7 +200,7 @@ export default function BeszelSystemStatsWidget({
<Button
variant="default"
size="compact-xs"
leftSection={<IconServer size="var(--mantine-font-size-sm)" />}
leftSection={<IconServer style={iconSizes.sm} />}
className={classes.beszelStatsSystemToggle}
disabled={isSelectionSavePending}
>
@@ -18,6 +18,7 @@ import {
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { constructBoardPermissions } from "@homarr/auth/shared";
import { useOptionalBoard } from "@homarr/boards/context";
import { formatBytes } from "@homarr/common";
@@ -202,7 +203,7 @@ export default function BeszelSystemTableWidget({
ellipsis: true,
title: (
<Group gap={4} wrap="nowrap">
<Server size={size.iconSize} />
<Server style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.system")}</Text>
</Group>
),
@@ -269,7 +270,7 @@ export default function BeszelSystemTableWidget({
width: 140,
title: (
<Group gap={4} wrap="nowrap">
<Cpu size={size.iconSize} />
<Cpu style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{invariantTechnicalLabels.cpu}</Text>
</Group>
),
@@ -281,7 +282,7 @@ export default function BeszelSystemTableWidget({
width: 140,
title: (
<Group gap={4} wrap="nowrap">
<MemoryStick size={size.iconSize} />
<MemoryStick style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.memory")}</Text>
</Group>
),
@@ -293,7 +294,7 @@ export default function BeszelSystemTableWidget({
width: 160,
title: (
<Group gap={4} wrap="nowrap">
<HardDrive size={size.iconSize} />
<HardDrive style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.disk")}</Text>
</Group>
),
@@ -313,7 +314,7 @@ export default function BeszelSystemTableWidget({
width: 140,
title: (
<Group gap={4} wrap="nowrap">
<Monitor size={size.iconSize} />
<Monitor style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{invariantTechnicalLabels.gpu}</Text>
</Group>
),
@@ -325,7 +326,7 @@ export default function BeszelSystemTableWidget({
width: 100,
title: (
<Group gap={4} wrap="nowrap">
<Activity size={size.iconSize} />
<Activity style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.loadAvg")}</Text>
</Group>
),
@@ -347,7 +348,7 @@ export default function BeszelSystemTableWidget({
width: 100,
title: (
<Group gap={4} wrap="nowrap">
<Network size={size.iconSize} />
<Network style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.net")}</Text>
</Group>
),
@@ -363,7 +364,7 @@ export default function BeszelSystemTableWidget({
width: 80,
title: (
<Group gap={4} wrap="nowrap">
<Thermometer size={size.iconSize} />
<Thermometer style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.temp")}</Text>
</Group>
),
@@ -375,7 +376,7 @@ export default function BeszelSystemTableWidget({
width: 70,
title: (
<Group gap={4} wrap="nowrap">
<Battery size={size.iconSize} />
<Battery style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.battery")}</Text>
</Group>
),
@@ -386,7 +387,7 @@ export default function BeszelSystemTableWidget({
width: 80,
title: (
<Group gap={4} wrap="nowrap">
<Server size={size.iconSize} />
<Server style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tCommon("services")}</Text>
</Group>
),
@@ -398,7 +399,7 @@ export default function BeszelSystemTableWidget({
width: 90,
title: (
<Group gap={4} wrap="nowrap">
<Activity size={size.iconSize} />
<Activity style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.uptime")}</Text>
</Group>
),
@@ -410,7 +411,7 @@ export default function BeszelSystemTableWidget({
width: 90,
title: (
<Group gap={4} wrap="nowrap">
<Wifi size={size.iconSize} />
<Wifi style={zoomCompensatedSize(size.iconSize)} />
<Text inherit>{tBeszel("metric.agent")}</Text>
</Group>
),
@@ -8,6 +8,7 @@ import { clientApi } from "@homarr/api/client";
import { invariantTechnicalLabels } from "@homarr/definitions";
import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { getUsableWidgetQueryData } from "../../common/query-state";
import { containerColors } from "./colors";
@@ -209,7 +210,7 @@ export function BeszelStatsView({
return (
<Center h={CHART_HEIGHT}>
<Stack align="center" gap="sm" p="md">
<IconPlugConnectedX size={40} />
<IconPlugConnectedX style={zoomCompensatedSize(40)} />
<Text size="sm" c="dimmed" ta="center">
{t("error.liveConnectionFailed")}
</Text>
@@ -238,7 +239,7 @@ export function BeszelStatsView({
return (
<Center h={CHART_HEIGHT}>
<Stack align="center" gap="xs">
<IconServerOff size={24} opacity={0.5} />
<IconServerOff style={zoomCompensatedSize(24)} opacity={0.5} />
<Text size="sm" c="dimmed">
{tBeszel("error.internalServerError")}
</Text>
@@ -21,6 +21,7 @@ import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
import { useI18n } from "@homarr/translation/client";
import { iconSizes, zoomCompensatedSize } from "@homarr/ui";
import { getSafeAppHref, SAFE_NEW_TAB_REL } from "../common/application-url";
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
@@ -141,7 +142,7 @@ export default function BookmarksWidget({
<Center flex={1}>
<Stack align="center" gap={6}>
<ThemeIcon variant="light" size="lg" radius="xl">
<IconBookmark size={20} />
<IconBookmark style={iconSizes.xl} />
</ThemeIcon>
<Text size="sm" c="dimmed" ta="center">
{t("empty")}
@@ -262,7 +263,7 @@ const BookmarkCard = ({
transition: reduceMotion ? undefined : "opacity 120ms ease, transform 120ms ease",
}}
>
<IconArrowUpRight size={14} />
<IconArrowUpRight style={iconSizes.sm} />
</ThemeIcon>
) : null}
</Group>
@@ -339,7 +340,7 @@ const BookmarkAvatar = ({ bookmark, iconUrl, size }: { bookmark: BookmarkItem; i
imageProps={{ referrerPolicy: "no-referrer" }}
styles={{ image: { objectFit: "contain" } }}
>
<IconLink size={Math.max(14, size / 2)} />
<IconLink style={zoomCompensatedSize(Math.max(14, size / 2))} />
</Avatar>
);
@@ -95,7 +95,14 @@ const NotificationIndicator = ({ events, size, visible }: NotificationIndicatorP
>
{notificationEvents.map((notificationEvent) => {
return (
<Box key={notificationEvent} bg={notificationEvent} h={size} w={size} p={0} style={{ borderRadius: 999 }} />
<Box
key={notificationEvent}
bg={notificationEvent}
h={size}
w={size * 2}
p={0}
style={{ borderRadius: 999 }}
/>
);
})}
</Flex>
+2 -1
View File
@@ -16,6 +16,7 @@ import { IconAlertTriangle, IconClock } from "@tabler/icons-react";
import type { Dayjs } from "dayjs";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import { formatLocalizedDate, formatLocalizedTime } from "../common/locale";
import type { WidgetComponentProps } from "../definition";
@@ -102,7 +103,7 @@ const PrimaryClock = ({ now, options, primary }: Pick<ModeProps, "now" | "option
<Stack gap={5} miw={0}>
<Group gap="xs" wrap="nowrap">
<ThemeIcon color={getPhaseColor(primary.phase)} variant="light" radius="xl" size="md">
<IconClock size={16} />
<IconClock style={iconSizes.md} />
</ThemeIcon>
<Title order={2} size="h4" fw={600}>
{primary.label}
+1 -1
View File
@@ -47,7 +47,7 @@ export default function ClockWidget({ options, width, height, displayMode }: Wid
return (
<Box className="clock-widget-container" h="100%" pos="relative">
{showWeatherCorner && (
<Box pos="absolute" top={4} left={4}>
<Box pos="absolute" top={2} left={4}>
<ClockWeatherSummary
latitude={options.weatherLocation.latitude}
longitude={options.weatherLocation.longitude}
+30 -14
View File
@@ -2,6 +2,7 @@ import { Box, Group, Loader, Stack, Text } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { isInitialWidgetQueryPending } from "../common/query-state";
import { AnimatedWeatherIcon } from "../weather/animated-icon";
@@ -46,20 +47,35 @@ export const ClockWeatherSummary = ({
};
return (
<Stack gap={detailed ? 3 : 0} align="flex-start">
<Group gap={5} wrap="nowrap">
<AnimatedWeatherIcon
animated={animateIcon}
code={weather.current.weatherCode}
isDay={weather.current.isDay}
size={detailed ? 28 : 22}
/>
<Text size={detailed ? "sm" : "xs"} fw={detailed ? 600 : undefined} c={detailed ? undefined : "dimmed"}>
{Math.round(temperature)}
{unit}
</Text>
{detailed && <WeatherDescription weatherOnly weatherCode={weather.current.weatherCode} />}
</Group>
<Stack gap={detailed ? 3 : 2} align={detailed ? "flex-start" : "center"}>
{detailed ? (
<Group gap={5} wrap="nowrap">
<AnimatedWeatherIcon
animated={animateIcon}
code={weather.current.weatherCode}
isDay={weather.current.isDay}
style={zoomCompensatedSize(28)}
/>
<Text size="sm" fw={600}>
{Math.round(temperature)}
{unit}
</Text>
<WeatherDescription weatherOnly weatherCode={weather.current.weatherCode} />
</Group>
) : (
<Stack gap={1} align="center">
<AnimatedWeatherIcon
animated={animateIcon}
code={weather.current.weatherCode}
isDay={weather.current.isDay}
style={zoomCompensatedSize(26)}
/>
<Text size="xs" c="dimmed">
{Math.round(temperature)}
{unit}
</Text>
</Stack>
)}
{detailed && (
<Box>
<Text size="xs" fw={600} lineClamp={1}>
@@ -11,6 +11,7 @@ import {
} from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import actionTargetClasses from "../common/action-target.module.css";
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
@@ -73,7 +74,11 @@ export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRow
return (
<Stack gap={0}>
<Group wrap="nowrap" gap={isTiny ? 4 : "xs"}>
<StatusIcon aria-label={statusLabel} size={isTiny ? 12 : 16} color={`var(--mantine-color-${statusColor}-6)`} />
<StatusIcon
aria-label={statusLabel}
style={zoomCompensatedSize(isTiny ? 12 : 16)}
color={`var(--mantine-color-${statusColor}-6)`}
/>
{resourceUrl ? (
<Anchor
className={actionTargetClasses.root}
+8 -1
View File
@@ -18,6 +18,7 @@ import {
import { IconBell, IconBellOff, IconHourglass } from "@tabler/icons-react";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import {
claimBrowserAlertOccurrence,
@@ -339,7 +340,13 @@ const AlertControls = ({
disabled={disabled || notificationPermission === "unsupported" || notificationPermission === "denied"}
onChange={(event) => void onNotificationsChange(event.currentTarget.checked)}
label={t("notifications")}
thumbIcon={preferences.notifications ? <IconBell size={12} /> : <IconBellOff size={12} />}
thumbIcon={
preferences.notifications ? (
<IconBell style={iconSizes.sm} />
) : (
<IconBellOff style={iconSizes.sm} />
)
}
/>
{notificationPermission === "denied" && (
<Text size="xs" c="dimmed" mt="xs">
@@ -7,7 +7,7 @@ import { IconAlertTriangle } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { Link, zoomCompensatedSize } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition";
import { isLegacyCustomWidgetMigrationError, isTerminalCustomWidgetDefinitionError } from "./migration-state";
@@ -107,7 +107,10 @@ function Unavailable({
return (
<Center h="100%" p="sm">
<Stack align="center" gap="xs">
<IconAlertTriangle size={32} color={`var(--mantine-color-${danger ? "red" : "yellow"}-6)`} />
<IconAlertTriangle
style={zoomCompensatedSize(32)}
color={`var(--mantine-color-${danger ? "red" : "yellow"}-6)`}
/>
<Text c="dimmed" size="sm" ta="center">
{message}
</Text>
@@ -26,6 +26,7 @@ import type { DnsHoleSummary } from "@homarr/integrations/types";
import type { stringOrTranslation, TranslationFunction } from "@homarr/translation";
import { translateIfNecessary } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import type { TablerIcon } from "@homarr/ui";
import type { widgetKind } from ".";
@@ -214,7 +215,14 @@ const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
direction={isLong ? "row" : "column"}
gap={0}
>
<item.icon className="summary-card-icon" size={24} style={{ minWidth: 24, minHeight: 24 }} />
<item.icon
className="summary-card-icon"
style={{
...zoomCompensatedSize(24),
minWidth: "calc(24px * var(--board-canvas-ui-scale, 1))",
minHeight: "calc(24px * var(--board-canvas-ui-scale, 1))",
}}
/>
<Flex
className="summary-card-texts"
justify="center"
+9 -3
View File
@@ -44,6 +44,7 @@ import { useModalAction } from "@homarr/modals";
import { AddDockerAppToHomarr, useDockerContainerRemovalConfirmation } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition";
import { getUsableWidgetQueryData } from "../common/query-state";
@@ -82,7 +83,7 @@ const containerMenuWidth = 240;
const rowActionButtonSize = 22;
const rowActionIconVisualSize = 32;
const footerRefreshButtonSize = 24;
const footerRefreshIconVisualSize = 30;
const footerRefreshIconVisualSize = 18;
const createContainerLogsPath = (container: Pick<DockerContainer, "endpointId" | "id" | "name">) =>
`/manage/tools/docker/logs/${container.id}?name=${encodeURIComponent(container.name)}&endpointId=${encodeURIComponent(container.endpointId)}`;
@@ -493,8 +494,13 @@ export default function DockerWidget({
style={{ position: "relative", overflow: "visible" }}
>
<IconRefresh
size={footerRefreshIconVisualSize}
style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)" }}
style={{
...zoomCompensatedSize(footerRefreshIconVisualSize),
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
/>
</ActionIcon>
</Tooltip>
@@ -5,7 +5,7 @@ import { useSession } from "@homarr/auth/client";
import type { stringOrTranslation } from "@homarr/translation";
import { translateIfNecessary } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { Link, zoomCompensatedSize } from "@homarr/ui";
import type { TablerIcon } from "@homarr/ui";
export interface BaseWidgetErrorProps {
@@ -23,7 +23,7 @@ export const BaseWidgetError = (props: BaseWidgetErrorProps) => {
return (
<Stack h="100%" align="center" justify="center" gap="md" data-homarr-widget-error>
<props.icon size={40} />
<props.icon style={zoomCompensatedSize(40)} />
<Stack gap={0}>
<Text ta="center">{translateIfNecessary(t, props.message)}</Text>
{props.showLogsLink && session?.user.permissions.includes("other-view-logs") && (
+2 -1
View File
@@ -22,6 +22,7 @@ import { clientApi } from "@homarr/api/client";
import { invariantTechnicalLabels } from "@homarr/definitions";
import type { FirewallInterfacesSummary } from "@homarr/integrations";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition";
import { calculateBandwidth, formatBitsPerSec } from "./bandwidth";
@@ -329,7 +330,7 @@ const MetricRing = ({ value, icon: Icon, size, label, t }: MetricRingProps) => {
label={
<Center style={{ flexDirection: "column" }}>
<Text size="xs">{safeValue.toFixed(1)}%</Text>
{showIcon && <Icon size={size < 96 ? 12 : 16} />}
{showIcon && <Icon style={zoomCompensatedSize(size < 96 ? 12 : 16)} />}
{showStatus && <Text size="xs">{statusLabel}</Text>}
</Center>
}
@@ -6,6 +6,7 @@ import { clientApi } from "@homarr/api/client";
import { invariantTechnicalLabels } from "@homarr/definitions";
import type { Resource } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../../common/empty-state";
import { getUsableWidgetQueryData } from "../../common/query-state";
@@ -185,7 +186,7 @@ const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
thickness={isTiny ? 2 : 4}
label={
<Center>
<IconCpu size={isTiny ? 12 : 20} />
<IconCpu style={zoomCompensatedSize(isTiny ? 12 : 20)} />
</Center>
}
sections={[{ value: cpu.value, color: cpu.value > 75 ? "orange" : "green" }]}
@@ -208,7 +209,7 @@ const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
thickness={isTiny ? 2 : 4}
label={
<Center>
<IconBrain size={isTiny ? 12 : 20} />
<IconBrain style={zoomCompensatedSize(isTiny ? 12 : 20)} />
</Center>
}
sections={[{ value: memory.value, color: memory.value > 75 ? "orange" : "green" }]}
@@ -19,6 +19,7 @@ import duration from "dayjs/plugin/duration";
import { capitalize, formatBytes, formatBytesPair } from "@homarr/common";
import type { ComputeResource, Resource, StorageResource } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
dayjs.extend(duration);
@@ -197,17 +198,18 @@ const StorageType = ({ item }: { item: StorageResource }) => {
};
const ResourceIcon = ({ type, size }: { type: Resource["type"]; size: number }) => {
const style = zoomCompensatedSize(size);
switch (type) {
case "node":
return <IconServer size={size} />;
return <IconServer style={style} />;
case "lxc":
return <IconDeviceLaptop size={size} />;
return <IconDeviceLaptop style={style} />;
case "qemu":
return <IconDeviceLaptop size={size} />;
return <IconDeviceLaptop style={style} />;
case "storage":
return <IconDatabase size={size} />;
return <IconDatabase style={style} />;
default:
console.error(`Unknown resource type: ${type as string}`);
return <IconQuestionMark size={size} />;
return <IconQuestionMark style={style} />;
}
};
@@ -1,6 +1,8 @@
import { Center, RingProgress, Text } from "@mantine/core";
import { IconCpu } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
export const CpuRing = ({ cpuUtilization, isTiny }: { cpuUtilization: number; isTiny: boolean }) => {
@@ -16,7 +18,7 @@ export const CpuRing = ({ cpuUtilization, isTiny }: { cpuUtilization: number; is
className="health-monitoring-cpu-utilization-value"
size={isTiny ? "8px" : "xs"}
>{`${cpuUtilization.toFixed(2)}%`}</Text>
<IconCpu className="health-monitoring-cpu-utilization-icon" size={isTiny ? 8 : 16} />
<IconCpu className="health-monitoring-cpu-utilization-icon" style={zoomCompensatedSize(isTiny ? 8 : 16)} />
</Center>
}
sections={[
@@ -1,6 +1,8 @@
import { Center, RingProgress, Text } from "@mantine/core";
import { IconCpu } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
export const CpuTempRing = ({
@@ -27,7 +29,7 @@ export const CpuTempRing = ({
<Text className="health-monitoring-cpu-temp-value" size={isTiny ? "8px" : "xs"}>
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
</Text>
<IconCpu className="health-monitoring-cpu-temp-icon" size={isTiny ? 8 : 16} />
<IconCpu className="health-monitoring-cpu-temp-icon" style={zoomCompensatedSize(isTiny ? 8 : 16)} />
</Center>
}
sections={[
@@ -1,6 +1,8 @@
import { Center, RingProgress, Stack, Text, Tooltip } from "@mantine/core";
import { IconDeviceDesktop } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
interface GpuRingProps {
@@ -49,7 +51,10 @@ export const GpuRing = ({ gpu, isTiny, fahrenheit }: GpuRingProps) => {
className="health-monitoring-gpu-utilization-value"
size={isTiny ? "8px" : "xs"}
>{`${gpu.processorUtilization.toFixed(0)}%`}</Text>
<IconDeviceDesktop className="health-monitoring-gpu-utilization-icon" size={isTiny ? 8 : 16} />
<IconDeviceDesktop
className="health-monitoring-gpu-utilization-icon"
style={zoomCompensatedSize(isTiny ? 8 : 16)}
/>
</Center>
}
sections={[
@@ -1,6 +1,8 @@
import { Center, RingProgress, Text } from "@mantine/core";
import { IconBrain } from "@tabler/icons-react";
import { zoomCompensatedSize } from "@homarr/ui";
import { progressColor } from "../system-health";
export const MemoryRing = ({ available, used, isTiny }: { available: number; used: number; isTiny: boolean }) => {
@@ -17,7 +19,7 @@ export const MemoryRing = ({ available, used, isTiny }: { available: number; use
<Text className="health-monitoring-memory-value" size={isTiny ? "8px" : "xs"}>
{memoryUsage.memUsed.GB}GiB
</Text>
<IconBrain className="health-monitoring-memory-icon" size={isTiny ? 8 : 16} />
<IconBrain className="health-monitoring-memory-icon" style={zoomCompensatedSize(isTiny ? 8 : 16)} />
</Center>
}
sections={[
@@ -40,6 +40,7 @@ import { useRequiredBoard } from "@homarr/boards/context";
import { formatBytes } from "@homarr/common";
import type { ScopedTranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { filterStorageVolumes, normalizeStorageDeviceName } from "../filter-storage-volumes";
import { WidgetEmptyState } from "../common/empty-state";
@@ -156,7 +157,10 @@ export const SystemHealthMonitoring = ({
onClick={() => setOpenedIntegrationId(integrationId)}
aria-label={t("popover.information")}
>
<IconInfoCircle className="health-monitoring-information-icon" size={30} />
<IconInfoCircle
className="health-monitoring-information-icon"
style={zoomCompensatedSize(30)}
/>
</ActionIcon>
</Indicator>
<Modal
@@ -284,45 +288,76 @@ const SystemInformationList = ({
memoryUsage: ReturnType<typeof formatMemoryUsage>;
t: ScopedTranslationFunction<"widget.healthMonitoring">;
compact?: boolean;
}) => (
<List
className="health-monitoring-information-list"
center
spacing={compact ? 4 : "xs"}
size={compact ? "sm" : undefined}
>
<List.Item className="health-monitoring-information-processor" icon={<IconCpu2 size={compact ? 18 : 30} />}>
{t("popover.processor", { cpuModelName: healthInfo.cpuModelName })}
</List.Item>
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={compact ? 18 : 30} />}>
{t("popover.memory", { memory: memoryUsage.memTotal.GB })}
</List.Item>
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={compact ? 18 : 30} />}>
{t("popover.memoryAvailable", {
memoryAvailable: memoryUsage.memFree.GB,
percent: String(memoryUsage.memFree.percent),
})}
</List.Item>
<List.Item className="health-monitoring-information-version" icon={<IconVersions size={compact ? 18 : 30} />}>
{t("popover.version", { version: healthInfo.version })}
</List.Item>
<List.Item className="health-monitoring-information-uptime" icon={<IconClock size={compact ? 18 : 30} />}>
{formatUptime(healthInfo.uptime, t)}
</List.Item>
{healthInfo.loadAverage && (
<List.Item className="health-monitoring-information-load-average" icon={<IconCpu size={compact ? 18 : 30} />}>
{t("popover.loadAverage")}: {healthInfo.loadAverage["1min"]}% / {healthInfo.loadAverage["5min"]}% /{" "}
{healthInfo.loadAverage["15min"]}%
}) => {
// compact is only true for the inline on-board card; the non-compact usage renders inside a portaled Modal,
// which sits outside the zoomed canvas and must not be zoom-compensated.
const iconSize = compact ? 18 : 30;
const iconSizeProp = compact ? undefined : iconSize;
const iconStyle = compact ? zoomCompensatedSize(iconSize) : undefined;
return (
<List
className="health-monitoring-information-list"
center
spacing={compact ? 4 : "xs"}
size={compact ? "sm" : undefined}
>
<List.Item
className="health-monitoring-information-processor"
icon={<IconCpu2 size={iconSizeProp} style={iconStyle} />}
>
{t("popover.processor", { cpuModelName: healthInfo.cpuModelName })}
</List.Item>
)}
<List.Item className="health-monitoring-information-updates" icon={<IconPackages size={compact ? 18 : 30} />}>
{t("popover.updatesAvailable", { count: healthInfo.availablePkgUpdates })}
</List.Item>
<List.Item className="health-monitoring-information-reboot" icon={<IconRefreshAlert size={compact ? 18 : 30} />}>
{healthInfo.rebootRequired ? t("popover.rebootRequired") : t("popover.rebootNotRequired")}
</List.Item>
</List>
);
<List.Item
className="health-monitoring-information-memory"
icon={<IconBrain size={iconSizeProp} style={iconStyle} />}
>
{t("popover.memory", { memory: memoryUsage.memTotal.GB })}
</List.Item>
<List.Item
className="health-monitoring-information-memory"
icon={<IconBrain size={iconSizeProp} style={iconStyle} />}
>
{t("popover.memoryAvailable", {
memoryAvailable: memoryUsage.memFree.GB,
percent: String(memoryUsage.memFree.percent),
})}
</List.Item>
<List.Item
className="health-monitoring-information-version"
icon={<IconVersions size={iconSizeProp} style={iconStyle} />}
>
{t("popover.version", { version: healthInfo.version })}
</List.Item>
<List.Item
className="health-monitoring-information-uptime"
icon={<IconClock size={iconSizeProp} style={iconStyle} />}
>
{formatUptime(healthInfo.uptime, t)}
</List.Item>
{healthInfo.loadAverage && (
<List.Item
className="health-monitoring-information-load-average"
icon={<IconCpu size={iconSizeProp} style={iconStyle} />}
>
{t("popover.loadAverage")}: {healthInfo.loadAverage["1min"]}% / {healthInfo.loadAverage["5min"]}% /{" "}
{healthInfo.loadAverage["15min"]}%
</List.Item>
)}
<List.Item
className="health-monitoring-information-updates"
icon={<IconPackages size={iconSizeProp} style={iconStyle} />}
>
{t("popover.updatesAvailable", { count: healthInfo.availablePkgUpdates })}
</List.Item>
<List.Item
className="health-monitoring-information-reboot"
icon={<IconRefreshAlert size={iconSizeProp} style={iconStyle} />}
>
{healthInfo.rebootRequired ? t("popover.rebootRequired") : t("popover.rebootNotRequired")}
</List.Item>
</List>
);
};
export const formatUptime = (uptimeInSeconds: number, t: ScopedTranslationFunction<"widget.healthMonitoring">) => {
const uptimeDuration = dayjs.duration(uptimeInSeconds, "seconds");
@@ -15,6 +15,7 @@ import {
import { clientApi } from "@homarr/api/client";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
@@ -257,7 +258,7 @@ function NoPhotosInAlbum() {
return (
<Center h="100%">
<Stack align="center" gap="xs">
<IconAlertCircle size={32} />
<IconAlertCircle style={iconSizes.xl} />
<Text size="sm" fw={500}>
{t("noPhotos")}
</Text>
@@ -8,6 +8,7 @@ import { getQueryKey } from "@trpc/react-query";
import { clientApi } from "@homarr/api/client";
import { formatBytes } from "@homarr/common";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
@@ -59,28 +60,28 @@ export default function ImmichServerStatsWidget({
{
key: "users",
enabled: statVisibility.showUsers,
icon: <IconUsers size={statsLayout.dense ? 16 : 18} />,
icon: <IconUsers style={zoomCompensatedSize(statsLayout.dense ? 16 : 18)} />,
label: t("users"),
value: stats.userCount.toLocaleString(locale),
},
{
key: "photos",
enabled: statVisibility.showPhotos,
icon: <IconPhoto size={statsLayout.dense ? 16 : 18} />,
icon: <IconPhoto style={zoomCompensatedSize(statsLayout.dense ? 16 : 18)} />,
label: t("photos"),
value: stats.photoCount.toLocaleString(locale),
},
{
key: "videos",
enabled: statVisibility.showVideos,
icon: <IconVideo size={statsLayout.dense ? 16 : 18} />,
icon: <IconVideo style={zoomCompensatedSize(statsLayout.dense ? 16 : 18)} />,
label: t("videos"),
value: stats.videoCount.toLocaleString(locale),
},
{
key: "storage",
enabled: statVisibility.showStorage,
icon: <IconDatabase size={statsLayout.dense ? 16 : 18} />,
icon: <IconDatabase style={zoomCompensatedSize(statsLayout.dense ? 16 : 18)} />,
label: t("storage"),
value: formatBytes(stats.totalLibraryUsageInBytes),
},
@@ -24,6 +24,7 @@ import { getQueryKey } from "@trpc/react-query";
import { clientApi } from "@homarr/api/client";
import type { MissingMediaItem, QueuedMediaItem } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
@@ -261,7 +262,11 @@ const Poster = ({ src, type, density }: { src?: string | null; type: "movie" | "
variant="light"
color={type === "movie" ? "yellow" : "blue"}
>
{type === "movie" ? <IconMovie size={size * 0.5} /> : <IconVideo size={size * 0.5} />}
{type === "movie" ? (
<IconMovie style={zoomCompensatedSize(size * 0.5)} />
) : (
<IconVideo style={zoomCompensatedSize(size * 0.5)} />
)}
</ThemeIcon>
);
};
@@ -6,6 +6,7 @@ import { IconCube, IconUsersGroup } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { formatNumber } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
@@ -71,7 +72,12 @@ export default function MinecraftServerStatusWidget({
showServerIcon &&
(data.icon ? (
<img
style={{ flex: 1, width: iconSize, maxHeight: iconSize, objectFit: "contain" }}
style={{
flex: 1,
width: `calc(${iconSize}px * var(--board-canvas-ui-scale, 1))`,
maxHeight: `calc(${iconSize}px * var(--board-canvas-ui-scale, 1))`,
objectFit: "contain",
}}
alt={`minecraft icon ${options.domain}`}
src={data.icon}
/>
@@ -84,12 +90,12 @@ export default function MinecraftServerStatusWidget({
justifyContent: "center",
}}
>
<IconCube size={iconSize} color="var(--mantine-color-gray-5)" />
<IconCube style={zoomCompensatedSize(iconSize)} color="var(--mantine-color-gray-5)" />
</Box>
))}
<Stack gap={4} w={showCapacity ? "min(100%, 420px)" : "auto"} align="stretch">
<Group gap={5} c="dimmed" align="center" justify="center">
<IconUsersGroup size={showMetadata ? "1.25rem" : "1rem"} />
<IconUsersGroup style={zoomCompensatedSize(showMetadata ? 20 : 16)} />
<Text size={showMetadata ? "lg" : isDense ? "sm" : "md"}>
{formatNumber(data.players.online, 1)} / {formatNumber(data.players.max, 1)}
</Text>
@@ -2,6 +2,7 @@ import { Center, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconWifi } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import { StatRow } from "./stat-row";
@@ -21,7 +22,7 @@ export const WifiVariant = ({
<Stack h="100%" align="center" justify="center" gap={compact ? "sm" : "md"}>
<Group gap="xs" wrap="nowrap" justify="center">
<Center w={24} h={24}>
<IconWifi size={20} />
<IconWifi style={iconSizes.xl} />
</Center>
<Text size={"md"} fw={"bold"}>
{t("variants.wifi.name")}
@@ -2,6 +2,7 @@ import { Center, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconNetwork } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import { StatRow } from "./stat-row";
@@ -21,7 +22,7 @@ export const WiredVariant = ({
<Stack h="100%" align="center" justify="center" gap={compact ? "sm" : "md"}>
<Group gap="xs" wrap="nowrap" justify="center">
<Center w={24} h={24}>
<IconNetwork size={20} />
<IconNetwork style={iconSizes.xl} />
</Center>
<Text size={"md"} fw={"bold"}>
{t("variants.wired.name")}
@@ -1,5 +1,6 @@
"use client";
import type { CSSProperties } from "react";
import { Badge, Card, Center, Group, List, ScrollArea, SimpleGrid, Stack, Text, useMantineTheme } from "@mantine/core";
import { IconCircleCheckFilled, IconCircleXFilled } from "@tabler/icons-react";
import dayjs from "dayjs";
@@ -10,6 +11,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { clientApi } from "@homarr/api/client";
import type { ScopedTranslationFunction } from "@homarr/translation";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import type { WidgetComponentProps } from "../../definition";
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
@@ -187,7 +189,7 @@ export default function NetworkControllerSummaryWidget({
<StatusIcon
status={summary.wanStatus}
label={statusLabels[getBinaryStatusKey(summary.wanStatus)]}
size={isDense ? 16 : 20}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
@@ -198,7 +200,7 @@ export default function NetworkControllerSummaryWidget({
<StatusIcon
status={summary.www.status}
label={statusLabels[getBinaryStatusKey(summary.www.status)]}
size={isDense ? 16 : 20}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
@@ -214,7 +216,7 @@ export default function NetworkControllerSummaryWidget({
<StatusIcon
status={summary.wifi.status}
label={statusLabels[getBinaryStatusKey(summary.wifi.status)]}
size={isDense ? 16 : 20}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
@@ -227,7 +229,7 @@ export default function NetworkControllerSummaryWidget({
<StatusIcon
status={summary.lan.status}
label={statusLabels[getBinaryStatusKey(summary.lan.status)]}
size={isDense ? 16 : 20}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
@@ -239,7 +241,7 @@ export default function NetworkControllerSummaryWidget({
<StatusIcon
status={summary.vpn.status}
label={statusLabels[getBinaryStatusKey(summary.vpn.status)]}
size={isDense ? 16 : 20}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
@@ -285,17 +287,19 @@ const StatusIcon = ({
status,
label,
size,
style,
}: {
status?: "enabled" | "disabled";
label: string;
size: number | string;
size?: number | string;
style?: CSSProperties;
}) => {
const mantineTheme = useMantineTheme();
if (status === "enabled") {
return <IconCircleCheckFilled aria-label={label} size={size} color={mantineTheme.colors.green[6]} />;
return <IconCircleCheckFilled aria-label={label} size={size} style={style} color={mantineTheme.colors.green[6]} />;
}
if (status === "disabled") {
return <IconCircleXFilled aria-label={label} size={size} color={mantineTheme.colors.red[6]} />;
return <IconCircleXFilled aria-label={label} size={size} style={style} color={mantineTheme.colors.red[6]} />;
}
return <IconCircleXFilled aria-label={label} size={size} color={mantineTheme.colors.gray[6]} />;
return <IconCircleXFilled aria-label={label} size={size} style={style} color={mantineTheme.colors.gray[6]} />;
};
@@ -8,6 +8,7 @@ import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { useTimeAgo } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
import { getUsableWidgetQueryData } from "../common/query-state";
@@ -158,7 +159,11 @@ const InfoDisplay = ({ date, source, dense }: { date: Date; source?: string; den
return (
<Group gap={5} align="center" wrap="nowrap">
<IconClock aria-hidden size={dense ? 12 : "1rem"} color="var(--mantine-color-dimmed)" />
<IconClock
aria-hidden
style={dense ? iconSizes.xs : iconSizes.md}
color="var(--mantine-color-dimmed)"
/>
<Text size={dense ? "xs" : "sm"} c="dimmed">
{timeAgo}
</Text>
@@ -7,6 +7,7 @@ import { IconFileDescription, IconFileText, IconInbox, IconTag, IconUsers } from
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
@@ -184,7 +185,7 @@ export default function PaperlessNgxWidget({
const Icon = statIcons[statKey];
return (
<div key={statKey} className={classes.statTile}>
<Icon className={classes.statIcon} size={iconSize} stroke={1.5} />
<Icon className={classes.statIcon} style={zoomCompensatedSize(iconSize)} stroke={1.5} />
<span className={classes.statValue}>{statValues[statKey]}</span>
<span className={classes.statLabel}>{t(statKey)}</span>
</div>
+8 -2
View File
@@ -17,6 +17,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import { iconSizes, zoomCompensatedSize } from "@homarr/ui";
import type { TablerIcon } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
@@ -234,7 +235,12 @@ export default function PatchMonWidget({
style={{ "--stat-bg": severityToMantineBgColor(severity) } as CSSProperties}
>
{showIcons && (
<Icon className={classes.statIcon} size={iconSize} stroke={1.5} color={severityToIconColor(severity)} />
<Icon
className={classes.statIcon}
style={zoomCompensatedSize(iconSize)}
stroke={1.5}
color={severityToIconColor(severity)}
/>
)}
<span className={classes.statValue}>
<Text component="span" fw={700} size="inherit" c={severityToMantineColor(severity)}>
@@ -444,7 +450,7 @@ function CompactStatGrid({
{showIcon && (
<Icon
className={classes.statIcon}
size="var(--mantine-font-size-lg)"
style={iconSizes.lg}
stroke={1.5}
color={severityToIconColor(severity)}
/>
@@ -19,6 +19,7 @@ import { clientApi } from "@homarr/api/client";
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
import { useRegisterSpotlightContextActions } from "@homarr/spotlight";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import type { WidgetComponentProps } from "../../definition";
import { isSmartHomeTiny } from "../layout";
@@ -126,7 +127,7 @@ export default function SmartHomeTriggerAutomationWidget({
<Center w="100%" h="100%">
<Stack align="center" gap={isTiny ? 6 : "md"} p="xs" maw="100%">
<ThemeIcon variant="light" size={isTiny ? "md" : "xl"} radius="xl">
<IconAutomation size={isTiny ? 14 : 24} />
<IconAutomation style={zoomCompensatedSize(isTiny ? 14 : 24)} />
</ThemeIcon>
<Text ta="center" fw={600} fz={isTiny ? "xs" : "sm"} lineClamp={2} maw="100%">
{options.displayName}
@@ -4,6 +4,7 @@ import { Card, Flex, Text, Tooltip, VisuallyHidden } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { useRequiredBoard } from "@homarr/boards/context";
import { zoomCompensatedSize } from "@homarr/ui";
import type { TablerIcon } from "@homarr/ui";
export interface SpeedStatCardProps {
@@ -45,7 +46,7 @@ export function SpeedStatCard({ icon: Icon, color, value, label, compact = false
gap={isWide ? 8 : 4}
style={{ minWidth: 0 }}
>
<Icon size={compact ? 16 : 20} color={accentColor} style={{ flexShrink: 0 }} />
<Icon color={accentColor} style={zoomCompensatedSize(compact ? 16 : 20)} />
<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}
@@ -7,6 +7,7 @@ import { useElementSize, useHover, useMergedRef } from "@mantine/hooks";
import type { TooltipProps, YAxisProps } from "recharts";
import { useRequiredBoard } from "@homarr/boards/context";
import { zoomCompensatedSize } from "@homarr/ui";
import type { TablerIcon } from "@homarr/ui";
import type { LabelDisplayModeOption } from "..";
@@ -69,7 +70,7 @@ export const CommonChart = ({
style={{ zIndex: 2, pointerEvents: "none" }}
align="center"
>
{showIcon && <Icon color={"var(--mantine-color-dimmed)"} size={height > 100 ? 20 : 14} stroke={1.5} />}
{showIcon && <Icon color={"var(--mantine-color-dimmed)"} style={zoomCompensatedSize(height > 100 ? 20 : 14)} stroke={1.5} />}
{showText && (
<Text c={"dimmed"} size={height > 100 ? "md" : "xs"} fw={"bold"}>
{title}
@@ -92,7 +93,7 @@ export const CommonChart = ({
{data.length <= 1 ? (
<Center pos="absolute" w="100%" h="100%">
<Stack px={"xs"} align={"center"} gap={4}>
{showIcon && <Icon color={"var(--mantine-color-dimmed)"} size={height > 100 ? 20 : 14} stroke={1.5} />}
{showIcon && <Icon color={"var(--mantine-color-dimmed)"} style={zoomCompensatedSize(height > 100 ? 20 : 14)} stroke={1.5} />}
{showText && (
<Text c={"dimmed"} size={height > 100 ? "md" : "xs"} fw={"bold"} ta="center">
{title}
+4 -3
View File
@@ -21,6 +21,7 @@ import { IconBell, IconPlayerPause, IconPlayerPlay, IconRefresh, IconSettings, I
import { useTiks } from "@rexa-developer/tiks/react";
import { useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import {
claimBrowserAlertOccurrence,
@@ -427,7 +428,7 @@ const AdvancedTimer = ({
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<Paper withBorder p="md">
<Group gap="xs" mb="sm">
<IconSettings size={18} aria-hidden />
<IconSettings style={iconSizes.lg} aria-hidden />
<Text fw={600}>{t("advanced.settings")}</Text>
</Group>
<SettingsSummary mode={runtime.mode} durations={durations} />
@@ -438,7 +439,7 @@ const AdvancedTimer = ({
<Paper withBorder p="md">
<Group gap="xs" mb="sm">
<IconBell size={18} aria-hidden />
<IconBell style={iconSizes.lg} aria-hidden />
<Text fw={600}>{t("advanced.localAlerts")}</Text>
</Group>
<Stack gap="sm">
@@ -453,7 +454,7 @@ const AdvancedTimer = ({
disabled={disabled}
label={t("advanced.sound")}
onChange={(event) => onSoundChange(event.currentTarget.checked)}
thumbIcon={<IconVolume size={12} />}
thumbIcon={<IconVolume style={iconSizes.sm} />}
/>
<Text size="xs" c="dimmed">
{t("advanced.localAlertsHint")}
+3 -2
View File
@@ -14,6 +14,7 @@ import {
import { clientApi } from "@homarr/api/client";
import type { TraefikDashboardData, TraefikProtocolSummary, TraefikResourceSummary } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
@@ -176,12 +177,12 @@ function TraefikWidgetContent({
<div className={classes.hero}>
<SummaryMetric
icon={<IconRoute size={getHeroIconSize(width)} />}
icon={<IconRoute style={zoomCompensatedSize(getHeroIconSize(width))} />}
label={t("summary.routers")}
value={totalRouters}
/>
<SummaryMetric
icon={<IconDoorEnter size={getHeroIconSize(width)} />}
icon={<IconDoorEnter style={zoomCompensatedSize(getHeroIconSize(width))} />}
label={t("summary.entryPoints")}
value={dedupe(combined.entryPoints).length}
/>
@@ -7,6 +7,7 @@ import { IconArrowDown, IconArrowUp, IconClockPause, IconServer } from "@tabler/
import { clientApi } from "@homarr/api/client";
import { formatNumber } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
@@ -215,7 +216,7 @@ function UptimeKumaContent({ integrationIds, options, width, height, displayMode
<div key={statKey} className={classes.statTile}>
<Icon
className={classes.statIcon}
size={iconSize}
style={zoomCompensatedSize(iconSize)}
stroke={1.5}
color={`var(--mantine-color-${color}-6)`}
/>
+14 -9
View File
@@ -16,6 +16,7 @@ import {
import { IconDroplets, IconMapPin, IconSunHigh, IconSunrise, IconSunset, IconWind } from "@tabler/icons-react";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { iconSizes, zoomCompensatedSize } from "@homarr/ui";
import type { WidgetProps } from "../definition";
import { AnimatedWeatherIcon } from "./animated-icon";
@@ -206,7 +207,7 @@ export const AdvancedWeather = ({ height, options, weather, width }: AdvancedWea
<SimpleGrid cols={layout.metricColumns} spacing="xs">
<MetricCard
icon={<IconDroplets size={18} aria-hidden />}
icon={<IconDroplets style={iconSizes.lg} aria-hidden />}
label={t("advanced.nextHourPrecipitation")}
value={t("advanced.precipitationValue", {
amount: nextHour?.precipitation?.toFixed(1) ?? "?",
@@ -215,13 +216,13 @@ export const AdvancedWeather = ({ height, options, weather, width }: AdvancedWea
/>
{options.showHumidity && (
<MetricCard
icon={<IconDroplets size={18} aria-hidden />}
icon={<IconDroplets style={iconSizes.lg} aria-hidden />}
label={t("advanced.currentHumidity")}
value={`${weather.current.relativeHumidity}%`}
/>
)}
<MetricCard
icon={<IconWind size={18} aria-hidden />}
icon={<IconWind style={iconSizes.lg} aria-hidden />}
label={t("advanced.currentWind")}
value={t("advanced.windValue", {
gusts: getPreferredWindSpeed(weather.current.windGusts, options.useImperialSpeed),
@@ -230,17 +231,17 @@ export const AdvancedWeather = ({ height, options, weather, width }: AdvancedWea
})}
/>
<MetricCard
icon={<IconSunHigh size={18} aria-hidden />}
icon={<IconSunHigh style={iconSizes.lg} aria-hidden />}
label={t("advanced.todayUv")}
value={today?.uvIndex?.toFixed(1) ?? "?"}
/>
<MetricCard
icon={<IconSunrise size={18} aria-hidden />}
icon={<IconSunrise style={iconSizes.lg} aria-hidden />}
label={t("dailyForecast.sunrise")}
value={getPreferredTime(today?.sunriseAt, locale, weather.timezone)}
/>
<MetricCard
icon={<IconSunset size={18} aria-hidden />}
icon={<IconSunset style={iconSizes.lg} aria-hidden />}
label={t("dailyForecast.sunset")}
value={getPreferredTime(today?.sunsetAt, locale, weather.timezone)}
/>
@@ -322,7 +323,11 @@ export const AdvancedWeather = ({ height, options, weather, width }: AdvancedWea
{getPreferredDate(day.date, locale, { month: "short", day: "numeric" })}
</Text>
</Stack>
<AnimatedWeatherIcon animated={options.animateIcons} code={day.weatherCode} size={28} />
<AnimatedWeatherIcon
animated={options.animateIcons}
code={day.weatherCode}
style={zoomCompensatedSize(28)}
/>
</Group>
<Text size="sm" fw={500} lineClamp={1}>
@@ -348,13 +353,13 @@ export const AdvancedWeather = ({ height, options, weather, width }: AdvancedWea
<Group className={classes.dailyCardFacts} justify="space-between" gap={4} wrap="nowrap">
<Group gap={3} wrap="nowrap">
<IconDroplets size={14} aria-hidden />
<IconDroplets style={iconSizes.sm} aria-hidden />
<Text className={classes.dailyCardFact} fz={11}>
{day.precipitationProbability ?? "?"}%
</Text>
</Group>
<Group gap={3} wrap="nowrap">
<IconWind size={14} aria-hidden />
<IconWind style={iconSizes.sm} aria-hidden />
<Text className={classes.dailyCardFact} fz={11}>
{getPreferredWindSpeed(day.maxWindSpeed, options.useImperialSpeed)} {speedUnit}
</Text>
@@ -1,3 +1,5 @@
import type { CSSProperties } from "react";
import "../widgets-common.css";
import { WeatherIcon } from "./icon";
@@ -7,6 +9,7 @@ interface AnimatedWeatherIconProps {
code: number;
isDay?: boolean;
size?: string | number;
style?: CSSProperties;
}
const getAnimationClass = (code: number): string => {
@@ -19,12 +22,12 @@ const getAnimationClass = (code: number): string => {
return "";
};
export const AnimatedWeatherIcon = ({ animated = false, code, isDay, size = 26 }: AnimatedWeatherIconProps) => {
export const AnimatedWeatherIcon = ({ animated = false, code, isDay, size = 26, style }: AnimatedWeatherIconProps) => {
let animationClass = "";
if (animated && (code !== 0 || isDay !== false)) animationClass = getAnimationClass(code);
return (
<span className={`weather-anim-wrapper ${animationClass}`}>
<WeatherIcon code={code} isDay={isDay} size={size} />
<WeatherIcon code={code} isDay={isDay} size={size} style={style} />
</span>
);
};
+7 -2
View File
@@ -3,6 +3,7 @@ import { Group, Popover, Stack, Text, UnstyledButton } from "@mantine/core";
import { IconArrowDownRight, IconArrowUpRight, IconDroplets, IconMapPin, IconWind } from "@tabler/icons-react";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import type { WidgetProps } from "../definition";
import { AnimatedWeatherIcon } from "./animated-icon";
@@ -55,7 +56,7 @@ export const CompactWeather = ({ height, isEditMode, options, weather, width }:
animated={options.animateIcons}
code={weather.current.weatherCode}
isDay={weather.current.isDay}
size={layout.tier === "micro" ? 18 : 32}
style={zoomCompensatedSize(layout.tier === "micro" ? 18 : 32)}
/>
</UnstyledButton>
</Popover.Target>
@@ -132,7 +133,11 @@ export const CompactWeather = ({ height, isEditMode, options, weather, width }:
<Text component="span" size="sm" fw={600} tt="capitalize">
{getPreferredDate(day.date, locale, { weekday: "short" })}
</Text>
<AnimatedWeatherIcon animated={options.animateIcons} code={day.weatherCode} size={18} />
<AnimatedWeatherIcon
animated={options.animateIcons}
code={day.weatherCode}
style={zoomCompensatedSize(18)}
/>
<Text component="span" size="sm">
{getPreferredUnit(
day.maxTemperature,
+10 -9
View File
@@ -11,6 +11,7 @@ import {
} from "@tabler/icons-react";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { iconSizes, zoomCompensatedSize } from "@homarr/ui";
import type { WidgetProps } from "../definition";
import { AnimatedWeatherIcon } from "./animated-icon";
@@ -88,7 +89,7 @@ export const DailyWeatherDetails = ({
<Group justify="space-between" align="center" gap="md" wrap="nowrap">
<Group gap="sm" wrap="nowrap" miw={0}>
<ThemeIcon variant="light" size={46} radius="xl">
<AnimatedWeatherIcon animated={animateIcons} code={day.weatherCode} size={28} />
<AnimatedWeatherIcon animated={animateIcons} code={day.weatherCode} style={zoomCompensatedSize(28)} />
</ThemeIcon>
<Stack gap={0} miw={0}>
<Text fw={700} truncate>
@@ -109,29 +110,29 @@ export const DailyWeatherDetails = ({
<SimpleGrid cols={2} spacing={6}>
<DetailMetric
icon={<IconTemperature size={16} aria-hidden />}
icon={<IconTemperature style={iconSizes.md} aria-hidden />}
label={t("advanced.detailLabel.feelsLike")}
value={`${getPreferredUnit(day.maxApparentTemperature, isFahrenheit, disableTemperatureDecimals)} / ${getPreferredUnit(day.minApparentTemperature, isFahrenheit, disableTemperatureDecimals)}`}
/>
<DetailMetric
icon={<IconDroplets size={16} aria-hidden />}
icon={<IconDroplets style={iconSizes.md} aria-hidden />}
label={t("advanced.detailLabel.precipitation")}
value={`${day.precipitationProbability ?? "?"}% · ${day.precipitation?.toFixed(1) ?? "?"} mm`}
/>
{showHumidity && (
<DetailMetric
icon={<IconGauge size={16} aria-hidden />}
icon={<IconGauge style={iconSizes.md} aria-hidden />}
label={t("advanced.detailLabel.humidity")}
value={`${day.averageHumidity ?? "?"}%`}
/>
)}
<DetailMetric
icon={<IconWind size={16} aria-hidden />}
icon={<IconWind style={iconSizes.md} aria-hidden />}
label={t("advanced.detailLabel.wind")}
value={`${getPreferredWindSpeed(day.maxWindSpeed, useImperialSpeed)} / ${getPreferredWindSpeed(day.maxWindGusts, useImperialSpeed)} ${speedUnit}`}
/>
<DetailMetric
icon={<IconSunHigh size={16} aria-hidden />}
icon={<IconSunHigh style={iconSizes.md} aria-hidden />}
label={t("advanced.detailLabel.uv")}
value={day.uvIndex?.toFixed(1) ?? "?"}
/>
@@ -140,17 +141,17 @@ export const DailyWeatherDetails = ({
<Paper className={classes.solarDetails} p="xs" radius="md">
<SimpleGrid cols={3} spacing="xs">
<SolarMetric
icon={<IconSunrise size={17} aria-hidden />}
icon={<IconSunrise style={iconSizes.md} aria-hidden />}
label={t("dailyForecast.sunrise")}
value={getPreferredTime(day.sunriseAt, locale, timeZone)}
/>
<SolarMetric
icon={<IconSunset size={17} aria-hidden />}
icon={<IconSunset style={iconSizes.md} aria-hidden />}
label={t("dailyForecast.sunset")}
value={getPreferredTime(day.sunsetAt, locale, timeZone)}
/>
<SolarMetric
icon={<IconSunHigh size={17} aria-hidden />}
icon={<IconSunHigh style={iconSizes.md} aria-hidden />}
label={t("advanced.detailLabel.daylight")}
value={getPreferredDaylightDuration(day.daylightDuration)}
/>
+8 -3
View File
@@ -1,3 +1,4 @@
import type { CSSProperties } from "react";
import { List, Stack, Text } from "@mantine/core";
import {
IconCloud,
@@ -25,19 +26,23 @@ interface WeatherIconProps {
code: number;
isDay?: boolean;
size?: string | number;
style?: CSSProperties;
}
/**
* Icon which should be displayed when specific code is defined
* @param code weather code from api
* @param size size of the icon, accepts relative sizes too
* @param style CSS overrides (e.g. from `zoomCompensatedSize`) - prefer this over `size` for a
* board-zoom-safe size, since `size` becomes a raw SVG width/height attribute that `var()` isn't
* guaranteed to resolve inside (see `iconSizes` in @homarr/ui).
* @returns Icon corresponding to the weather code
*/
export const WeatherIcon = ({ code, isDay = true, size = 50 }: WeatherIconProps) => {
if (code === 0 && !isDay) return <IconMoon style={{ float: "left" }} size={size} />;
export const WeatherIcon = ({ code, isDay = true, size = 50, style }: WeatherIconProps) => {
if (code === 0 && !isDay) return <IconMoon style={{ float: "left", ...style }} size={size} />;
const { icon: Icon } = weatherDefinitions.find((definition) => definition.codes.includes(code)) ?? unknownWeather;
return <Icon style={{ float: "left" }} size={size} />;
return <Icon style={{ float: "left", ...style }} size={size} />;
};
export const getWeatherKind = (code: number) =>
+2 -1
View File
@@ -9,6 +9,7 @@ import { useRequiredBoard } from "@homarr/boards/context";
import { getIconUrl } from "@homarr/definitions";
import type { WudContainerUpdate } from "@homarr/integrations";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
@@ -64,7 +65,7 @@ const WudWidgetContent = ({
<Text size={isTiny ? "8px" : "xs"} fw={700}>
{updatePercentage}%
</Text>
<IconBrandDocker size={isTiny ? 8 : 16} />
<IconBrandDocker style={zoomCompensatedSize(isTiny ? 8 : 16)} />
</Center>
}
sections={[{ value: updatePercentage, color: progressColor(updatePercentage) }]}