+
+ {visibleCount == null
+ ? groupButtons()
+ : groupButtons().slice(0, visibleCount)}
+
+ {visibleCount != null && (
setAddGroup(true)}
+ variant="ghost"
+ size="sm"
+ className="shrink-0 px-2 text-secondary-foreground"
+ aria-label={t("group.showAll")}
+ onClick={() => setExpanded(true)}
>
-
+
)}
- {isMobile &&
}
+
+ {/* invisible row used only to measure natural button widths so we
+ can render exactly the buttons that fully fit */}
+
+
+ {groupButtons()}
+
+
+
+
+
+
+ {expanded && (
+
setExpanded(false)}
+ />
+ )}
+
+ {expanded && (
+
+
+ {groupButtons(() => setExpanded(false))}
+ setExpanded(false)}
+ >
+
+
+
+
+ )}
+
-
+ )}
>
);
}
diff --git a/web/src/components/icons/LiveIcons.tsx b/web/src/components/icons/LiveIcons.tsx
index 9a8d58fcf9..b8ff089bba 100644
--- a/web/src/components/icons/LiveIcons.tsx
+++ b/web/src/components/icons/LiveIcons.tsx
@@ -6,9 +6,9 @@ export function LiveGridIcon({ layout }: LiveIconProps) {
return (
-
+
@@ -16,7 +16,7 @@ export function LiveGridIcon({ layout }: LiveIconProps) {
className={`w-full ${layout == "grid" ? "bg-selected" : "bg-muted-foreground"}`}
/>
-
+
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/mobile/MobilePage.tsx b/web/src/components/mobile/MobilePage.tsx
index ee56468cb2..b445a9f036 100644
--- a/web/src/components/mobile/MobilePage.tsx
+++ b/web/src/components/mobile/MobilePage.tsx
@@ -146,9 +146,10 @@ export function MobilePageContent({
(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"],
)
}
>
-
+
);
diff --git a/web/src/components/ui/drawer.tsx b/web/src/components/ui/drawer.tsx
index 220708e7e2..af73e93e9d 100644
--- a/web/src/components/ui/drawer.tsx
+++ b/web/src/components/ui/drawer.tsx
@@ -2,8 +2,6 @@ import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
-import { isPWA } from "@/utils/isPWA";
-import { isIOS } from "react-device-detect";
const Drawer = ({
shouldScaleBackground = true,
@@ -43,10 +41,9 @@ const DrawerContent = React.forwardRef<
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx
index 629bf30b87..e935148151 100644
--- a/web/src/views/live/LiveDashboardView.tsx
+++ b/web/src/views/live/LiveDashboardView.tsx
@@ -404,34 +404,38 @@ export default function LiveDashboardView({
{isMobile && (
-
+
{(!cameraGroup || cameraGroup == "default" || isMobileOnly) && (
setMobileLayout("grid")}
>
-
+
+
+
setMobileLayout("list")}
>
-
+
+
+
)}
@@ -439,18 +443,21 @@ export default function LiveDashboardView({
setIsEditMode((prevIsEditMode) => !prevIsEditMode)
}
>
- {isEditMode ? : }
+ {isEditMode ? (
+
+ ) : (
+
+ )}
)}