pl-fe: layout changes for the deck

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-06-30 12:35:43 +02:00
parent f3e7c62073
commit 045e42003c
6 changed files with 101 additions and 35 deletions

View File

@ -22,11 +22,12 @@ interface ISidebarNavigationLink {
to?: string; to?: string;
/** Callback when the link is clicked. */ /** Callback when the link is clicked. */
onClick?: React.EventHandler<React.MouseEvent>; onClick?: React.EventHandler<React.MouseEvent>;
shrink?: boolean;
} }
/** Desktop sidebar navigation link. */ /** Desktop sidebar navigation link. */
const SidebarNavigationLink = React.memo(React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef<HTMLAnchorElement>): JSX.Element => { const SidebarNavigationLink = React.memo(React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef<HTMLAnchorElement>): JSX.Element => {
const { icon, activeIcon, text, to = '', count, countMax, onClick } = props; const { icon, activeIcon, text, to = '', count, countMax, onClick, shrink } = props;
const isActive = location.pathname === to; const isActive = location.pathname === to;
const { demetricator } = useSettings(); const { demetricator } = useSettings();
@ -69,7 +70,9 @@ const SidebarNavigationLink = React.memo(React.forwardRef((props: ISidebarNaviga
/> />
</span> </span>
<Text weight='semibold' theme='inherit'>{text}</Text> {!shrink && (
<Text weight='semibold' theme='inherit'>{text}</Text>
)}
</NavLink> </NavLink>
); );
}), (prevProps, nextProps) => prevProps.count === nextProps.count); }), (prevProps, nextProps) => prevProps.count === nextProps.count);

View File

