mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-30 00:51:14 +03:00
adaptively size bottom bar nav targets to 48px when they fit, else compact
icon size now targets the standardized 48×48px mobile touch target (Material Design 3 / Android 48dp bottom-nav minimum)
This commit is contained in:
parent
8e878cd9c4
commit
cb2568a9c3
@ -82,9 +82,13 @@ import { MdCategory } from "react-icons/md";
|
|||||||
|
|
||||||
type GeneralSettingsProps = {
|
type GeneralSettingsProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
large?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
export default function GeneralSettings({
|
||||||
|
className,
|
||||||
|
large,
|
||||||
|
}: GeneralSettingsProps) {
|
||||||
const { t } = useTranslation(["common", "views/settings"]);
|
const { t } = useTranslation(["common", "views/settings"]);
|
||||||
const { getLocaleDocUrl } = useDocDomain();
|
const { getLocaleDocUrl } = useDocDomain();
|
||||||
const { data: profile } = useSWR("profile");
|
const { data: profile } = useSWR("profile");
|
||||||
@ -225,10 +229,13 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
isDesktop
|
isDesktop
|
||||||
? "cursor-pointer rounded-lg bg-secondary text-secondary-foreground hover:bg-muted"
|
? "cursor-pointer rounded-lg bg-secondary text-secondary-foreground hover:bg-muted"
|
||||||
: "text-secondary-foreground",
|
: "text-secondary-foreground",
|
||||||
|
large && "size-12",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<LuSettings className="size-5 md:m-[6px]" />
|
<LuSettings
|
||||||
|
className={cn("md:m-[6px]", large ? "size-6" : "size-5")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
|
|||||||
@ -4,7 +4,14 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateStats } from "@/types/stats";
|
import { FrigateStats } from "@/types/stats";
|
||||||
import { useEmbeddingsReindexProgress, useFrigateStats } from "@/api/ws";
|
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 useStats from "@/hooks/use-stats";
|
||||||
import GeneralSettings from "../menu/GeneralSettings";
|
import GeneralSettings from "../menu/GeneralSettings";
|
||||||
import useNavigation from "@/hooks/use-navigation";
|
import useNavigation from "@/hooks/use-navigation";
|
||||||
@ -21,8 +28,47 @@ import { useTranslation } from "react-i18next";
|
|||||||
function Bottombar() {
|
function Bottombar() {
|
||||||
const navItems = useNavigation("secondary");
|
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<HTMLDivElement | null>(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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-x-4 bottom-0 flex h-16 flex-row items-center justify-between",
|
"absolute inset-x-4 bottom-0 flex h-16 flex-row items-center justify-between",
|
||||||
isMobile &&
|
isMobile &&
|
||||||
@ -32,18 +78,25 @@ function Bottombar() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<NavItem key={item.id} className="p-2" item={item} Icon={item.icon} />
|
<NavItem
|
||||||
|
key={item.id}
|
||||||
|
large={large}
|
||||||
|
className="p-2"
|
||||||
|
item={item}
|
||||||
|
Icon={item.icon}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<GeneralSettings className="p-2" />
|
<GeneralSettings large={large} className="p-2" />
|
||||||
<StatusAlertNav className="p-2" />
|
<StatusAlertNav large={large} className="p-2" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusAlertNavProps = {
|
type StatusAlertNavProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
large?: boolean;
|
||||||
};
|
};
|
||||||
function StatusAlertNav({ className }: StatusAlertNavProps) {
|
function StatusAlertNav({ className, large }: StatusAlertNavProps) {
|
||||||
const { t } = useTranslation(["views/system"]);
|
const { t } = useTranslation(["views/system"]);
|
||||||
const { data: initialStats } = useSWR<FrigateStats>("stats", {
|
const { data: initialStats } = useSWR<FrigateStats>("stats", {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
@ -105,8 +158,18 @@ function StatusAlertNav({ className }: StatusAlertNavProps) {
|
|||||||
return (
|
return (
|
||||||
<Drawer>
|
<Drawer>
|
||||||
<DrawerTrigger asChild>
|
<DrawerTrigger asChild>
|
||||||
<div className="p-2">
|
<div
|
||||||
<IoIosWarning className="size-5 text-danger md:m-[6px]" />
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center p-2",
|
||||||
|
large && "size-12",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IoIosWarning
|
||||||
|
className={cn(
|
||||||
|
"text-danger md:m-[6px]",
|
||||||
|
large ? "size-6" : "size-5",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
<DrawerContent
|
<DrawerContent
|
||||||
|
|||||||
@ -27,6 +27,7 @@ type NavItemProps = {
|
|||||||
item: NavData;
|
item: NavData;
|
||||||
Icon: IconType;
|
Icon: IconType;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
large?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NavItem({
|
export default function NavItem({
|
||||||
@ -34,6 +35,7 @@ export default function NavItem({
|
|||||||
item,
|
item,
|
||||||
Icon,
|
Icon,
|
||||||
onClick,
|
onClick,
|
||||||
|
large,
|
||||||
}: NavItemProps) {
|
}: NavItemProps) {
|
||||||
const { t } = useTranslation(["common"]);
|
const { t } = useTranslation(["common"]);
|
||||||
if (item.enabled == false) {
|
if (item.enabled == false) {
|
||||||
@ -48,11 +50,12 @@ export default function NavItem({
|
|||||||
cn(
|
cn(
|
||||||
"flex flex-col items-center justify-center rounded-lg p-[6px]",
|
"flex flex-col items-center justify-center rounded-lg p-[6px]",
|
||||||
className,
|
className,
|
||||||
|
large && "size-12",
|
||||||
variants[item.variant ?? "primary"][isActive ? "active" : "inactive"],
|
variants[item.variant ?? "primary"][isActive ? "active" : "inactive"],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon className="size-5" />
|
<Icon className={large ? "size-6" : "size-5"} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user