From 3f02da7b5997a5ac7cece3c60ed20f210059b515 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 7 Oct 2026 10:00:24 +0200 Subject: [PATCH] fix(dns-hole-summary): restore colors and fit compact cards (#7045) Related to #7020 and #7018. Restore DNS summary colors, fit compact cards, and match preview text scaling. --- .../docs/widgets/dns-hole-summary/index.mdx | 4 + .../board/layout/scaled-board-canvas.tsx | 10 +-- packages/boards/package.json | 3 +- packages/boards/src/scale.ts | 6 ++ .../src/dns-hole/summary/component.tsx | 77 +++++++++++++------ .../widgets/src/modals/widget-edit-modal.tsx | 10 ++- 6 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 packages/boards/src/scale.ts 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 = ({ ) : (