From cb2568a9c339d0d44c72751b6e7a845fbc06da50 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:27:38 -0500 Subject: [PATCH] adaptively size bottom bar nav targets to 48px when they fit, else compact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit icon size now targets the standardized 48×48px mobile touch target (Material Design 3 / Android 48dp bottom-nav minimum) --- web/src/components/menu/GeneralSettings.tsx | 11 ++- web/src/components/navigation/Bottombar.tsx | 77 +++++++++++++++++++-- web/src/components/navigation/NavItem.tsx | 5 +- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index 88d834d4dc..19a0f51022 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -82,9 +82,13 @@ import { MdCategory } from "react-icons/md"; type GeneralSettingsProps = { className?: string; + large?: boolean; }; -export default function GeneralSettings({ className }: GeneralSettingsProps) { +export default function GeneralSettings({ + className, + large, +}: GeneralSettingsProps) { const { t } = useTranslation(["common", "views/settings"]); const { getLocaleDocUrl } = useDocDomain(); const { data: profile } = useSWR("profile"); @@ -225,10 +229,13 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { isDesktop ? "cursor-pointer rounded-lg bg-secondary text-secondary-foreground hover:bg-muted" : "text-secondary-foreground", + large && "size-12", className, )} > - + diff --git a/web/src/components/navigation/Bottombar.tsx b/web/src/components/navigation/Bottombar.tsx index c0169bf3e7..309cf63a86 100644 --- a/web/src/components/navigation/Bottombar.tsx +++ b/web/src/components/navigation/Bottombar.tsx @@ -4,7 +4,14 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import useSWR from "swr"; import { FrigateStats } from "@/types/stats"; import { useEmbeddingsReindexProgress, useFrigateStats } from "@/api/ws"; -import { useContext, useEffect, useMemo } from "react"; +import { + useContext, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; import useStats from "@/hooks/use-stats"; import GeneralSettings from "../menu/GeneralSettings"; import useNavigation from "@/hooks/use-navigation"; @@ -21,8 +28,47 @@ import { useTranslation } from "react-i18next"; function Bottombar() { const navItems = useNavigation("secondary"); + // Render 48px touch targets when they fit with even spacing, otherwise fall + // back to the compact size. Measured against the live bar width and icon + // count (which varies with enabled nav items and the status alert). + const containerRef = useRef(null); + const [large, setLarge] = useState(false); + + useLayoutEffect(() => { + const el = containerRef.current; + if (!el) { + return; + } + + const TARGET = 48; // standard bottom-nav touch target (px) + const MIN_GAP = 8; // minimum spacing between targets (px) + + const compute = () => { + const count = el.children.length; + if (count === 0) { + return; + } + const needed = count * TARGET + Math.max(count - 1, 0) * MIN_GAP; + setLarge(needed <= el.clientWidth); + }; + + compute(); + + const resize = new ResizeObserver(compute); + resize.observe(el); + // recompute when items are added/removed (e.g. the status alert appears) + const mutation = new MutationObserver(compute); + mutation.observe(el, { childList: true }); + + return () => { + resize.disconnect(); + mutation.disconnect(); + }; + }, [navItems]); + return (
{navItems.map((item) => ( - + ))} - - + +
); } type StatusAlertNavProps = { className?: string; + large?: boolean; }; -function StatusAlertNav({ className }: StatusAlertNavProps) { +function StatusAlertNav({ className, large }: StatusAlertNavProps) { const { t } = useTranslation(["views/system"]); const { data: initialStats } = useSWR("stats", { revalidateOnFocus: false, @@ -105,8 +158,18 @@ function StatusAlertNav({ className }: StatusAlertNavProps) { return ( -
- +
+
void; + large?: boolean; }; export default function NavItem({ @@ -34,6 +35,7 @@ export default function NavItem({ item, Icon, onClick, + large, }: NavItemProps) { const { t } = useTranslation(["common"]); if (item.enabled == false) { @@ -48,11 +50,12 @@ export default function NavItem({ cn( "flex flex-col items-center justify-center rounded-lg p-[6px]", className, + large && "size-12", variants[item.variant ?? "primary"][isActive ? "active" : "inactive"], ) } > - + );