Redesign: Remove old navigation in single-column mode (#40249)

This commit is contained in:
diondiondion
2026-08-24 15:47:38 +00:00
committed by GitHub
parent 1bb1aba44d
commit 9b7ba582c3
7 changed files with 379 additions and 213 deletions

View File

@@ -61,6 +61,7 @@
}
.label {
max-width: 100%;
min-width: 0;
flex-grow: 1;
flex-shrink: 1;

View File

@@ -5,6 +5,7 @@
flex-direction: column;
height: 100%;
overflow-y: auto;
scrollbar-width: thin;
}
.list {

View File

@@ -1,213 +0,0 @@
import {
Children,
cloneElement,
forwardRef,
isValidElement,
lazy,
Suspense,
useCallback,
} from 'react';
import classNames from 'classnames';
import type { List, Record } from 'immutable';
import { ColumnIndexContext } from '@/mastodon/components/column/context';
import { useAppSelector } from '@/mastodon/store';
import { isRedesignEnabled } from '@/mastodon/utils/environment';
import { Footer } from 'mastodon/features/custom_homepage/components/footer';
import { Header } from 'mastodon/features/custom_homepage/components/header';
import { CollapsibleNavigationPanel } from 'mastodon/features/navigation_panel';
import { useBreakpoint } from '../hooks/useBreakpoint';
import {
Compose,
Notifications,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
HashtagTimeline,
DirectTimeline,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Directory,
} from '../util/async-components';
import { useColumnsContext } from '../util/columns_context';
import Bundle from './bundle';
import { BundleColumnError } from './bundle_column_error';
import { ColumnLoading } from './column_loading';
import { ComposePanel, RedirectToMobileComposeIfNeeded } from './compose_panel';
import DrawerLoading from './drawer_loading';
const LazyRedesignNavigationPanel = lazy(() =>
import('@/mastodon/features/navigation_panel/redesign').then(
({ RedesignNavigationPanel }) => ({ default: RedesignNavigationPanel }),
),
);
const componentMap = {
COMPOSE: Compose,
HOME: HomeTimeline,
NOTIFICATIONS: Notifications,
PUBLIC: PublicTimeline,
REMOTE: PublicTimeline,
COMMUNITY: CommunityTimeline,
HASHTAG: HashtagTimeline,
DIRECT: DirectTimeline,
FAVOURITES: FavouritedStatuses,
BOOKMARKS: BookmarkedStatuses,
LIST: ListTimeline,
DIRECTORY: Directory,
} as const;
const TabsBarPortal = () => {
const { setTabsBarElement } = useColumnsContext();
const setRef = useCallback(
(element: HTMLDivElement | null) => {
if (element) {
setTabsBarElement(element);
}
},
[setTabsBarElement],
);
return <div id='tabs-bar__portal' ref={setRef} />;
};
interface Column {
uuid: string;
id: keyof typeof componentMap;
params?: null | Record<{ other?: unknown }>;
}
type FetchedComponent = React.FC<{
columnId?: string;
multiColumn?: boolean;
params: unknown;
}>;
export const ColumnsArea = forwardRef<
HTMLDivElement,
{
singleColumn?: boolean;
minimalShell?: boolean;
children: React.ReactElement | React.ReactElement[];
}
>(({ children, minimalShell, singleColumn }, ref) => {
const renderComposePanel = !useBreakpoint('full');
const columns = useAppSelector(
(state) => state.settings.get('columns') as List<Record<Column>>,
);
const isModalOpen = useAppSelector(
(state) => !state.modal.get('stack').isEmpty(),
);
if (minimalShell) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__main'>
<Header />
<div className='tabs-bar__wrapper'>
<TabsBarPortal />
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
<Footer />
</div>
</div>
);
}
if (singleColumn) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{isRedesignEnabled() ? (
<Suspense>
<LazyRedesignNavigationPanel />
</Suspense>
) : (
<>
{renderComposePanel && <ComposePanel />}
<RedirectToMobileComposeIfNeeded />
</>
)}
</div>
</div>
<main className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'>
<TabsBarPortal />
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
</main>
<CollapsibleNavigationPanel />
</div>
);
}
return (
<main
className={classNames('columns-area', { unscrollable: isModalOpen })}
ref={ref}
tabIndex={isModalOpen ? undefined : 0}
>
{columns.map((column, index) => {
const params = column.get('params')
? column.get('params')?.toJS()
: null;
const other = params?.other ?? {};
const uuid = column.get('uuid');
const id = column.get('id');
return (
<ColumnIndexContext.Provider value={index} key={uuid}>
<Bundle
key={uuid}
fetchComponent={componentMap[id]}
loading={renderLoading(id)}
error={ErrorComponent}
>
{(SpecificComponent: FetchedComponent) => (
<SpecificComponent
columnId={uuid}
params={params}
multiColumn
{...other}
/>
)}
</Bundle>
</ColumnIndexContext.Provider>
);
})}
<ColumnIndexContext.Provider value={columns.size}>
{Children.map(children, (child) =>
isValidElement<{ multiColumn?: boolean }>(child)
? cloneElement(child, { multiColumn: true })
: child,
)}
</ColumnIndexContext.Provider>
</main>
);
});
ColumnsArea.displayName = 'ColumnsArea';
const ErrorComponent = (props: { onRetry: () => void }) => {
return <BundleColumnError multiColumn errorType='network' {...props} />;
};
const renderLoading = (columnId: string) => {
const LoadingComponent =
columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
return () => LoadingComponent;
};

