diff --git a/app/javascript/mastodon/features/compose/redesign/trigger.module.scss b/app/javascript/mastodon/features/compose/redesign/trigger.module.scss index 641a4468908..7cccba24a64 100644 --- a/app/javascript/mastodon/features/compose/redesign/trigger.module.scss +++ b/app/javascript/mastodon/features/compose/redesign/trigger.module.scss @@ -1,9 +1,12 @@ @use '@/styles/mastodon/variables'; .button { - position: fixed; - bottom: var(--space-md); - right: var(--space-md); + &:not(.buttonInline) { + position: fixed; + bottom: var(--space-md); + right: var(--space-md); + z-index: 2; + } } .composer, diff --git a/app/javascript/mastodon/features/compose/redesign/trigger.tsx b/app/javascript/mastodon/features/compose/redesign/trigger.tsx index 5666a80f6c7..f53c98038fa 100644 --- a/app/javascript/mastodon/features/compose/redesign/trigger.tsx +++ b/app/javascript/mastodon/features/compose/redesign/trigger.tsx @@ -4,6 +4,8 @@ import { lazy, Suspense, useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; + import { ChatCircleIcon, NewspaperIcon, @@ -32,7 +34,12 @@ const ComposeLazyForm = lazy(() => })), ); -export const ComposeRedesignButton: React.FC = () => { +export const ComposeRedesignButton: React.FC<{ + /** + * Render the button in regular document flow instead of fixed positioning for mobile layout + */ + inline?: boolean; +}> = ({ inline }) => { const displayState = useAppSelector((state) => state.composer.displayState); const dispatch = useAppDispatch(); @@ -75,7 +82,7 @@ export const ComposeRedesignButton: React.FC = () => { as={IconButton} icon={PenNibIcon} variant='solid' - className={classes.button} + className={classNames(classes.button, inline && classes.buttonInline)} size='lg' > * { + pointer-events: initial; + } + + @media (width > $mobile-menu-breakpoint) { + display: none; + } +} + +.list { + box-sizing: border-box; + display: flex; + align-items: center; + align-self: center; + width: 100%; + max-width: 400px; + padding: var(--space-2xs); + background-color: var(--color-bg-primary); + border-radius: var(--radius-round); + @include mixins.elevation-1; + + > li { + flex-grow: 1; + } +} + +.avatar, +.avatar img { + border-radius: var(--radius-round); +} diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx b/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx new file mode 100644 index 00000000000..d841c6e782b --- /dev/null +++ b/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx @@ -0,0 +1,71 @@ +import { FormattedMessage } from 'react-intl'; + +import { + BellIcon, + ChatCircleIcon, + HouseIcon, + MagnifyingGlassIcon, +} from '@phosphor-icons/react'; + +import { Avatar } from '@/mastodon/components/avatar'; +import { FOCUS_TARGET } from '@/mastodon/components/navigation_focus_target'; +import { ComposeRedesignButton } from '@/mastodon/features/compose/redesign/trigger'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useIdentity } from '@/mastodon/identity_context'; +import { selectUnreadNotificationGroupsCount } from '@/mastodon/selectors/notifications'; +import { useAppSelector } from '@/mastodon/store'; + +import classes from './mobile_nav.module.scss'; +import { MobileNavLink } from './navigation_link'; + +export const RedesignMobileNavigation: React.FC = () => { + const { accountId } = useIdentity(); + const account = useAccount(accountId); + const notificationsCount = useAppSelector( + selectUnreadNotificationGroupsCount, + ); + return ( + + ); +}; 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 acf44c67ed4..4cff65ccb57 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 @@ -9,6 +9,9 @@ } .link { + --icon-size: 20px; + --border-radius: var(--radius-sm); + position: relative; box-sizing: border-box; display: flex; @@ -16,7 +19,7 @@ width: 100%; padding: var(--space-xs); column-gap: var(--space-sm); - border-radius: var(--radius-sm); + border-radius: var(--border-radius); border: none; color: inherit; background-color: transparent; @@ -26,6 +29,10 @@ text-decoration: none; text-align: start; + &:not(:where(.linkStacked, .linkMobile)) { + color: var(--color-text-secondary); + } + &:hover, &:focus-visible, &[aria-current='page'] { @@ -33,26 +40,42 @@ text-decoration: none; } + &[aria-current='page'] { + color: var(--color-text-primary); + } + &:focus-visible { outline: var(--outline-focus-default); } &:focus { // Override silly global focus style - border-radius: var(--radius-sm); + border-radius: var(--border-radius); } } .linkStacked { + --icon-size: 24px; + flex-direction: column; row-gap: var(--space-2xs); padding-inline: 0; @include mixins.type-micro; } +.linkMobile { + --icon-size: 24px; + --border-radius: var(--radius-round); + + width: 100%; + justify-content: center; + gap: 0; + padding-block: var(--space-sm); +} + .icon { - width: 20px; - height: 20px; + width: var(--icon-size); + height: var(--icon-size); & > svg { width: inherit; @@ -60,6 +83,23 @@ } } +// A dot is shown instead of a number badge on mobile +.iconWithDot { + position: relative; + + &::after { + content: ''; + position: absolute; + top: var(--space-4xs); + left: calc(50% + var(--space-3xs)); + width: var(--space-xs); + height: var(--space-xs); + border-radius: var(--radius-round); + background-color: var(--color-bg-brand-base); + border: 1px solid var(--color-bg-primary); + } +} + .label { max-width: 100%; min-width: 0; diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.tsx b/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.tsx index c4267f1a0ab..9b872dbb4a0 100644 --- a/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.tsx +++ b/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.tsx @@ -1,12 +1,13 @@ -import type { SVGProps } from 'react'; +import type { ReactNode, SVGProps } from 'react'; import classNames from 'classnames'; -import { NavLink, matchPath, useLocation } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; import type { NavLinkProps } from 'react-router-dom'; import type { Icon } from '@phosphor-icons/react'; import { Badge } from '@/mastodon/components/badge'; +import { useIsLinkActive } from '@/mastodon/hooks/useIsLinkActive'; import classes from './navigation_link.module.scss'; @@ -29,20 +30,14 @@ export const NavigationLink: React.FC = ({ children, ...otherProps }) => { - const location = useLocation(); - let Comp: React.ElementType = as; if (as === 'link') { Comp = NavLink; } - const to = 'to' in otherProps && otherProps.to; - const isActive = - to && typeof to !== 'function' - ? !!matchPath(location.pathname, { - path: typeof to === 'string' ? to : to.pathname, - }) - : false; + const isActive = useIsLinkActive( + 'to' in otherProps ? otherProps.to : undefined, + ); return (
  • = ({ > {IconComp && ( - + )} {children} @@ -72,3 +70,49 @@ export const NavigationLink: React.FC = ({
  • ); }; + +type MobileNavLink = NavLinkProps & { + withDot?: boolean; + children: ReactNode; +} & ( + | { + iconComponent: Icon | React.FC>; + customIcon?: never; + } + | { + customIcon: ReactNode; + iconComponent?: never; + } + ); + +export const MobileNavLink: React.FC = ({ + iconComponent: IconComp, + customIcon, + withDot, + children, + ...otherProps +}) => { + const isActive = useIsLinkActive( + 'to' in otherProps ? otherProps.to : undefined, + ); + + return ( +
  • + + + {IconComp ? ( + + ) : ( + customIcon + )} + + {children} + +
  • + ); +}; 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 index 0e4f6184a3d..cc45dc1d430 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss +++ b/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss @@ -1,11 +1,12 @@ @use '@/styles/mastodon/variables' as *; .root { - --main-column-max-width: 640px; + --main-column-max-width: 600px; --navigation-min-width: 280px; - --navigation-max-width: 360px; + --navigation-max-width: 320px; box-sizing: border-box; + display: flex; width: 100%; height: 100%; min-height: 100vh; diff --git a/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx b/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx index 00a32c7e099..e53485421a5 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx +++ b/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx @@ -2,8 +2,10 @@ import { useCallback } from 'react'; import classNames from 'classnames'; +import { ComposeRedesignButton } from '@/mastodon/features/compose/redesign/trigger'; import { CollapsibleNavigationPanel } from '@/mastodon/features/navigation_panel'; import { RedesignNavigationPanel } from '@/mastodon/features/navigation_panel/redesign'; +import { RedesignMobileNavigation } from '@/mastodon/features/navigation_panel/redesign/mobile_nav'; import { useAppSelector } from '@/mastodon/store'; import { Footer } from 'mastodon/features/custom_homepage/components/footer'; import { Header } from 'mastodon/features/custom_homepage/components/header'; @@ -38,7 +40,7 @@ export const ColumnsAreaRedesign: React.FC<{ const isModalOpen = useAppSelector( (state) => !state.modal.get('stack').isEmpty(), ); - const renderLegacyNavForMobile = useBreakpoint('openable'); + const isMobile = useBreakpoint('openable'); if (minimalShell) { return ( @@ -64,6 +66,7 @@ export const ColumnsAreaRedesign: React.FC<{
    + {isMobile ? : }
    @@ -72,7 +75,10 @@ export const ColumnsAreaRedesign: React.FC<{
    {children}
    - {renderLegacyNavForMobile && } + {isMobile && ( + // Legacy mobile navigation, to be removed + + )}
    ); diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 4d4f0da050f..6048ec6fe6e 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -272,11 +272,6 @@ class SwitchingColumnsArea extends PureComponent { } } -const LazyRedesignComposeButton = lazy( - () => import('@/mastodon/features/compose/redesign/trigger') - .then(({ ComposeRedesignButton }) => ({ default: ComposeRedesignButton })) -); - class UI extends PureComponent { static propTypes = { identity: identityContextPropShape, @@ -662,7 +657,7 @@ class UI extends PureComponent { {children} - {!minimalShell && } + {!minimalShell && !isRedesignEnabled() && } {layout !== 'mobile' && } {!disableHoverCards && } @@ -670,12 +665,6 @@ class UI extends PureComponent { - - {isRedesignEnabled() && ( - - - - )} ); diff --git a/app/javascript/mastodon/hooks/useIsLinkActive.ts b/app/javascript/mastodon/hooks/useIsLinkActive.ts new file mode 100644 index 00000000000..5a57c1b1970 --- /dev/null +++ b/app/javascript/mastodon/hooks/useIsLinkActive.ts @@ -0,0 +1,16 @@ +import { matchPath, useLocation } from 'react-router'; +import type { NavLinkProps } from 'react-router-dom'; + +export function useIsLinkActive(to?: NavLinkProps['to']) { + const location = useLocation(); + + if (!to) { + return false; + } + + const linkTarget = typeof to === 'function' ? to(location) : to; + + return !!matchPath(location.pathname, { + path: typeof linkTarget === 'string' ? linkTarget : linkTarget.pathname, + }); +} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 5c46dbfc5c2..ec8515bdf98 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -1411,6 +1411,7 @@ "tabs_bar.messages": "Messages", "tabs_bar.more": "More", "tabs_bar.notifications": "Notifications", + "tabs_bar.profile": "Profile", "tabs_bar.publish": "New Post", "tabs_bar.quick_links": "Quick links", "tabs_bar.saved": "Saved",