@ -1,4 +1,5 @@
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
import clsx from 'clsx';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@ -21,6 +22,7 @@ import DropdownMenu, { Menu } from './dropdown-menu';
import SearchInput from './search-input'; import SearchInput from './search-input';
import SidebarNavigationLink from './sidebar-navigation-link'; import SidebarNavigationLink from './sidebar-navigation-link';
import SiteLogo from './site-logo'; import SiteLogo from './site-logo';
import Avatar from './ui/avatar';
const messages = defineMessages({ const messages = defineMessages({
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
@ -36,8 +38,13 @@ const messages = defineMessages({
interactionRequests: { id: 'navigation.interaction_requests', defaultMessage: 'Interaction requests' }, interactionRequests: { id: 'navigation.interaction_requests', defaultMessage: 'Interaction requests' },
}); });
interface ISidebarNavigation {
/** Whether the sidebar is in shrinked mode. */
shrink?: boolean;
}
/** Desktop sidebar with links to different views in the app. */ /** Desktop sidebar with links to different views in the app. */
const SidebarNavigation = React.memo(() => { const SidebarNavigation: React.FC<ISidebarNavigation> = React.memo(({ shrink }) => {
const intl = useIntl(); const intl = useIntl();
const { unreadChatsCount } = useStatContext(); const { unreadChatsCount } = useStatContext();
@ -161,24 +168,41 @@ const SidebarNavigation = React.memo(() => {
}, [!!account, features, followRequestsCount, interactionRequestsCount, scheduledStatusCount, draftCount]); }, [!!account, features, followRequestsCount, interactionRequestsCount, scheduledStatusCount, draftCount]);
return ( return (
<Stack space={4}> <Stack space={4} alignItems={shrink ? 'center' : undefined}>
<SiteLogo className='h-12 w-auto cursor-pointer' /> <SiteLogo
className={clsx('h-12 w-auto cursor-pointer', {
'max-w-10 h-auto': shrink,
})}
/>
{account && ( {account && (
<Stack space={4}> <Stack space={4}>
<div className='relative flex items-center'> <div className='relative flex items-center'>
<ProfileDropdown account={account}> <ProfileDropdown account={account}>
<Account {shrink ? (
account={account} <Avatar
action={<Icon src={require('@tabler/icons/outline/chevron-down.svg')} className='text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500' />} src={account.avatar}
disabled alt={account.avatar_description}
withLinkToProfile={false} isCat={account.is_cat}
/> username={account.username}
size={40}
/>
// className='size-10 bg-gray-50 ring-2 ring-white'
) : (
<Account
account={account}
action={<Icon src={require('@tabler/icons/outline/chevron-down.svg')} className='text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500' />}
disabled
withLinkToProfile={false}
/>
)}
</ProfileDropdown> </ProfileDropdown>
</div> </div>
<div className='block w-full max-w-xs'> {!shrink && (
<SearchInput /> <div className='block w-full max-w-xs'>
</div> <SearchInput />
</div>
)}
</Stack> </Stack>
)} )}
@ -188,12 +212,14 @@ const SidebarNavigation = React.memo(() => {
icon={require('@tabler/icons/outline/home.svg')} icon={require('@tabler/icons/outline/home.svg')}
activeIcon={require('@tabler/icons/filled/home.svg')} activeIcon={require('@tabler/icons/filled/home.svg')}
text={<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />} text={<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />}
shrink={shrink}
/> />
<SidebarNavigationLink <SidebarNavigationLink
to='/search' to='/search'
icon={require('@tabler/icons/outline/search.svg')} icon={require('@tabler/icons/outline/search.svg')}
text={<FormattedMessage id='tabs_bar.search' defaultMessage='Search' />} text={<FormattedMessage id='tabs_bar.search' defaultMessage='Search' />}
shrink={shrink}
/> />
{account && ( {account && (
@ -204,6 +230,7 @@ const SidebarNavigation = React.memo(() => {
activeIcon={require('@tabler/icons/filled/bell.svg')} activeIcon={require('@tabler/icons/filled/bell.svg')}
count={notificationCount} count={notificationCount}
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />} text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
shrink={shrink}
/> />
{features.chats && ( {features.chats && (
@ -213,6 +240,7 @@ const SidebarNavigation = React.memo(() => {
count={unreadChatsCount} count={unreadChatsCount}
countMax={9} countMax={9}
text={<FormattedMessage id='navigation.chats' defaultMessage='Chats' />} text={<FormattedMessage id='navigation.chats' defaultMessage='Chats' />}
shrink={shrink}
/> />
)} )}
@ -222,6 +250,7 @@ const SidebarNavigation = React.memo(() => {
icon={require('@tabler/icons/outline/mail.svg')} icon={require('@tabler/icons/outline/mail.svg')}
activeIcon={require('@tabler/icons/filled/mail.svg')} activeIcon={require('@tabler/icons/filled/mail.svg')}
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Direct messages' />} text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Direct messages' />}
shrink={shrink}
/> />
)} )}
@ -231,6 +260,7 @@ const SidebarNavigation = React.memo(() => {
icon={require('@tabler/icons/outline/circles.svg')} icon={require('@tabler/icons/outline/circles.svg')}
activeIcon={require('@tabler/icons/filled/circles.svg')} activeIcon={require('@tabler/icons/filled/circles.svg')}
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />} text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
shrink={shrink}
/> />
)} )}
@ -239,6 +269,7 @@ const SidebarNavigation = React.memo(() => {
icon={require('@tabler/icons/outline/user.svg')} icon={require('@tabler/icons/outline/user.svg')}
activeIcon={require('@tabler/icons/filled/user.svg')} activeIcon={require('@tabler/icons/filled/user.svg')}
text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />} text={<FormattedMessage id='tabs_bar.profile' defaultMessage='Profile' />}
shrink={shrink}
/> />
<SidebarNavigationLink <SidebarNavigationLink
@ -246,6 +277,7 @@ const SidebarNavigation = React.memo(() => {
icon={require('@tabler/icons/outline/settings.svg')} icon={require('@tabler/icons/outline/settings.svg')}
activeIcon={require('@tabler/icons/filled/settings.svg')} activeIcon={require('@tabler/icons/filled/settings.svg')}
text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />} text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />}
shrink={shrink}
/> />
{(account.is_admin || account.is_moderator) && ( {(account.is_admin || account.is_moderator) && (
@ -254,6 +286,7 @@ const SidebarNavigation = React.memo(() => {
icon={require('@tabler/icons/outline/dashboard.svg')} icon={require('@tabler/icons/outline/dashboard.svg')}
count={dashboardCount} count={dashboardCount}
text={<FormattedMessage id='tabs_bar.dashboard' defaultMessage='Dashboard' />} text={<FormattedMessage id='tabs_bar.dashboard' defaultMessage='Dashboard' />}
shrink={shrink}
/> />
)} )}
</> </>
@ -267,6 +300,7 @@ const SidebarNavigation = React.memo(() => {
icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')} icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')}
activeIcon={features.federating ? require('@tabler/icons/filled/affiliate.svg') : undefined} activeIcon={features.federating ? require('@tabler/icons/filled/affiliate.svg') : undefined}
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />} text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
shrink={shrink}
/> />
)} )}
@ -276,6 +310,7 @@ const SidebarNavigation = React.memo(() => {
icon={require('@tabler/icons/outline/chart-bubble.svg')} icon={require('@tabler/icons/outline/chart-bubble.svg')}
activeIcon={require('@tabler/icons/filled/chart-bubble.svg')} activeIcon={require('@tabler/icons/filled/chart-bubble.svg')}
text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />} text={<FormattedMessage id='tabs_bar.bubble' defaultMessage='Bubble' />}
shrink={shrink}
/> />
)} )}
@ -284,6 +319,7 @@ const SidebarNavigation = React.memo(() => {
to='/timeline/fediverse' to='/timeline/fediverse'
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')} icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />} text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
shrink={shrink}
/> />
)} )}
</> </>
@ -294,6 +330,7 @@ const SidebarNavigation = React.memo(() => {
<SidebarNavigationLink <SidebarNavigationLink
icon={require('@tabler/icons/outline/dots-circle-horizontal.svg')} icon={require('@tabler/icons/outline/dots-circle-horizontal.svg')}
text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />} text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />}
shrink={shrink}
/> />
</DropdownMenu> </DropdownMenu>
)} )}
@ -304,19 +341,21 @@ const SidebarNavigation = React.memo(() => {
to='/login' to='/login'
icon={require('@tabler/icons/outline/login.svg')} icon={require('@tabler/icons/outline/login.svg')}
text={<FormattedMessage id='account.login' defaultMessage='Log in' />} text={<FormattedMessage id='account.login' defaultMessage='Log in' />}
shrink={shrink}
/> />
{isOpen && <SidebarNavigationLink {isOpen && <SidebarNavigationLink
to='/signup' to='/signup'
icon={require('@tabler/icons/outline/user-plus.svg')} icon={require('@tabler/icons/outline/user-plus.svg')}
text={<FormattedMessage id='account.register' defaultMessage='Sign up' />} text={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
shrink={shrink}
/>} />}
</Stack> </Stack>
)} )}
</Stack> </Stack>
{account && ( {account && (
<ComposeButton /> <ComposeButton shrink={shrink} />
)} )}
</Stack> </Stack>
); );