View File

@@ -0,0 +1,122 @@
import { lazy, Suspense, useCallback } from 'react';
import classNames from 'classnames';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import { Footer } from '@/mastodon/features/custom_homepage/components/footer';
import { Header } from '@/mastodon/features/custom_homepage/components/header';
import { CollapsibleNavigationPanel } from '@/mastodon/features/navigation_panel';
import { useBreakpoint } from '@/mastodon/features/ui/hooks/useBreakpoint';
import { useColumnsContext } from '@/mastodon/features/ui/util/columns_context';
import { useAppSelector } from '@/mastodon/store';
import { isRedesignEnabled } from '@/mastodon/utils/environment';
import {
ComposePanel,
RedirectToMobileComposeIfNeeded,
} from '../compose_panel';
import { MultiColumnContent } from './multi_column_content';
const LazyColumnsAreaRedesign = lazy(() =>
import('@/mastodon/features/ui/components/columns_area/redesign').then(
({ ColumnsAreaRedesign }) => ({ default: ColumnsAreaRedesign }),
),
);
const TabsBarPortal = () => {
const { setTabsBarElement } = useColumnsContext();
const setRef = useCallback(
(element: HTMLDivElement | null) => {
if (element) {
setTabsBarElement(element);
}
},
[setTabsBarElement],
);
return <div id='tabs-bar__portal' ref={setRef} />;
};
interface ColumnsAreaProps {
singleColumn?: boolean;
minimalShell?: boolean;
children: React.ReactElement | React.ReactElement[];
ref?: React.Ref<HTMLDivElement>;
}
const ColumnsAreaLegacy: React.FC<ColumnsAreaProps> = ({
children,
minimalShell,
singleColumn,
ref,
}) => {
const renderComposePanel = !useBreakpoint('full');
const isModalOpen = useAppSelector(
(state) => !state.modal.get('stack').isEmpty(),
);
if (minimalShell) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__main'>
<Header />
<div className='tabs-bar__wrapper'>
<TabsBarPortal />
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
<Footer />
</div>
</div>
);
}
if (singleColumn) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
<RedirectToMobileComposeIfNeeded />
</div>
</div>
<main className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'>
<TabsBarPortal />
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
</main>
<CollapsibleNavigationPanel />
</div>
);
}
return (
<main
className={classNames('columns-area', { unscrollable: isModalOpen })}
ref={ref}
tabIndex={isModalOpen ? undefined : 0}
>
<MultiColumnContent>{children}</MultiColumnContent>
</main>
);
};
export const ColumnsArea: React.FC<ColumnsAreaProps> = (props) => {
if (isRedesignEnabled()) {
return (
<Suspense fallback={<LoadingIndicator />}>
<LazyColumnsAreaRedesign {...props} />
</Suspense>
);
} else {
return <ColumnsAreaLegacy {...props} />;
}
};

View File

@@ -0,0 +1,109 @@
import { Children, cloneElement, isValidElement } from 'react';
import type { List, Record } from 'immutable';
import { ColumnIndexContext } from '@/mastodon/components/column/context';
import { useAppSelector } from '@/mastodon/store';
import {
Compose,
Notifications,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
HashtagTimeline,
DirectTimeline,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Directory,
} from '../../util/async-components';
import Bundle from '../bundle';
import { BundleColumnError } from '../bundle_column_error';
import { ColumnLoading } from '../column_loading';
import DrawerLoading from '../drawer_loading';
const componentMap = {
COMPOSE: Compose,
HOME: HomeTimeline,
NOTIFICATIONS: Notifications,
PUBLIC: PublicTimeline,
REMOTE: PublicTimeline,
COMMUNITY: CommunityTimeline,
HASHTAG: HashtagTimeline,
DIRECT: DirectTimeline,
FAVOURITES: FavouritedStatuses,
BOOKMARKS: BookmarkedStatuses,
LIST: ListTimeline,
DIRECTORY: Directory,
} as const;
interface Column {
uuid: string;
id: keyof typeof componentMap;
params?: null | Record<{ other?: unknown }>;
}
type FetchedComponent = React.FC<{
columnId?: string;
multiColumn?: boolean;
params: unknown;
}>;
const ErrorComponent = (props: { onRetry: () => void }) => (
<BundleColumnError multiColumn errorType='network' {...props} />
);
const renderLoading = (columnId: string) => {
const LoadingComponent =
columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
return () => LoadingComponent;
};
export const MultiColumnContent: React.FC<{
children: React.ReactElement | React.ReactElement[];
}> = ({ children }) => {
const columns = useAppSelector(
(state) => state.settings.get('columns') as List<Record<Column>>,
);
return (
<>
{columns.map((column, index) => {
const params = column.get('params')
? column.get('params')?.toJS()
: null;
const other = params?.other ?? {};
const uuid = column.get('uuid');
const id = column.get('id');
return (
<ColumnIndexContext.Provider value={index} key={uuid}>
<Bundle
key={uuid}
fetchComponent={componentMap[id]}
loading={renderLoading(id)}
error={ErrorComponent}
>
{(SpecificComponent: FetchedComponent) => (
<SpecificComponent
columnId={uuid}
params={params}
multiColumn
{...other}
/>
)}
</Bundle>
</ColumnIndexContext.Provider>
);
})}
<ColumnIndexContext.Provider value={columns.size}>
{Children.map(children, (child) =>
isValidElement<{ multiColumn?: boolean }>(child)
? cloneElement(child, { multiColumn: true })
: child,
)}
</ColumnIndexContext.Provider>
</>
);
};

