diff --git a/apps/docs/docs/widgets/dns-hole-summary/index.mdx b/apps/docs/docs/widgets/dns-hole-summary/index.mdx
index 214aff1fe..6e282ea93 100644
--- a/apps/docs/docs/widgets/dns-hole-summary/index.mdx
+++ b/apps/docs/docs/widgets/dns-hole-summary/index.mdx
@@ -57,4 +57,8 @@ import { technitiumDnsIntegration } from "@site/docs/integrations/technitium-dns
### Configuration
+Enable **Use Pi-Hole colors** to color the four statistics red, yellow, cyan, and green with any supported DNS integration. Disable it to use the board's primary color. Both modes respect the board opacity.
+
+Compact cards, including 1×1 widgets on scaled boards, show abbreviated values without icons or labels. Hover or focus a value to see its metric label; larger cards keep their icons and labels.
+
diff --git a/apps/nextjs/src/components/board/layout/scaled-board-canvas.tsx b/apps/nextjs/src/components/board/layout/scaled-board-canvas.tsx
index eec308414..02c8eacd8 100644
--- a/apps/nextjs/src/components/board/layout/scaled-board-canvas.tsx
+++ b/apps/nextjs/src/components/board/layout/scaled-board-canvas.tsx
@@ -5,8 +5,12 @@ import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { Box } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
+import { calculateBoardUiScale } from "@homarr/boards/scale";
+
import classes from "./scaled-board-canvas.module.css";
+export { calculateBoardUiScale } from "@homarr/boards/scale";
+
const BoardCanvasScaleContext = createContext(1);
export const useBoardCanvasScale = () => useContext(BoardCanvasScaleContext);
@@ -18,12 +22,6 @@ export const calculateBoardCanvasScale = (availableWidth: number, logicalWidth:
return availableWidth / logicalWidth;
};
-export const calculateBoardUiScale = (canvasScale: number) => {
- if (!Number.isFinite(canvasScale) || canvasScale <= 0) return 1;
-
- return canvasScale < 1 ? 1 / canvasScale : 1;
-};
-
interface ScaledBoardCanvasProps {
logicalWidth: number;
initialLogicalHeight: number;
diff --git a/packages/boards/package.json b/packages/boards/package.json
index a63b3cb49..b3bfbdfed 100644
--- a/packages/boards/package.json
+++ b/packages/boards/package.json
@@ -16,7 +16,8 @@
"./layout-selection": "./src/layout-selection.ts",
"./layout-preview": "./src/layout-preview.ts",
"./updater": "./src/updater.ts",
- "./edit-mode": "./src/edit-mode.tsx"
+ "./edit-mode": "./src/edit-mode.tsx",
+ "./scale": "./src/scale.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
diff --git a/packages/boards/src/scale.ts b/packages/boards/src/scale.ts
new file mode 100644
index 000000000..95ba4d0eb
--- /dev/null
+++ b/packages/boards/src/scale.ts
@@ -0,0 +1,6 @@
+export const calculateBoardUiScale = (canvasScale: number) => {
+ if (!Number.isFinite(canvasScale) || canvasScale <= 0) return 1;
+ if (canvasScale < 1) return 1 / canvasScale;
+
+ return 1;
+};
diff --git a/packages/widgets/src/dns-hole/summary/component.tsx b/packages/widgets/src/dns-hole/summary/component.tsx
index a51a725f0..a682ddd7d 100644
--- a/packages/widgets/src/dns-hole/summary/component.tsx
+++ b/packages/widgets/src/dns-hole/summary/component.tsx
@@ -40,6 +40,7 @@ export default function DnsHoleSummaryWidget({
integrationIds,
width,
height,
+ displayScale = 1,
}: WidgetComponentProps) {
const summaryQuery = clientApi.widget.dnsHole.summary.useQuery({
integrationIds,
@@ -67,7 +68,14 @@ export default function DnsHoleSummaryWidget({
{data.length > 0 ? (
stats.map((item) => (
-
+
))
) : (
t("widget.dnsHoleSummary.data.adsBlockedToday"),
- color: "var(--mantine-color-red-light)",
+ color: "var(--mantine-color-red-6)",
},
{
icon: IconPercentage,
@@ -132,7 +140,7 @@ const stats = [
return `${formatNumber(totalCount === 0 ? 0 : (blocked / totalCount) * 100, size === "sm" ? 0 : 2)}%`;
},
label: (t) => t("widget.dnsHoleSummary.data.adsBlockedTodayPercentage"),
- color: "var(--mantine-color-yellow-light)",
+ color: "var(--mantine-color-yellow-6)",
},
{
icon: IconSearch,
@@ -142,7 +150,7 @@ const stats = [
size === "sm" ? 0 : 2,
),
label: (t) => t("widget.dnsHoleSummary.data.dnsQueriesToday"),
- color: "var(--mantine-color-cyan-light)",
+ color: "var(--mantine-color-cyan-6)",
},
{
icon: IconWorldWww,
@@ -158,7 +166,7 @@ const stats = [
},
tooltip: (data, t) => (data.length >= 2 ? t("widget.dnsHoleSummary.domainsTooltip") : undefined),
label: (t) => t("widget.dnsHoleSummary.data.domainsBeingBlocked"),
- color: "var(--mantine-color-green-light)",
+ color: "var(--mantine-color-green-6)",
},
] satisfies StatItem[];
@@ -175,19 +183,36 @@ interface StatCardProps {
data: DnsHoleSummary[];
usePiHoleColors: boolean;
t: TranslationFunction;
+ displayScale: number;
}
-const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
+const StatCard = ({ item, data, usePiHoleColors, t, displayScale }: StatCardProps) => {
const { ref, height, width } = useElementSize();
- const isLong = width > height + 20;
- const canStackText = height > 32;
- const hideLabel = (height <= 32 && width <= 256) || (height <= 64 && width <= 92);
- const tooltip = item.tooltip?.(data, t);
+ let layoutScale = displayScale;
+ if (!Number.isFinite(layoutScale) || layoutScale <= 0) layoutScale = 1;
+
+ // Element measurements are logical pixels; density follows the displayed card.
+ const displayedWidth = width * layoutScale;
+ const displayedHeight = height * layoutScale;
+ const isLong = displayedWidth > displayedHeight + 20;
+ const canStackText = displayedHeight > 32;
+ const isCompact = (displayedHeight <= 32 && displayedWidth <= 256) || (displayedHeight <= 64 && displayedWidth <= 92);
const board = useRequiredBoard();
const label = translateIfNecessary(t, item.label);
- const value = item.value(data, width <= 64 ? "sm" : "md");
+ const value = item.value(data, isCompact || displayedWidth <= 64 ? "sm" : "md");
+ let tooltip = item.tooltip?.(data, t);
+ let padding: number | "sm" = "sm";
+ let valueSize = "lg";
+ if (isCompact) {
+ padding = 2;
+ // Reserve padding and borders so four-character values fit narrow mobile cards.
+ valueSize = `calc(${Math.min(12, Math.max(0, displayedWidth - 6) / 3)}px * var(--board-canvas-ui-scale, 1))`;
+ let compactTooltip = label;
+ if (tooltip) compactTooltip += `. ${tooltip}`;
+ tooltip = compactTooltip;
+ }
const backgroundColor = usePiHoleColors
- ? `rgb(from ${item.color} r g b / calc(var(--opacity, 1) * 0.4))`
- : "rgb(from var(--mantine-color-primaryColor-filled) r g b / calc(var(--opacity, 1) * 0.12))";
+ ? `color-mix(in srgb, ${item.color} calc(var(--opacity, 1) * 40%), transparent)`
+ : "color-mix(in srgb, var(--mantine-color-primaryColor-filled) calc(var(--opacity, 1) * 12%), transparent)";
return (
@@ -197,13 +222,13 @@ const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
tabIndex={tooltip ? 0 : undefined}
aria-label={`${label}: ${value}`}
className="summary-card"
- p="sm"
+ p={padding}
radius={board.itemRadius}
bg={backgroundColor}
style={{
flex: 1,
border:
- "1px solid rgb(from var(--mantine-color-secondaryColor-filled) r g b / calc(var(--opacity, 1) * 0.45))",
+ "1px solid color-mix(in srgb, var(--mantine-color-secondaryColor-filled) calc(var(--opacity, 1) * 45%), transparent)",
}}
>
{
direction={isLong ? "row" : "column"}
gap={0}
>
-
+ {!isCompact && (
+
+ )}
{
gap={isLong ? 4 : 0}
wrap="wrap"
>
-
+
{value}
- {!hideLabel && (
+ {!isCompact && (
{label}
diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx
index 55529d51d..c7a93918a 100644
--- a/packages/widgets/src/modals/widget-edit-modal.tsx
+++ b/packages/widgets/src/modals/widget-edit-modal.tsx
@@ -35,6 +35,7 @@ import { z } from "zod/v4";
import { objectEntries } from "@homarr/common";
import { IntegrationProvider, useSession } from "@homarr/auth/client";
import { useOptionalBoard } from "@homarr/boards/context";
+import { calculateBoardUiScale } from "@homarr/boards/scale";
import type { WidgetKind } from "@homarr/definitions";
import { createModal, ModalFormFooter, modalSizeForm } from "@homarr/modals";
import type { SettingsContextProps } from "@homarr/settings/creator";
@@ -316,7 +317,12 @@ const WidgetEditPreview = ({
) : (