Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e420bc087 | ||
|
|
322d4a7fc4 | ||
|
|
63e1bc7f6c |
No files matched your search
@@ -111,7 +111,7 @@ export const createBoardLayout = <TParams extends Params>({
|
|||||||
<ClientShell hasNavigation={false}>
|
<ClientShell hasNavigation={false}>
|
||||||
<MainHeader
|
<MainHeader
|
||||||
logo={<BoardLogo size={appShellLogoHeight} />}
|
logo={<BoardLogo size={appShellLogoHeight} />}
|
||||||
logoWithTitle={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
logoWithTitle={<BoardLogoWithTitle size="md" />}
|
||||||
actions={headerActions}
|
actions={headerActions}
|
||||||
boardEditAction={headerBoardEditAction}
|
boardEditAction={headerBoardEditAction}
|
||||||
boardSettingsAction={headerBoardSettingsAction}
|
boardSettingsAction={headerBoardSettingsAction}
|
||||||
|
|||||||
+3
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Box, Button, FileButton, Menu, UnstyledButton } from "@mantine/core";
|
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 { IconPencil, IconPhotoEdit, IconPhotoX } from "@tabler/icons-react";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
@@ -12,6 +12,7 @@ import { useConfirmModal } from "@homarr/modals";
|
|||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { UserAvatar } from "@homarr/ui";
|
import { UserAvatar } from "@homarr/ui";
|
||||||
|
import { useIsMobile } from "@homarr/ui/hooks";
|
||||||
|
|
||||||
interface UserProfileAvatarForm {
|
interface UserProfileAvatarForm {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
@@ -25,7 +26,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [opened, { toggle }] = useDisclosure(false);
|
const [opened, { toggle }] = useDisclosure(false);
|
||||||
const isMobile = useMediaQuery("(max-width: 48em)");
|
const isMobile = useIsMobile();
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const tCommon = useI18n("common");
|
const tCommon = useI18n("common");
|
||||||
const tManageAvatar = useI18n("user.action.manageAvatar");
|
const tManageAvatar = useI18n("user.action.manageAvatar");
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.searchInput {
|
.searchInput {
|
||||||
border-color: color-mix(in srgb, var(--mantine-color-default-border) 72%, transparent);
|
border-color: color-mix(in srgb, var(--mantine-color-default-border) 72%, transparent);
|
||||||
background: color-mix(in srgb, var(--mantine-color-body) 54%, transparent);
|
background: color-mix(in srgb, var(--mantine-color-body) 54%, transparent);
|
||||||
@@ -44,3 +48,19 @@
|
|||||||
border-color: var(--mantine-primary-color-filled);
|
border-color: var(--mantine-primary-color-filled);
|
||||||
box-shadow: var(--mantine-shadow-md);
|
box-shadow: var(--mantine-shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mantine-breakpoint-sm) {
|
||||||
|
.modalContent {
|
||||||
|
height: calc(100dvh - var(--modal-y-offset) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBody,
|
||||||
|
.content {
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -228,7 +228,13 @@ export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
|
|||||||
radius="xl"
|
radius="xl"
|
||||||
classNames={{ input: classes.searchInput }}
|
classNames={{ input: classes.searchInput }}
|
||||||
/>
|
/>
|
||||||
<ScrollArea.Autosize mah="min(80vh, 42rem)" type="never">
|
<ScrollArea.Autosize
|
||||||
|
mah="min(80vh, 42rem)"
|
||||||
|
type="never"
|
||||||
|
scrollbars="y"
|
||||||
|
overscrollBehavior="contain"
|
||||||
|
className={classes.results}
|
||||||
|
>
|
||||||
{results}
|
{results}
|
||||||
</ScrollArea.Autosize>
|
</ScrollArea.Autosize>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.mobileDrawerContent {
|
||||||
|
height: min(80dvh, rem(640px));
|
||||||
|
max-height: 80dvh;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileDrawerBody {
|
||||||
|
padding-bottom: max(var(--mantine-spacing-md), env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileActionList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { MutableRefObject, ReactNode, RefObject } from "react";
|
import type { MutableRefObject, ReactNode, RefObject } from "react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Group, Loader, Menu, Text, Tooltip } from "@mantine/core";
|
import { Drawer, Group, Loader, Menu, Text, Tooltip } from "@mantine/core";
|
||||||
import { IconAlertTriangle, IconCircleCheck, IconMaximize, IconRefresh, IconSettings } from "@tabler/icons-react";
|
import { IconAlertTriangle, IconCircleCheck, IconMaximize, IconRefresh, IconSettings } from "@tabler/icons-react";
|
||||||
import type { QueryClient, QueryKey } from "@tanstack/react-query";
|
import type { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||||
import { useIsFetching, useQueryClient } from "@tanstack/react-query";
|
import { useIsFetching, useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -20,6 +20,7 @@ import { getWidgetName } from "@homarr/definitions";
|
|||||||
import { translateIfNecessary } from "@homarr/translation";
|
import { translateIfNecessary } from "@homarr/translation";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { useIsMobile } from "@homarr/ui/hooks";
|
||||||
import type { WidgetDefinition, WidgetRuntimeRef } from "@homarr/widgets/definition";
|
import type { WidgetDefinition, WidgetRuntimeRef } from "@homarr/widgets/definition";
|
||||||
import { getWidgetQueryKeys, getWidgetRuntimeQueries, supportsAdvancedFocus } from "@homarr/widgets/definition";
|
import { getWidgetQueryKeys, getWidgetRuntimeQueries, supportsAdvancedFocus } from "@homarr/widgets/definition";
|
||||||
import { reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest";
|
import { reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest";
|
||||||
@@ -31,6 +32,9 @@ import { useBoardPermissions } from "../permissions/client";
|
|||||||
import { useItemActions } from "./item-actions";
|
import { useItemActions } from "./item-actions";
|
||||||
import { LazyWidgetEditModal, preloadWidgetEditModal } from "./lazy-widget-edit-modal";
|
import { LazyWidgetEditModal, preloadWidgetEditModal } from "./lazy-widget-edit-modal";
|
||||||
import { matchesWidgetItemQuery } from "./widget-query-scope";
|
import { matchesWidgetItemQuery } from "./widget-query-scope";
|
||||||
|
import classes from "./widget-context-menu.module.css";
|
||||||
|
|
||||||
|
type ToggleOption = [key: string, option: { type: string; skipContextMenu?: boolean }];
|
||||||
|
|
||||||
interface WidgetContextMenuProps {
|
interface WidgetContextMenuProps {
|
||||||
item: SectionItem;
|
item: SectionItem;
|
||||||
@@ -134,11 +138,12 @@ export const WidgetContextMenu = ({
|
|||||||
[item.id, persistBoard],
|
[item.id, persistBoard],
|
||||||
);
|
);
|
||||||
|
|
||||||
type OptionDef = { type: string; skipContextMenu?: boolean };
|
|
||||||
const toggleOptions = useMemo(() => {
|
const toggleOptions = useMemo(() => {
|
||||||
if (!canConfigureWidget) return [];
|
if (!canConfigureWidget) return [];
|
||||||
const rawOptions = definition.createOptions(settings) as unknown as Record<string, OptionDef>;
|
const rawOptions = definition.createOptions(settings) as unknown as Record<string, ToggleOption[1]>;
|
||||||
return Object.entries(rawOptions).filter(([, option]) => option.type === "switch" && !option.skipContextMenu);
|
return Object.entries(rawOptions).filter(
|
||||||
|
([, option]) => option.type === "switch" && !option.skipContextMenu,
|
||||||
|
) as ToggleOption[];
|
||||||
}, [canConfigureWidget, definition, settings]);
|
}, [canConfigureWidget, definition, settings]);
|
||||||
|
|
||||||
const widgetContextActions =
|
const widgetContextActions =
|
||||||
@@ -222,6 +227,18 @@ export const WidgetContextMenu = ({
|
|||||||
[setItemOptions],
|
[setItemOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleOpenAdvancedFocus = useCallback(() => {
|
||||||
|
if (!sourceRef.current) return;
|
||||||
|
openAdvancedFocus(item.id, sourceRef.current, {
|
||||||
|
restoreFocusTarget:
|
||||||
|
sourceRef.current.querySelector<HTMLElement>("[data-advanced-focus-trigger]") ?? sourceRef.current,
|
||||||
|
});
|
||||||
|
}, [item.id, openAdvancedFocus, sourceRef]);
|
||||||
|
|
||||||
|
const handleCloseMenu = useCallback(() => {
|
||||||
|
setMenuOpened(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!session || !settings.enableRightClickOnWidgets || isEditMode || item.kind === "app") {
|
if (!session || !settings.enableRightClickOnWidgets || isEditMode || item.kind === "app") {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
@@ -239,21 +256,10 @@ export const WidgetContextMenu = ({
|
|||||||
onChange={setMenuOpened}
|
onChange={setMenuOpened}
|
||||||
>
|
>
|
||||||
<Menu.ContextMenu>{children}</Menu.ContextMenu>
|
<Menu.ContextMenu>{children}</Menu.ContextMenu>
|
||||||
<Menu.Dropdown>
|
<WidgetContextMenuDropdown opened={menuOpened} onClose={handleCloseMenu} title={getWidgetName(item.kind, t)}>
|
||||||
{canOpenAdvancedFocus && (
|
{canOpenAdvancedFocus && (
|
||||||
<>
|
<>
|
||||||
<Menu.Item
|
<Menu.Item closeMenuOnClick leftSection={<IconMaximize size={16} />} onClick={handleOpenAdvancedFocus}>
|
||||||
closeMenuOnClick
|
|
||||||
leftSection={<IconMaximize size={16} />}
|
|
||||||
onClick={() => {
|
|
||||||
if (sourceRef.current)
|
|
||||||
openAdvancedFocus(item.id, sourceRef.current, {
|
|
||||||
restoreFocusTarget:
|
|
||||||
sourceRef.current.querySelector<HTMLElement>("[data-advanced-focus-trigger]") ??
|
|
||||||
sourceRef.current,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("item.advancedFocus.open")}
|
{t("item.advancedFocus.open")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
@@ -319,11 +325,39 @@ export const WidgetContextMenu = ({
|
|||||||
>
|
>
|
||||||
{tMenu("settings")}
|
{tMenu("settings")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</WidgetContextMenuDropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface WidgetContextMenuDropdownProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WidgetContextMenuDropdown = ({ opened, onClose, title, children }: WidgetContextMenuDropdownProps) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
if (!isMobile) return <Menu.Dropdown>{children}</Menu.Dropdown>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
position="bottom"
|
||||||
|
size="min(80dvh, 40rem)"
|
||||||
|
title={title}
|
||||||
|
classNames={{ content: classes.mobileDrawerContent, body: classes.mobileDrawerBody }}
|
||||||
|
>
|
||||||
|
<div className={classes.mobileActionList} role="menu" aria-orientation="vertical" data-menu-dropdown>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface WidgetQueryStatusProps {
|
interface WidgetQueryStatusProps {
|
||||||
queryClient: QueryClient;
|
queryClient: QueryClient;
|
||||||
matchesQuery: (queryKey: QueryKey) => boolean;
|
matchesQuery: (queryKey: QueryKey) => boolean;
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ export const MainHeader = async ({
|
|||||||
const session = await auth();
|
const session = await auth();
|
||||||
const isAdmin = Boolean(session?.user.permissions.includes("admin"));
|
const isAdmin = Boolean(session?.user.permissions.includes("admin"));
|
||||||
const isDockerEnabled = isAdmin && env.ENABLE_DOCKER;
|
const isDockerEnabled = isAdmin && env.ENABLE_DOCKER;
|
||||||
|
const resolvedLogoWithTitle = logoWithTitle ?? <HomarrLogoWithTitle size="md" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigurableHeader
|
<ConfigurableHeader
|
||||||
logo={logo ?? <HomarrLogo size={appShellLogoHeight} />}
|
logo={logo ?? <HomarrLogo size={appShellLogoHeight} />}
|
||||||
logoWithTitle={logoWithTitle ?? <HomarrLogoWithTitle size="md" />}
|
logoWithTitle={resolvedLogoWithTitle}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
boardEditAction={boardEditAction}
|
boardEditAction={boardEditAction}
|
||||||
boardSettingsAction={boardSettingsAction}
|
boardSettingsAction={boardSettingsAction}
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header[data-desktop-visible="false"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktopContent {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -64,6 +73,41 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobileContent {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--mantine-spacing-xs);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-inline: var(--mantine-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileIdentity,
|
||||||
|
.mobileActions {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileIdentity {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileIdentity > :last-child {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileIdentity h2 {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileActions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.floatingControls {
|
.floatingControls {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: var(--homarr-z-index-board-header);
|
z-index: var(--homarr-z-index-board-header);
|
||||||
@@ -111,18 +155,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $mantine-breakpoint-sm) {
|
@media (max-width: $mantine-breakpoint-sm) {
|
||||||
.content {
|
.header[data-desktop-visible="false"] {
|
||||||
gap: var(--mantine-spacing-xs);
|
display: block;
|
||||||
padding-inline: var(--mantine-spacing-xs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone {
|
.desktopContent {
|
||||||
gap: var(--mantine-spacing-xs);
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.mobileContent {
|
||||||
width: auto;
|
display: flex;
|
||||||
flex: 0 0 auto;
|
}
|
||||||
|
|
||||||
|
.floatingControls,
|
||||||
|
.floatingControlsCorner {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,7 @@
|
|||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import { AppShellHeader, Avatar, Group, Indicator, Loader, Tooltip, useMantineColorScheme } from "@mantine/core";
|
||||||
AppShellHeader,
|
|
||||||
Avatar,
|
|
||||||
Group,
|
|
||||||
Indicator,
|
|
||||||
Loader,
|
|
||||||
Tooltip,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import {
|
import {
|
||||||
IconBrandDocker,
|
IconBrandDocker,
|
||||||
IconHome,
|
IconHome,
|
||||||
@@ -116,14 +108,15 @@ export const ConfigurableHeader = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{headerPreferences.visible ? (
|
<AppShellHeader
|
||||||
<AppShellHeader
|
maw="100vw"
|
||||||
maw="100vw"
|
zIndex="var(--homarr-z-index-board-header)"
|
||||||
zIndex="var(--homarr-z-index-board-header)"
|
className={classes.header}
|
||||||
className={classes.header}
|
data-desktop-visible={headerPreferences.visible}
|
||||||
data-advanced-focus-background
|
data-advanced-focus-background
|
||||||
data-app-shell-header
|
data-app-shell-header
|
||||||
>
|
>
|
||||||
|
<div className={classes.desktopContent}>
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
<div className={classes.zone} data-zone="left">
|
<div className={classes.zone} data-zone="left">
|
||||||
{hasNavigation ? <ClientBurger /> : null}
|
{hasNavigation ? <ClientBurger /> : null}
|
||||||
@@ -137,8 +130,31 @@ export const ConfigurableHeader = ({
|
|||||||
{actions ? <Group className={classes.contextActions}>{actions}</Group> : null}
|
{actions ? <Group className={classes.contextActions}>{actions}</Group> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppShellHeader>
|
</div>
|
||||||
) : (
|
<div className={classes.mobileContent}>
|
||||||
|
<Group className={classes.mobileIdentity} gap="xs" wrap="nowrap">
|
||||||
|
{hasNavigation ? <ClientBurger /> : null}
|
||||||
|
<HeaderLogo display="logo" logo={logo} logoWithTitle={logoWithTitle} label={t("items.logo")} />
|
||||||
|
</Group>
|
||||||
|
<Group className={classes.mobileActions} gap="xs" wrap="nowrap">
|
||||||
|
{actions}
|
||||||
|
{boardEditAction}
|
||||||
|
{boardSettingsAction}
|
||||||
|
<TourTarget id="board-search">
|
||||||
|
<MobileSearchButton alwaysVisible />
|
||||||
|
</TourTarget>
|
||||||
|
<TourTarget id="board-user-menu">
|
||||||
|
<UserButtonClient
|
||||||
|
avatar={avatar}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
isDockerEnabled={isDockerEnabled}
|
||||||
|
boardSwitcher={boardSwitcher}
|
||||||
|
/>
|
||||||
|
</TourTarget>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</AppShellHeader>
|
||||||
|
{!headerPreferences.visible ? (
|
||||||
<FloatingHeaderControls>
|
<FloatingHeaderControls>
|
||||||
{hasNavigation ? <ClientBurger /> : null}
|
{hasNavigation ? <ClientBurger /> : null}
|
||||||
<UserButtonClient
|
<UserButtonClient
|
||||||
@@ -148,7 +164,7 @@ export const ConfigurableHeader = ({
|
|||||||
boardSwitcher={boardSwitcher}
|
boardSwitcher={boardSwitcher}
|
||||||
/>
|
/>
|
||||||
</FloatingHeaderControls>
|
</FloatingHeaderControls>
|
||||||
)}
|
) : null}
|
||||||
<LazySpotlight />
|
<LazySpotlight />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -291,7 +307,7 @@ const HeaderItem = ({
|
|||||||
<Tooltip label={board.name}>
|
<Tooltip label={board.name}>
|
||||||
<HeaderButton href={`/boards/${encodeURIComponent(board.name)}`} aria-label={board.name}>
|
<HeaderButton href={`/boards/${encodeURIComponent(board.name)}`} aria-label={board.name}>
|
||||||
<Avatar src={board.logoImageUrl} size={22} radius="sm">
|
<Avatar src={board.logoImageUrl} size={22} radius="sm">
|
||||||
<IconLayoutDashboard size={17} stroke={1.5} />
|
{getBoardInitial(board.name)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</HeaderButton>
|
</HeaderButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -299,14 +315,7 @@ const HeaderItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.id === "logo") {
|
if (item.id === "logo") {
|
||||||
return (
|
return <HeaderLogo display={logoDisplay} logo={logo} logoWithTitle={logoWithTitle} label={label("logo")} />;
|
||||||
<HeaderLogo
|
|
||||||
display={logoDisplay}
|
|
||||||
logo={logo}
|
|
||||||
logoWithTitle={logoWithTitle}
|
|
||||||
label={label("logo")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.id === "boardEdit") return boardEditAction;
|
if (item.id === "boardEdit") return boardEditAction;
|
||||||
@@ -416,3 +425,5 @@ const HeaderItem = ({
|
|||||||
</TourTarget>
|
</TourTarget>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBoardInitial = (name: string) => Array.from(name.trim())[0]?.toLocaleUpperCase() ?? "?";
|
||||||
@@ -22,12 +22,12 @@ export const ClientShell = ({
|
|||||||
const collapsed = useAtomValue(navigationCollapsedAtom);
|
const collapsed = useAtomValue(navigationCollapsedAtom);
|
||||||
const backgroundProps = useOptionalBackgroundProps();
|
const backgroundProps = useOptionalBackgroundProps();
|
||||||
const { headerPreferences } = useSettings();
|
const { headerPreferences } = useSettings();
|
||||||
const isHeaderVisible = hasHeader && headerPreferences.visible;
|
const headerHeight = headerPreferences.visible ? appShellHeaderHeight : { base: appShellHeaderHeight, sm: 0 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
{...backgroundProps}
|
{...backgroundProps}
|
||||||
header={isHeaderVisible ? { height: appShellHeaderHeight } : undefined}
|
header={hasHeader ? { height: headerHeight } : undefined}
|
||||||
navbar={
|
navbar={
|
||||||
hasNavigation
|
hasNavigation
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { PropsWithChildren } from "react";
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import type { OnboardingTourFocusRevealProps, OnboardingTourStep } from "@gfazioli/mantine-onboarding-tour";
|
import type { OnboardingTourFocusRevealProps, OnboardingTourStep } from "@gfazioli/mantine-onboarding-tour";
|
||||||
import { useMediaQuery } from "@mantine/hooks";
|
|
||||||
import {
|
import {
|
||||||
IconAffiliateFilled,
|
IconAffiliateFilled,
|
||||||
IconAppsFilled,
|
IconAppsFilled,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { createDocumentationLink } from "@homarr/definitions";
|
import { createDocumentationLink } from "@homarr/definitions";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { useIsMobile } from "@homarr/ui/hooks";
|
||||||
|
|
||||||
import { TourTargetsProvider } from "~/components/layout/header/tour-target";
|
import { TourTargetsProvider } from "~/components/layout/header/tour-target";
|
||||||
import { TourShell } from "./tour-shell";
|
import { TourShell } from "./tour-shell";
|
||||||
@@ -48,7 +48,7 @@ const usersStepFocusRevealProps: OnboardingTourFocusRevealProps = {
|
|||||||
export const ManageTourProvider = ({ children, isAdmin }: ManageTourProviderProps) => {
|
export const ManageTourProvider = ({ children, isAdmin }: ManageTourProviderProps) => {
|
||||||
const t = useI18n("onboardingTour.manage");
|
const t = useI18n("onboardingTour.manage");
|
||||||
const { mutate: completeTour } = clientApi.user.completeTour.useMutation();
|
const { mutate: completeTour } = clientApi.user.completeTour.useMutation();
|
||||||
const isMobile = useMediaQuery("(max-width: 48em)");
|
const isMobile = useIsMobile();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from "./use-is-mobile";
|
||||||
export * from "./use-translated-mantine-react-table";
|
export * from "./use-translated-mantine-react-table";
|
||||||
@@ -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})`);
|
||||||
|
};
|
||||||
Reference in new issue
Block a user