force the header in mobile mode if it is too large even if above the mobile breakpoint (fixed #2080)

This commit is contained in:
Athou
2026-08-24 07:17:57 +02:00
parent 4ef27e3027
commit cf43988fae
4 changed files with 35 additions and 16 deletions

View File

@@ -5,14 +5,15 @@ import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
import { Constants } from "@/app/constants" import { Constants } from "@/app/constants"
import { useActionButton } from "@/hooks/useActionButton" import { useActionButton } from "@/hooks/useActionButton"
export type Mode = "auto" | "mobile" | "desktop"
interface ActionButtonProps { interface ActionButtonProps {
icon: ReactNode icon: ReactNode
className?: string className?: string
label?: string | MessageDescriptor label?: string | MessageDescriptor
onClick?: MouseEventHandler onClick?: MouseEventHandler
variant?: ActionIconVariant & ButtonVariant variant?: ActionIconVariant & ButtonVariant
hideLabelOnDesktop?: boolean mode?: Mode
showLabelOnMobile?: boolean
} }
/** /**
@@ -25,7 +26,9 @@ export const ActionButton = forwardRef<HTMLDivElement, ActionButtonProps>((props
const label = typeof props.label === "string" ? props.label : props.label && _(props.label) const label = typeof props.label === "string" ? props.label : props.label && _(props.label)
const variant = props.variant ?? "subtle" const variant = props.variant ?? "subtle"
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
const mode: Mode = props.mode ?? "auto"
const iconOnly = mode === "mobile" || (mode === "auto" && mobile)
return ( return (
<Box ref={ref} className="cf-action-button"> <Box ref={ref} className="cf-action-button">

View File

@@ -2,7 +2,8 @@ import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react" import { useLingui } from "@lingui/react"
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core" import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { useEffect } from "react" import { useElementSize, useViewportSize } from "@mantine/hooks"
import { type ReactNode, type RefCallback, useEffect } from "react"
import { import {
TbArrowDown, TbArrowDown,
TbArrowUp, TbArrowUp,
@@ -20,7 +21,7 @@ import {
import { markAllAsReadWithConfirmationIfRequired, reloadEntries, search, selectNextEntry, selectPreviousEntry } from "@/app/entries/thunks" import { markAllAsReadWithConfirmationIfRequired, reloadEntries, search, selectNextEntry, selectPreviousEntry } from "@/app/entries/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store" import { useAppDispatch, useAppSelector } from "@/app/store"
import { changeSettings } from "@/app/user/thunks" import { changeSettings } from "@/app/user/thunks"
import { ActionButton } from "@/components/ActionButton" import { ActionButton, type Mode } from "@/components/ActionButton"
import { Loader } from "@/components/Loader" import { Loader } from "@/components/Loader"
import { useActionButton } from "@/hooks/useActionButton" import { useActionButton } from "@/hooks/useActionButton"
import { useBrowserExtension } from "@/hooks/useBrowserExtension" import { useBrowserExtension } from "@/hooks/useBrowserExtension"
@@ -31,7 +32,7 @@ function HeaderDivider() {
return <Divider orientation="vertical" /> return <Divider orientation="vertical" />
} }
function HeaderToolbar(props: { children: React.ReactNode }) { function HeaderToolbar(props: { children: ReactNode; ref?: RefCallback<typeof HeaderToolbar> }) {
const { spacing } = useActionButton() const { spacing } = useActionButton()
const mobile = useMobile("480px") const mobile = useMobile("480px")
return mobile ? ( return mobile ? (
@@ -47,7 +48,7 @@ function HeaderToolbar(props: { children: React.ReactNode }) {
{props.children} {props.children}
</Box> </Box>
) : ( ) : (
<Group gap={spacing} className="cf-toolbar"> <Group gap={spacing} wrap="nowrap" className="cf-toolbar">
{props.children} {props.children}
</Group> </Group>
) )
@@ -61,6 +62,13 @@ export function Header() {
const searchFromStore = useAppSelector(state => state.entries.search) const searchFromStore = useAppSelector(state => state.entries.search)
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension() const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const viewport = useViewportSize()
const { ref: headerRef, width: headerWidth } = useElementSize()
const sidebarWidth = useAppSelector(state => state.user.localSettings.sidebarWidth)
const headerTooLarge = headerWidth > viewport.width - sidebarWidth
const mode: Mode = headerTooLarge ? "mobile" : "auto"
const { _ } = useLingui() const { _ } = useLingui()
const searchForm = useForm<{ search: string }>() const searchForm = useForm<{ search: string }>()
@@ -94,10 +102,11 @@ export function Header() {
return ( return (
<Center className="cf-toolbar-wrapper"> <Center className="cf-toolbar-wrapper">
<HeaderToolbar> <HeaderToolbar ref={headerRef}>
<ActionButton <ActionButton
icon={<TbArrowUp size={iconSize} />} icon={<TbArrowUp size={iconSize} />}
label={msg`Previous`} label={msg`Previous`}
mode={mode}
onClick={async () => onClick={async () =>
await dispatch( await dispatch(
selectPreviousEntry({ selectPreviousEntry({
@@ -111,6 +120,7 @@ export function Header() {
<ActionButton <ActionButton
icon={<TbArrowDown size={iconSize} />} icon={<TbArrowDown size={iconSize} />}
label={msg`Next`} label={msg`Next`}
mode={mode}
onClick={async () => onClick={async () =>
await dispatch( await dispatch(
selectNextEntry({ selectNextEntry({
@@ -127,11 +137,13 @@ export function Header() {
<ActionButton <ActionButton
icon={<TbRefresh size={iconSize} />} icon={<TbRefresh size={iconSize} />}
label={msg`Refresh`} label={msg`Refresh`}
mode={mode}
onClick={async () => await dispatch(reloadEntries())} onClick={async () => await dispatch(reloadEntries())}
/> />
<ActionButton <ActionButton
icon={<TbChecks size={iconSize} />} icon={<TbChecks size={iconSize} />}
label={msg`Mark all as read`} label={msg`Mark all as read`}
mode={mode}
onClick={() => dispatch(markAllAsReadWithConfirmationIfRequired())} onClick={() => dispatch(markAllAsReadWithConfirmationIfRequired())}
/> />
@@ -140,18 +152,20 @@ export function Header() {
<ActionButton <ActionButton
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />} icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
label={settings.readingMode === "all" ? msg`All` : msg`Unread`} label={settings.readingMode === "all" ? msg`All` : msg`Unread`}
mode={mode}
onClick={toggleReadingMode} onClick={toggleReadingMode}
/> />
<ActionButton <ActionButton
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />} icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
label={settings.readingOrder === "asc" ? msg`Asc` : msg`Desc`} label={settings.readingOrder === "asc" ? msg`Asc` : msg`Desc`}
mode={mode}
onClick={toggleReadingOrder} onClick={toggleReadingOrder}
/> />
<Popover> <Popover>
<Popover.Target> <Popover.Target>
<Indicator disabled={!searchFromStore}> <Indicator disabled={!searchFromStore}>
<ActionButton icon={<TbSearch size={iconSize} />} label={msg`Search`} /> <ActionButton icon={<TbSearch size={iconSize} />} label={msg`Search`} mode={mode} />
</Indicator> </Indicator>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
@@ -169,7 +183,7 @@ export function Header() {
<HeaderDivider /> <HeaderDivider />
<ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} /> <ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} mode={mode} />} />
{isBrowserExtensionPopup && ( {isBrowserExtensionPopup && (
<> <>
@@ -178,11 +192,13 @@ export function Header() {
<ActionButton <ActionButton
icon={<TbSettings size={iconSize} />} icon={<TbSettings size={iconSize} />}
label={msg`Extension options`} label={msg`Extension options`}
mode={mode}
onClick={() => openSettingsPage()} onClick={() => openSettingsPage()}
/> />
<ActionButton <ActionButton
icon={<TbExternalLink size={iconSize} />} icon={<TbExternalLink size={iconSize} />}
label={msg`Open CommaFeed`} label={msg`Open CommaFeed`}
mode={mode}
onClick={() => openAppInNewTab()} onClick={() => openAppInNewTab()}
/> />
</> </>

View File

@@ -42,7 +42,7 @@ export function WelcomePage() {
icon={<TbClock size={iconSize} />} icon={<TbClock size={iconSize} />}
variant="outline" variant="outline"
onClick={async () => await login.execute({ name: "demo", password: "demo" })} onClick={async () => await login.execute({ name: "demo", password: "demo" })}
showLabelOnMobile mode="desktop"
/> />
</Center> </Center>
)} )}
@@ -100,7 +100,7 @@ function Buttons() {
icon={<TbKey size={iconSize} />} icon={<TbKey size={iconSize} />}
variant="outline" variant="outline"
onClick={async () => await dispatch(redirectToLogin())} onClick={async () => await dispatch(redirectToLogin())}
showLabelOnMobile mode="desktop"
/> />
{serverInfos?.allowRegistrations && ( {serverInfos?.allowRegistrations && (
<ActionButton <ActionButton
@@ -108,7 +108,7 @@ function Buttons() {
icon={<TbUserPlus size={iconSize} />} icon={<TbUserPlus size={iconSize} />}
variant="filled" variant="filled"
onClick={async () => await dispatch(redirectToRegistration())} onClick={async () => await dispatch(redirectToRegistration())}
showLabelOnMobile mode="desktop"
/> />
)} )}
@@ -116,7 +116,7 @@ function Buttons() {
label={dark ? msg`Switch to light theme` : msg`Switch to dark theme`} label={dark ? msg`Switch to light theme` : msg`Switch to dark theme`}
icon={colorScheme === "dark" ? <TbSun size={18} /> : <TbMoon size={iconSize} />} icon={colorScheme === "dark" ? <TbSun size={18} /> : <TbMoon size={iconSize} />}
onClick={() => toggleColorScheme()} onClick={() => toggleColorScheme()}
hideLabelOnDesktop mode="mobile"
/> />
{isBrowserExtensionPopup && ( {isBrowserExtensionPopup && (
@@ -124,7 +124,7 @@ function Buttons() {
label={msg`Extension options`} label={msg`Extension options`}
icon={<TbSettings size={iconSize} />} icon={<TbSettings size={iconSize} />}
onClick={() => openSettingsPage()} onClick={() => openSettingsPage()}
hideLabelOnDesktop mode="mobile"
/> />
)} )}
</Group> </Group>

View File

@@ -227,7 +227,7 @@ export default function Layout(props: Readonly<LayoutProps>) {
)} )}
</OnMobile> </OnMobile>
<OnDesktop> <OnDesktop>
<Group p="md"> <Group p="md" wrap="nowrap">
<Group justify="space-between" style={{ width: sidebarWidth - 16 }}> <Group justify="space-between" style={{ width: sidebarWidth - 16 }}>
<Box> <Box>
<LogoAndTitle /> <LogoAndTitle />