diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx
index 128d3c4cc..2a0830a0b 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx
@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { Box, Button, FileButton, Menu, UnstyledButton } from "@mantine/core";
-import { useDisclosure, useMediaQuery } from "@mantine/hooks";
+import { useDisclosure } from "@mantine/hooks";
import { IconPencil, IconPhotoEdit, IconPhotoX } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
@@ -12,6 +12,7 @@ import { useConfirmModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { UserAvatar } from "@homarr/ui";
+import { useIsMobile } from "@homarr/ui/hooks";
interface UserProfileAvatarForm {
user: RouterOutputs["user"]["getById"];
@@ -25,7 +26,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
},
});
const [opened, { toggle }] = useDisclosure(false);
- const isMobile = useMediaQuery("(max-width: 48em)");
+ const isMobile = useIsMobile();
const { openConfirmModal } = useConfirmModal();
const tCommon = useI18n("common");
const tManageAvatar = useI18n("user.action.manageAvatar");
diff --git a/apps/nextjs/src/components/board/items/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx
index c29eb8252..c8821aac4 100644
--- a/apps/nextjs/src/components/board/items/widget-context-menu.tsx
+++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx
@@ -3,7 +3,6 @@
import type { MutableRefObject, ReactNode, RefObject } from "react";
import { useCallback, useMemo, useState } from "react";
import { Drawer, Group, Loader, Menu, Text, Tooltip } from "@mantine/core";
-import { useMediaQuery } from "@mantine/hooks";
import { IconAlertTriangle, IconCircleCheck, IconMaximize, IconRefresh, IconSettings } from "@tabler/icons-react";
import type { QueryClient, QueryKey } from "@tanstack/react-query";
import { useIsFetching, useQueryClient } from "@tanstack/react-query";
@@ -21,7 +20,8 @@ import { getWidgetName } from "@homarr/definitions";
import { translateIfNecessary } from "@homarr/translation";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
-import type { WidgetContextMenuAction, WidgetDefinition, WidgetRuntimeRef } from "@homarr/widgets/definition";
+import { useIsMobile } from "@homarr/ui/hooks";
+import type { WidgetDefinition, WidgetRuntimeRef } from "@homarr/widgets/definition";
import { getWidgetQueryKeys, getWidgetRuntimeQueries, supportsAdvancedFocus } from "@homarr/widgets/definition";
import { reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest";
import { getWidgetOptionTranslationNamespace } from "@homarr/widgets/option-translation";
@@ -60,6 +60,8 @@ export const WidgetContextMenu = ({
const { hasChangeAccess } = useBoardPermissions(board);
const { updateAndPersistBoard } = usePersistBoard(board);
const t = useI18n();
+ const tMenu = useI18n("item.menu.label");
+ const tCommon = useI18n("common.action");
const settings = useSettings();
const { openModal } = useModalAction(LazyWidgetEditModal);
const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations } = useItemActions();
@@ -74,7 +76,6 @@ export const WidgetContextMenu = ({
const { open: openAdvancedFocus } = useAdvancedFocus();
const integrationsWithInteractAccess = useIntegrationsWithInteractAccess();
const [menuOpened, setMenuOpened] = useState(false);
- const isMobile = useMediaQuery("(max-width: 48em)");
const persistBoard = useCallback(
(updater: (previous: typeof board) => typeof board) => {
@@ -234,29 +235,15 @@ export const WidgetContextMenu = ({
});
}, [item.id, openAdvancedFocus, sourceRef]);
+ const handleCloseMenu = useCallback(() => {
+ setMenuOpened(false);
+ }, []);
+
if (!session || !settings.enableRightClickOnWidgets || isEditMode || item.kind === "app") {
return <>{children}>;
}
const visibleWidgetActions = widgetContextActions.filter((action) => !action.hidden);
- const actionContent = (
-
- );
return (
);
};
-interface WidgetContextActionsProps {
- canOpenAdvancedFocus: boolean;
- onOpenAdvancedFocus: () => void;
- toggleOptions: ToggleOption[];
- options: Record;
- onToggle: (key: string) => (checked: boolean) => void;
- itemKind: SectionItem["kind"];
- visibleWidgetActions: WidgetContextMenuAction[];
- isWidgetFetching: boolean;
- onRefetch: () => void;
- queryClient: QueryClient;
- matchesWidgetQuery: (queryKey: QueryKey) => boolean;
- onEdit: () => void;
- canConfigureWidget: boolean;
- isIntegrationDataPending: boolean;
+interface WidgetContextMenuDropdownProps {
+ opened: boolean;
+ onClose: () => void;
+ title: string;
+ children: ReactNode;
}
-const WidgetContextActions = ({
- canOpenAdvancedFocus,
- onOpenAdvancedFocus,
- toggleOptions,
- options,
- onToggle,
- itemKind,
- visibleWidgetActions,
- isWidgetFetching,
- onRefetch,
- queryClient,
- matchesWidgetQuery,
- onEdit,
- canConfigureWidget,
- isIntegrationDataPending,
-}: WidgetContextActionsProps) => {
- const t = useI18n();
- const tMenu = useI18n("item.menu.label");
- const tCommon = useI18n("common.action");
+const WidgetContextMenuDropdown = ({ opened, onClose, title, children }: WidgetContextMenuDropdownProps) => {
+ const isMobile = useIsMobile();
+
+ if (!isMobile) return {children};
return (
- <>
- {canOpenAdvancedFocus && (
- <>
- } onClick={onOpenAdvancedFocus}>
- {t("item.advancedFocus.open")}
-
-
- >
- )}
-
- {toggleOptions.length > 0 && (
- <>
- {tMenu("options")}
- {toggleOptions.map(([key]) => (
-
- {t(`${getWidgetOptionTranslationNamespace(itemKind, key)}.label` as never)}
-
- ))}
- >
- )}
-
- {visibleWidgetActions.length > 0 && (
- <>
- {toggleOptions.length > 0 && }
- {tMenu("actions")}
- {visibleWidgetActions.map((action) => {
- const Icon = action.icon;
- return (
- : undefined}
- onClick={action.onClick}
- disabled={action.disabled}
- color={action.color}
- >
- {translateIfNecessary(t, action.label)}
-
- );
- })}
- >
- )}
-
- {(toggleOptions.length > 0 || visibleWidgetActions.length > 0) && }
- : }
- onClick={onRefetch}
- disabled={isWidgetFetching}
- >
-
- {tCommon("refresh")}
-
-
-
- }
- onClick={onEdit}
- onFocus={preloadWidgetEditModal}
- onPointerEnter={preloadWidgetEditModal}
- disabled={!canConfigureWidget || isIntegrationDataPending}
- >
- {tMenu("settings")}
-
- >
+
+
+ {children}
+
+
);
};
diff --git a/apps/nextjs/src/components/onboarding/manage-tour.tsx b/apps/nextjs/src/components/onboarding/manage-tour.tsx
index ef3b38f7f..d81ccecb1 100644
--- a/apps/nextjs/src/components/onboarding/manage-tour.tsx
+++ b/apps/nextjs/src/components/onboarding/manage-tour.tsx
@@ -4,7 +4,6 @@ import type { PropsWithChildren } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import type { OnboardingTourFocusRevealProps, OnboardingTourStep } from "@gfazioli/mantine-onboarding-tour";
-import { useMediaQuery } from "@mantine/hooks";
import {
IconAffiliateFilled,
IconAppsFilled,
@@ -16,6 +15,7 @@ import {
import { clientApi } from "@homarr/api/client";
import { createDocumentationLink } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
+import { useIsMobile } from "@homarr/ui/hooks";
import { TourTargetsProvider } from "~/components/layout/header/tour-target";
import { TourShell } from "./tour-shell";
@@ -48,7 +48,7 @@ const usersStepFocusRevealProps: OnboardingTourFocusRevealProps = {
export const ManageTourProvider = ({ children, isAdmin }: ManageTourProviderProps) => {
const t = useI18n("onboardingTour.manage");
const { mutate: completeTour } = clientApi.user.completeTour.useMutation();
- const isMobile = useMediaQuery("(max-width: 48em)");
+ const isMobile = useIsMobile();
const pathname = usePathname();
const router = useRouter();
diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts
index f8552e94a..950536175 100644
--- a/packages/ui/src/hooks/index.ts
+++ b/packages/ui/src/hooks/index.ts
@@ -1 +1,2 @@
+export * from "./use-is-mobile";
export * from "./use-translated-mantine-react-table";
diff --git a/packages/ui/src/hooks/use-is-mobile.ts b/packages/ui/src/hooks/use-is-mobile.ts
new file mode 100644
index 000000000..8cf9cec1e
--- /dev/null
+++ b/packages/ui/src/hooks/use-is-mobile.ts
@@ -0,0 +1,9 @@
+"use client";
+
+import { useMantineTheme } from "@mantine/core";
+import { useMediaQuery } from "@mantine/hooks";
+
+export const useIsMobile = () => {
+ const theme = useMantineTheme();
+ return useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
+};