View File

@@ -0,0 +1,56 @@
@use '@/styles/mastodon/variables' as *;
.root {
--main-column-max-width: 640px;
--navigation-min-width: 280px;
--navigation-max-width: 360px;
box-sizing: border-box;
width: 100%;
height: 100%;
min-height: 100vh;
min-height: 100dvh;
padding-bottom: env(safe-area-inset-bottom);
@media (min-width: $no-gap-breakpoint) {
padding-inline: var(--space-2xs);
}
@media (min-width: $mobile-menu-breakpoint) {
display: grid;
grid-template-columns:
minmax(var(--navigation-min-width), 1fr)
minmax(0, var(--main-column-max-width))
minmax(0, 1fr);
gap: var(--space-xs);
}
}
.navigationWrapper {
position: sticky;
top: 0;
z-index: 1;
max-width: var(--navigation-max-width);
height: 100dvh;
padding-bottom: env(safe-area-inset-bottom);
@media (max-width: ($mobile-menu-breakpoint - 1)) {
display: none;
}
}
.main {
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 0 1 auto;
width: 100%;
contain: inline-size layout paint style;
container: column / inline-size;
color: var(--color-text-primary);
background-color: var(--color-bg-primary);
@media ($mobile-menu-breakpoint < width < $no-gap-breakpoint) {
border-inline: 1px solid var(--color-border-primary);
}
}

View File

@@ -0,0 +1,90 @@
import { useCallback } from 'react';
import classNames from 'classnames';
import { CollapsibleNavigationPanel } from '@/mastodon/features/navigation_panel';
import { RedesignNavigationPanel } from '@/mastodon/features/navigation_panel/redesign';
import { useAppSelector } from '@/mastodon/store';
import { Footer } from 'mastodon/features/custom_homepage/components/footer';
import { Header } from 'mastodon/features/custom_homepage/components/header';
import { useBreakpoint } from '../../hooks/useBreakpoint';
import { useColumnsContext } from '../../util/columns_context';
import { MultiColumnContent } from './multi_column_content';
import classes from './redesign.module.scss';
const TabsBarPortal = () => {
const { setTabsBarElement } = useColumnsContext();
const setRef = useCallback(
(element: HTMLDivElement | null) => {
if (element) {
setTabsBarElement(element);
}
},
[setTabsBarElement],
);
return <div id='tabs-bar__portal' ref={setRef} />;
};
export const ColumnsAreaRedesign: React.FC<{
singleColumn?: boolean;
minimalShell?: boolean;
children: React.ReactElement | React.ReactElement[];
ref?: React.Ref<HTMLDivElement>;
}> = ({ children, minimalShell, singleColumn, ref }) => {
const isModalOpen = useAppSelector(
(state) => !state.modal.get('stack').isEmpty(),
);
const renderLegacyNavForMobile = useBreakpoint('openable');
if (minimalShell) {
return (
<div className={classes.root}>
<div className={classes.main}>
<Header />
<div className='tabs-bar__wrapper'>
<TabsBarPortal />
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
<Footer />
</div>
</div>
);
}
if (singleColumn) {
return (
<div className={classes.root}>
<div className={classes.navigationWrapper}>
<RedesignNavigationPanel />
</div>
<main className={classes.main}>
<div className='tabs-bar__wrapper'>
<TabsBarPortal />
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
{renderLegacyNavForMobile && <CollapsibleNavigationPanel />}
</main>
</div>
);
}
return (
<main
className={classNames('columns-area', { unscrollable: isModalOpen })}
ref={ref}
tabIndex={isModalOpen ? undefined : 0}
>
<MultiColumnContent>{children}</MultiColumnContent>
</main>
);
};