From f221a5b4d3bbdcc69ce1b6fe66ec3bb3593677bc Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 28 Aug 2026 08:31:41 +0200 Subject: [PATCH] fix: respect mobile header and widget interactions (#6730) --- .../components/board/items/item-content.tsx | 1 + .../board/items/widget-context-menu.tsx | 6 +- .../header/configurable-header.module.css | 46 +++++------ .../layout/header/configurable-header.tsx | 79 +++++++++---------- apps/nextjs/src/components/layout/shell.tsx | 2 +- .../src/components/user-avatar-menu.tsx | 14 ++++ .../src/board-create/board-create-form.tsx | 3 +- packages/modals/src/modal-form-footer.tsx | 29 +++++-- packages/validation/src/header-preferences.ts | 2 +- .../src/modals/widget-edit-modal.module.css | 1 + .../widgets/src/modals/widget-edit-modal.tsx | 13 +-- 11 files changed, 104 insertions(+), 92 deletions(-) diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index e2ef8c88d..7964923c5 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -361,6 +361,7 @@ const LoadedBoardItemContent = ({ widgetStateRef={widgetStateRef} widgetRuntimeRef={widgetRuntimeRef} sourceRef={sourceRef} + disabled={isAdvanced} > | null>; widgetRuntimeRef: WidgetRuntimeRef; sourceRef: RefObject; + disabled?: boolean; children: ReactNode; } @@ -53,6 +54,7 @@ export const WidgetContextMenu = ({ previewDimensions, widgetRuntimeRef, sourceRef, + disabled = false, children, }: WidgetContextMenuProps) => { const { data: session } = useSession(); @@ -162,6 +164,7 @@ export const WidgetContextMenu = ({ }) ?? []; const openEditModal = useCallback(() => { + setMenuOpened(false); openModal( { kind: item.kind, @@ -230,6 +233,7 @@ export const WidgetContextMenu = ({ const handleOpenAdvancedFocus = useCallback(() => { if (!sourceRef.current) return; + setMenuOpened(false); openAdvancedFocus(item.id, sourceRef.current, { restoreFocusTarget: sourceRef.current.querySelector("[data-advanced-focus-trigger]") ?? sourceRef.current, @@ -256,7 +260,7 @@ export const WidgetContextMenu = ({ opened={menuOpened} onChange={setMenuOpened} > - {children} + {children} {canOpenAdvancedFocus && ( <> diff --git a/apps/nextjs/src/components/layout/header/configurable-header.module.css b/apps/nextjs/src/components/layout/header/configurable-header.module.css index 698943cb6..27e2c8402 100644 --- a/apps/nextjs/src/components/layout/header/configurable-header.module.css +++ b/apps/nextjs/src/components/layout/header/configurable-header.module.css @@ -2,7 +2,7 @@ overflow: hidden; } -.header[data-desktop-visible="false"] { +.header[data-visible="false"] { display: none; } @@ -73,39 +73,38 @@ flex: 0 0 auto; } +.userMenu { + display: flex; + padding-inline: rem(6px); +} + .mobileContent { display: none; + grid-template-columns: minmax(0, 1fr) auto; align-items: center; - justify-content: space-between; gap: var(--mantine-spacing-xs); width: 100%; height: 100%; padding-inline: var(--mantine-spacing-xs); } -.mobileIdentity, -.mobileActions { +.mobileZone { min-width: 0; + overflow-x: auto; + scrollbar-width: none; } -.mobileIdentity { - flex: 1 1 auto; - overflow: hidden; +.mobileZone::-webkit-scrollbar { + display: none; } -.mobileIdentity > :last-child { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; +.mobileZone[data-zone="left"] { + justify-content: flex-start; } -.mobileIdentity h2 { - overflow: hidden; - text-overflow: ellipsis; -} - -.mobileActions { - flex: 0 0 auto; +.mobileZone[data-zone="right"] { + justify-content: flex-end; + max-width: 72vw; } .floatingControls { @@ -155,21 +154,12 @@ } @media (max-width: $mantine-breakpoint-sm) { - .header[data-desktop-visible="false"] { - display: block; - } - .desktopContent { display: none; } .mobileContent { - display: flex; - } - - .floatingControls, - .floatingControlsCorner { - display: none; + display: grid; } } diff --git a/apps/nextjs/src/components/layout/header/configurable-header.tsx b/apps/nextjs/src/components/layout/header/configurable-header.tsx index cdbd10f3f..6584cc17f 100644 --- a/apps/nextjs/src/components/layout/header/configurable-header.tsx +++ b/apps/nextjs/src/components/layout/header/configurable-header.tsx @@ -3,15 +3,7 @@ import type { ReactNode } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import { AppShellHeader, Avatar, Group, Indicator, Loader, Tooltip, useMantineColorScheme } from "@mantine/core"; -import { - IconBrandDocker, - IconHome, - IconLayoutDashboard, - IconReplace, - IconRobot, - IconSettings, - IconSunMoon, -} from "@tabler/icons-react"; +import { IconBrandDocker, IconHome, IconReplace, IconRobot, IconSettings, IconSunMoon } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; @@ -81,7 +73,10 @@ export const ConfigurableHeader = ({ return ( {(boardSwitcher) => { - const renderItems = (items: HeaderItem[]) => + const renderItems = ( + items: HeaderItem[], + display: { search: "input" | "icon"; logo: "logo" | "logoAndText" }, + ) => items.map((item) => ( )); + const desktopDisplay = { + search: headerPreferences.searchDisplay, + logo: headerPreferences.logoDisplay, + }; + const mobileDisplay = { search: "icon", logo: "logo" } as const; + return ( <> @@ -120,37 +121,26 @@ export const ConfigurableHeader = ({
{hasNavigation ? : null} - {renderItems(headerPreferences.zones.left)} + {renderItems(headerPreferences.zones.left, desktopDisplay)}
- {renderItems(headerPreferences.zones.center)} + {renderItems(headerPreferences.zones.center, desktopDisplay)}
- {renderItems(headerPreferences.zones.right)} + {renderItems(headerPreferences.zones.right, desktopDisplay)} {actions ? {actions} : null}
- + {hasNavigation ? : null} - + {renderItems(headerPreferences.zones.left, mobileDisplay)} - - {actions} - {boardEditAction} - {boardSettingsAction} - - - - - - + + {renderItems(headerPreferences.zones.center, mobileDisplay)} + {renderItems(headerPreferences.zones.right, mobileDisplay)} + {actions ? {actions} : null}
@@ -298,7 +288,9 @@ const HeaderItem = ({ return ( - + + ? + ); @@ -416,14 +408,19 @@ const HeaderItem = ({ return ( - +
+ +
); }; -const getBoardInitial = (name: string) => Array.from(name.trim())[0]?.toLocaleUpperCase() ?? "?"; +const getBoardInitial = (name: string) => { + const firstCharacter = Array.from(name.trim())[0] ?? "?"; + return Array.from(firstCharacter.toUpperCase())[0] ?? "?"; +}; diff --git a/apps/nextjs/src/components/layout/shell.tsx b/apps/nextjs/src/components/layout/shell.tsx index 40a3c99f9..558ba0c69 100644 --- a/apps/nextjs/src/components/layout/shell.tsx +++ b/apps/nextjs/src/components/layout/shell.tsx @@ -22,7 +22,7 @@ export const ClientShell = ({ const collapsed = useAtomValue(navigationCollapsedAtom); const backgroundProps = useOptionalBackgroundProps(); const { headerPreferences } = useSettings(); - const headerHeight = headerPreferences.visible ? appShellHeaderHeight : { base: appShellHeaderHeight, sm: 0 }; + const headerHeight = headerPreferences.visible ? appShellHeaderHeight : 0; return ( }> {t("homeBoard")} + {board && boardPermissions?.hasChangeAccess && ( + } + > + {tBoard("action.settings")} + + )} } rightSection={{formatHotkeyLabel(boardSwitcher.hotkey, t("modifier"))}} diff --git a/packages/forms-collection/src/board-create/board-create-form.tsx b/packages/forms-collection/src/board-create/board-create-form.tsx index e96a13597..ccfa17440 100644 --- a/packages/forms-collection/src/board-create/board-create-form.tsx +++ b/packages/forms-collection/src/board-create/board-create-form.tsx @@ -49,8 +49,7 @@ export const BoardCreateForm = ({ onCancel }: BoardCreateFormProps) => { message: tBoard("action.create.notification.success.message", { name: values.name }), }); const name = encodeURIComponent(result.name); - const layoutId = encodeURIComponent(result.layoutId); - router.push(`/boards/${name}?layout=${layoutId}&edit=true&add=true`); + router.push(`/boards/${name}`); }, onError() { showErrorNotification({ diff --git a/packages/modals/src/modal-form-footer.tsx b/packages/modals/src/modal-form-footer.tsx index f274b2dd4..8927ee081 100644 --- a/packages/modals/src/modal-form-footer.tsx +++ b/packages/modals/src/modal-form-footer.tsx @@ -7,6 +7,7 @@ import { useI18n } from "@homarr/translation/client"; interface ModalFormFooterProps { onCancel: () => void; + form?: string; submitLabel?: string; cancelLabel?: string; loading?: boolean; @@ -18,24 +19,38 @@ const ActionButtons = ({ submitLabel, cancelLabel, loading, -}: Pick) => { - const t = useI18n(); + form, +}: Pick) => { + const t = useI18n("common.action"); return ( <> - ); }; -export const ModalFormFooter = ({ onCancel, submitLabel, cancelLabel, loading, leftSection }: ModalFormFooterProps) => { +export const ModalFormFooter = ({ + onCancel, + form, + submitLabel, + cancelLabel, + loading, + leftSection, +}: ModalFormFooterProps) => { const actionButtons = ( - + ); if (leftSection) { diff --git a/packages/validation/src/header-preferences.ts b/packages/validation/src/header-preferences.ts index 5b7abe1b6..1235809b5 100644 --- a/packages/validation/src/header-preferences.ts +++ b/packages/validation/src/header-preferences.ts @@ -16,7 +16,7 @@ export const headerBuiltinItemIds = [ "user", ] as const; -export const requiredHeaderBuiltinItemIds = ["boardEdit", "boardSettings", "user"] as const; +export const requiredHeaderBuiltinItemIds = ["user"] as const; export const headerPreferenceItemIds = headerBuiltinItemIds; export const headerItemIds = headerBuiltinItemIds; diff --git a/packages/widgets/src/modals/widget-edit-modal.module.css b/packages/widgets/src/modals/widget-edit-modal.module.css index 9294af70d..1e3008958 100644 --- a/packages/widgets/src/modals/widget-edit-modal.module.css +++ b/packages/widgets/src/modals/widget-edit-modal.module.css @@ -143,6 +143,7 @@ margin-inline: calc(var(--mantine-spacing-md) * -1); margin-bottom: calc(var(--mantine-spacing-md) * -1); padding: var(--mantine-spacing-sm) var(--mantine-spacing-md); + padding-bottom: max(var(--mantine-spacing-sm), env(safe-area-inset-bottom)); border-top: 1px solid var(--mantine-color-default-border); background: color-mix(in srgb, var(--mantine-color-body) 94%, transparent); backdrop-filter: blur(10px); diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index b493f92ad..3c9d71708 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -36,7 +36,7 @@ import { objectEntries } from "@homarr/common"; import { IntegrationProvider, useSession } from "@homarr/auth/client"; import { useOptionalBoard } from "@homarr/boards/context"; import type { WidgetKind } from "@homarr/definitions"; -import { createModal, modalSizeForm } from "@homarr/modals"; +import { createModal, ModalFormFooter, modalSizeForm } from "@homarr/modals"; import type { SettingsContextProps } from "@homarr/settings/creator"; import { SpotlightProvider } from "@homarr/spotlight"; import { useI18n } from "@homarr/translation/client"; @@ -674,16 +674,7 @@ export const WidgetEditModal = createModal>(({ const footer = ( - - - - - - + );