diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.module.scss b/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.module.scss index 3aec81ebb58..acf44c67ed4 100644 --- a/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.module.scss +++ b/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.module.scss @@ -61,6 +61,7 @@ } .label { + max-width: 100%; min-width: 0; flex-grow: 1; flex-shrink: 1; diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/styles.module.scss b/app/javascript/mastodon/features/navigation_panel/redesign/styles.module.scss index 52463424f55..43a4a3ccd4d 100644 --- a/app/javascript/mastodon/features/navigation_panel/redesign/styles.module.scss +++ b/app/javascript/mastodon/features/navigation_panel/redesign/styles.module.scss @@ -5,6 +5,7 @@ flex-direction: column; height: 100%; overflow-y: auto; + scrollbar-width: thin; } .list { diff --git a/app/javascript/mastodon/features/ui/components/columns_area.tsx b/app/javascript/mastodon/features/ui/components/columns_area.tsx deleted file mode 100644 index 0a6eec74341..00000000000 --- a/app/javascript/mastodon/features/ui/components/columns_area.tsx +++ /dev/null @@ -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
; -}; - -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>, - ); - const isModalOpen = useAppSelector( - (state) => !state.modal.get('stack').isEmpty(), - ); - - if (minimalShell) { - return ( -
-
-
- -
- -
- -
{children}
- -
-
-
- ); - } - - if (singleColumn) { - return ( -
-
-
- {isRedesignEnabled() ? ( - - - - ) : ( - <> - {renderComposePanel && } - - - )} -
-
- -
-
- -
- -
{children}
-
- - -
- ); - } - - 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 ( - - - {(SpecificComponent: FetchedComponent) => ( - - )} - - - ); - })} - - - {Children.map(children, (child) => - isValidElement<{ multiColumn?: boolean }>(child) - ? cloneElement(child, { multiColumn: true }) - : child, - )} - -
- ); -}); - -ColumnsArea.displayName = 'ColumnsArea'; - -const ErrorComponent = (props: { onRetry: () => void }) => { - return ; -}; - -const renderLoading = (columnId: string) => { - const LoadingComponent = - columnId === 'COMPOSE' ? : ; - return () => LoadingComponent; -}; diff --git a/app/javascript/mastodon/features/ui/components/columns_area/index.tsx b/app/javascript/mastodon/features/ui/components/columns_area/index.tsx new file mode 100644 index 00000000000..c2dd3bfaa9d --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/columns_area/index.tsx @@ -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
; +}; + +interface ColumnsAreaProps { + singleColumn?: boolean; + minimalShell?: boolean; + children: React.ReactElement | React.ReactElement[]; + ref?: React.Ref; +} + +const ColumnsAreaLegacy: React.FC = ({ + children, + minimalShell, + singleColumn, + ref, +}) => { + const renderComposePanel = !useBreakpoint('full'); + const isModalOpen = useAppSelector( + (state) => !state.modal.get('stack').isEmpty(), + ); + + if (minimalShell) { + return ( +
+
+
+ +
+ +
+ +
{children}
+ +
+
+
+ ); + } + + if (singleColumn) { + return ( +
+
+
+ {renderComposePanel && } + +
+
+ +
+
+ +
+ +
{children}
+
+ + +
+ ); + } + + return ( +
+ {children} +
+ ); +}; + +export const ColumnsArea: React.FC = (props) => { + if (isRedesignEnabled()) { + return ( + }> + + + ); + } else { + return ; + } +}; diff --git a/app/javascript/mastodon/features/ui/components/columns_area/multi_column_content.tsx b/app/javascript/mastodon/features/ui/components/columns_area/multi_column_content.tsx new file mode 100644 index 00000000000..957d9d13f3f --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/columns_area/multi_column_content.tsx @@ -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 }) => ( + +); + +const renderLoading = (columnId: string) => { + const LoadingComponent = + columnId === 'COMPOSE' ? : ; + return () => LoadingComponent; +}; + +export const MultiColumnContent: React.FC<{ + children: React.ReactElement | React.ReactElement[]; +}> = ({ children }) => { + const columns = useAppSelector( + (state) => state.settings.get('columns') as List>, + ); + 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 ( + + + {(SpecificComponent: FetchedComponent) => ( + + )} + + + ); + })} + + + {Children.map(children, (child) => + isValidElement<{ multiColumn?: boolean }>(child) + ? cloneElement(child, { multiColumn: true }) + : child, + )} + + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss b/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss new file mode 100644 index 00000000000..73c6078a0b1 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss @@ -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); + } +} diff --git a/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx b/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx new file mode 100644 index 00000000000..00a32c7e099 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx @@ -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
; +}; + +export const ColumnsAreaRedesign: React.FC<{ + singleColumn?: boolean; + minimalShell?: boolean; + children: React.ReactElement | React.ReactElement[]; + ref?: React.Ref; +}> = ({ children, minimalShell, singleColumn, ref }) => { + const isModalOpen = useAppSelector( + (state) => !state.modal.get('stack').isEmpty(), + ); + const renderLegacyNavForMobile = useBreakpoint('openable'); + + if (minimalShell) { + return ( +
+
+
+ +
+ +
+ +
{children}
+ +
+
+
+ ); + } + + if (singleColumn) { + return ( +
+
+ +
+ +
+
+ +
+ +
{children}
+ + {renderLegacyNavForMobile && } +
+
+ ); + } + + return ( +
+ {children} +
+ ); +};