View File

@ -6,6 +6,7 @@ import { useFeatures } from 'pl-fe/hooks/use-features';
interface ISidebar { interface ISidebar {
children: React.ReactNode; children: React.ReactNode;
shrink?: boolean;
} }
interface IAside { interface IAside {
children?: React.ReactNode; children?: React.ReactNode;
@ -13,6 +14,7 @@ interface IAside {
interface ILayout { interface ILayout {
children: React.ReactNode; children: React.ReactNode;
fullWidth?: boolean;
} }
interface LayoutComponent extends React.FC<ILayout> { interface LayoutComponent extends React.FC<ILayout> {
@ -22,17 +24,25 @@ interface LayoutComponent extends React.FC<ILayout> {
} }
/** Layout container, to hold Sidebar, Main, and Aside. */ /** Layout container, to hold Sidebar, Main, and Aside. */
const Layout: LayoutComponent = ({ children }) => ( const Layout: LayoutComponent = ({ children, fullWidth }) => (
<div className='relative flex grow flex-col black:pt-0 sm:pt-4'> <div className='relative flex grow flex-col black:pt-0 sm:pt-4'>
<div className='mx-auto w-full max-w-3xl grow sm:px-6 md:grid md:max-w-7xl md:grid-cols-12 md:gap-8 md:px-8 xl:max-w-[1440px]'> <div
className={clsx(
'mx-auto w-full max-w-3xl grow sm:px-6 md:gap-8 md:px-8',
{
'flex md:max-w-full': fullWidth,
'md:grid md:max-w-7xl md:grid-cols-12 xl:max-w-[1440px]': !fullWidth,
},
)}
>
{children} {children}
</div> </div>
</div> </div>
); );
/** Left sidebar container in the UI. */ /** Left sidebar container in the UI. */
const Sidebar: React.FC<ISidebar> = ({ children }) => ( const Sidebar: React.FC<ISidebar> = ({ children, shrink }) => (
<div className='hidden lg:col-span-3 lg:block'> <div className={clsx('hidden lg:block', { 'lg:col-span-3': !shrink, 'w-fit': shrink })}>
<StickyBox offsetTop={16} className='pb-4'> <StickyBox offsetTop={16} className='pb-4'>
{children} {children}
</StickyBox> </StickyBox>

View File

@ -10,7 +10,12 @@ import HStack from 'pl-fe/components/ui/hstack';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useModalsStore } from 'pl-fe/stores/modals'; import { useModalsStore } from 'pl-fe/stores/modals';
const ComposeButton = () => { interface IComposeButton {
/** Whether the button should shrink to fit in a smaller space. */
shrink?: boolean;
}
const ComposeButton: React.FC<IComposeButton> = ({ shrink }) => {
const location = useLocation(); const location = useLocation();
const isOnGroupPage = location.pathname.startsWith('/group/'); const isOnGroupPage = location.pathname.startsWith('/group/');
const match = useRouteMatch<{ groupId: string }>('/groups/:groupId'); const match = useRouteMatch<{ groupId: string }>('/groups/:groupId');
@ -18,13 +23,13 @@ const ComposeButton = () => {
const isGroupMember = !!group?.relationship?.member; const isGroupMember = !!group?.relationship?.member;
if (isOnGroupPage && isGroupMember) { if (isOnGroupPage && isGroupMember) {
return <GroupComposeButton />; return <GroupComposeButton shrink={shrink} />;
} }
return <HomeComposeButton />; return <HomeComposeButton shrink={shrink} />;
}; };
const HomeComposeButton = () => { const HomeComposeButton: React.FC<IComposeButton> = ({ shrink }) => {
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const onOpenCompose = () => openModal('COMPOSE'); const onOpenCompose = () => openModal('COMPOSE');
@ -34,13 +39,16 @@ const HomeComposeButton = () => {
size='lg' size='lg'
onClick={onOpenCompose} onClick={onOpenCompose}
block block
icon={shrink ? require('@tabler/icons/outline/plus.svg') : undefined}
> >
<FormattedMessage id='navigation.compose' defaultMessage='Compose' /> {!shrink && (
<FormattedMessage id='navigation.compose' defaultMessage='Compose' />
)}
</Button> </Button>
); );
}; };
const GroupComposeButton = () => { const GroupComposeButton: React.FC<IComposeButton> = ({ shrink }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const match = useRouteMatch<{ groupId: string }>('/groups/:groupId'); const match = useRouteMatch<{ groupId: string }>('/groups/:groupId');
const { group } = useGroup(match?.params.groupId || ''); const { group } = useGroup(match?.params.groupId || '');
@ -57,13 +65,16 @@ const GroupComposeButton = () => {
size='lg' size='lg'
onClick={onOpenCompose} onClick={onOpenCompose}
block block
icon={shrink ? require('@tabler/icons/outline/plus.svg') : undefined}
> >
<HStack space={3} alignItems='center'> {!shrink && (
<Avatar className='-my-1 border-2 border-white' size={30} src={group.avatar} alt={group.avatar_description} /> <HStack space={3} alignItems='center'>
<span> <Avatar className='-my-1 border-2 border-white' size={30} src={group.avatar} alt={group.avatar_description} />
<FormattedMessage id='navigation.compose_group' defaultMessage='Compose to group' /> <span>
</span> <FormattedMessage id='navigation.compose_group' defaultMessage='Compose to group' />
</HStack> </span>
</HStack>
)}
</Button> </Button>
); );
}; };

View File

@ -488,6 +488,9 @@ const UI: React.FC<IUI> = React.memo(({ children }) => {
pointerEvents: isDropdownMenuOpen ? 'none' : undefined, pointerEvents: isDropdownMenuOpen ? 'none' : undefined,
}; };
// to be used with the deck
const fullWidth = false;
return ( return (
<GlobalHotkeys node={node}> <GlobalHotkeys node={node}>
<div ref={node} style={style}> <div ref={node} style={style}>
@ -500,9 +503,9 @@ const UI: React.FC<IUI> = React.memo(({ children }) => {
<BackgroundShapes /> <BackgroundShapes />
<div className='z-10 flex min-h-screen flex-col'> <div className='z-10 flex min-h-screen flex-col'>
<Layout> <Layout fullWidth={fullWidth}>
<Layout.Sidebar> <Layout.Sidebar shrink={fullWidth}>
{!(standalone && !me) && <SidebarNavigation />} {!(standalone && !me) && <SidebarNavigation shrink={fullWidth} />}
</Layout.Sidebar> </Layout.Sidebar>
<SwitchingColumnsArea> <SwitchingColumnsArea>

View File

@ -6,7 +6,7 @@ interface IChatsLayout {
/** Custom layout for chats on desktop. */ /** Custom layout for chats on desktop. */
const ChatsLayout: React.FC<IChatsLayout> = ({ children }) => ( const ChatsLayout: React.FC<IChatsLayout> = ({ children }) => (
<div className='black:border-gray-800 md:col-span-12 lg:col-span-9 lg:black:border-l'> <div className='grow black:border-gray-800 md:col-span-12 lg:col-span-9 lg:black:border-l'>
{children} {children}
</div> </div>
); );