Skip to content

Commit 172d53a

Browse files
committed
Add mobile layout with animated sidebar drawer
Implements mobile-responsive layout following @wordpress/boot architecture: - SiteHubMobile header with back navigation and hamburger menu - Animated sidebar drawer that slides in from left - Backdrop overlay for dismissing sidebar - Full-screen stage/inspector with z-index stacking on mobile - Mobile header shows current navigation context from menuItems - Removes panel padding and rounded corners on mobile - Respects reduced motion preferences
1 parent 33463d0 commit 172d53a

File tree

8 files changed

+288
-42
lines changed

8 files changed

+288
-42
lines changed

src/social-web/components/layout/index.tsx

Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,63 @@
55
* - Sidebar (300px fixed) - Navigation
66
* - Stage (flexible) - Main content
77
* - Inspector (380px fixed, optional) - Detail panel
8+
*
9+
* On mobile (<782px), shows:
10+
* - SiteHubMobile header with back button and menu toggle
11+
* - Animated sidebar drawer (slides in from left)
12+
* - Full-screen content area (stage or mobile component)
13+
*
14+
* Follows @wordpress/boot architecture patterns for future compatibility.
15+
*/
16+
17+
/**
18+
* External dependencies
819
*/
20+
import { ParsedLocation } from '@tanstack/react-router';
921

22+
/**
23+
* WordPress dependencies
24+
*/
1025
import { CommandMenu } from '@wordpress/commands';
11-
import { SnackbarList } from '@wordpress/components';
26+
import {
27+
SnackbarList,
28+
__unstableMotion as motion,
29+
__unstableAnimatePresence as AnimatePresence,
30+
} from '@wordpress/components';
1231
import { useSelect, useDispatch } from '@wordpress/data';
32+
import { useState, useEffect, useMemo } from '@wordpress/element';
1333
import { store as noticesStore } from '@wordpress/notices';
14-
import { Outlet } from '../../router';
15-
import Sidebar from '../sidebar';
34+
import { useViewportMatch, useReducedMotion } from '@wordpress/compose';
35+
import { __ } from '@wordpress/i18n';
36+
37+
/**
38+
* Internal dependencies
39+
*/
40+
import { Outlet, useLocation } from '../../router';
41+
import Sidebar, { MenuItemConfig, menuItems } from '../sidebar';
42+
import { SiteHubMobile } from '../site-hub';
1643
import './style.scss';
44+
import { KeyboardEventHandler } from 'react';
1745

1846
export function Layout() {
47+
const isMobileViewport: boolean = useViewportMatch( 'medium', '<' );
48+
const location: ParsedLocation< any > = useLocation();
49+
const disableMotion: boolean = useReducedMotion();
50+
const [ isMobileSidebarOpen, setIsMobileSidebarOpen ] = useState( false );
51+
52+
// Get the current page title from menu items based on route
53+
const currentTitle: string = useMemo( () => {
54+
const menuItem: MenuItemConfig = menuItems.find(
55+
( item: MenuItemConfig ): boolean => item.path === location.pathname
56+
);
57+
return menuItem?.label || __( 'Social Web', 'activitypub' );
58+
}, [ location.pathname ] );
59+
60+
// Auto-close sidebar on navigation or viewport change
61+
useEffect( () => {
62+
setIsMobileSidebarOpen( false );
63+
}, [ location.pathname, isMobileViewport ] );
64+
1965
// Get notices for the snackbar
2066
const notices = useSelect( ( select ) => {
2167
const store = select( noticesStore ) as any;
@@ -26,15 +72,69 @@ export function Layout() {
2672
return (
2773
<div className="app-layout">
2874
<CommandMenu />
29-
<div className="app-content">
30-
{ /* Sidebar - 240px fixed width (no Panel wrapper, stays dark) */ }
31-
<div className="sidebar-region">
32-
<Sidebar />
75+
76+
{ /* Mobile: Backdrop for sidebar drawer */ }
77+
<AnimatePresence>
78+
{ isMobileViewport && isMobileSidebarOpen && (
79+
<motion.div
80+
className="sidebar-backdrop"
81+
initial={ { opacity: 0 } }
82+
animate={ { opacity: 1 } }
83+
exit={ { opacity: 0 } }
84+
transition={ {
85+
type: 'tween',
86+
duration: disableMotion ? 0 : 0.2,
87+
ease: 'easeOut',
88+
} }
89+
onClick={ () => setIsMobileSidebarOpen( false ) }
90+
onKeyDown={ ( event ) => {
91+
if ( event.key === 'Escape' ) {
92+
setIsMobileSidebarOpen( false );
93+
}
94+
} }
95+
role="button"
96+
tabIndex={ -1 }
97+
aria-label="Close menu"
98+
/>
99+
) }
100+
</AnimatePresence>
101+
102+
{ /* Mobile: Animated sidebar drawer */ }
103+
<AnimatePresence>
104+
{ isMobileViewport && isMobileSidebarOpen && (
105+
<motion.div
106+
className="sidebar-region is-mobile"
107+
initial={ { x: '-100%' } }
108+
animate={ { x: 0 } }
109+
exit={ { x: '-100%' } }
110+
transition={ {
111+
type: 'tween',
112+
duration: disableMotion ? 0 : 0.2,
113+
ease: 'easeOut',
114+
} }
115+
>
116+
<Sidebar />
117+
</motion.div>
118+
) }
119+
</AnimatePresence>
120+
121+
{ /* Desktop: Static sidebar + content */ }
122+
{ ! isMobileViewport && (
123+
<div className="app-content">
124+
<div className="sidebar-region">
125+
<Sidebar />
126+
</div>
127+
<Outlet />
33128
</div>
129+
) }
34130

35-
{ /* Route content (stage + inspector) rendered via Outlet */ }
36-
<Outlet />
37-
</div>
131+
{ /* Mobile: Header + content */ }
132+
{ isMobileViewport && (
133+
<div className="app-content is-mobile">
134+
<SiteHubMobile title={ currentTitle } onMenuClick={ () => setIsMobileSidebarOpen( true ) } />
135+
<Outlet />
136+
</div>
137+
) }
38138

39139
<SnackbarList notices={ notices } onRemove={ removeNotice } />
40140
</div>

src/social-web/components/layout/style.scss

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010
height: 100%;
1111
background-color: var(--wpds-color-bg-surface-neutral-weak, #1e1e1e);
1212
overflow: hidden;
13+
isolation: isolate; // Create stacking context for z-index
14+
}
15+
16+
// Backdrop for mobile sidebar drawer
17+
.sidebar-backdrop {
18+
position: fixed;
19+
inset: 0;
20+
background-color: rgba(0, 0, 0, 0.5);
21+
z-index: 100002;
22+
cursor: pointer;
1323
}
1424

1525
.app-content {
@@ -19,28 +29,81 @@
1929
display: flex;
2030
background-color: var(--wpds-color-bg-surface-neutral-weak, #1e1e1e);
2131
overflow: hidden;
32+
33+
// Mobile: stack content vertically
34+
&.is-mobile {
35+
flex-direction: column;
36+
}
2237
}
2338

2439
.sidebar-region {
2540
flex-shrink: 0;
2641
width: 300px;
2742
display: flex;
2843
flex-direction: column;
44+
height: 100%;
45+
46+
// Mobile drawer styles
47+
&.is-mobile {
48+
position: fixed;
49+
top: 0;
50+
bottom: 0;
51+
left: 0;
52+
width: 300px;
53+
max-width: 85vw;
54+
z-index: 100003;
55+
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.3);
56+
background-color: var(--wpds-color-bg-surface-neutral-weak, #1e1e1e);
57+
}
2958
}
3059

31-
.stage-region {
32-
flex-grow: 1;
33-
min-width: 0;
60+
// Stage and inspector regions
61+
// Mobile: full-screen fixed positioning with z-index stacking (inspector overlays stage)
62+
// Desktop: side-by-side flex layout
63+
.stage-region,
64+
.inspector-region {
3465
display: flex;
3566
flex-direction: column;
3667
overflow: hidden;
68+
69+
// Mobile: full-screen fixed positioning (below mobile header)
70+
position: fixed;
71+
top: 56px; // Height of SiteHubMobile header
72+
left: 0;
73+
right: 0;
74+
bottom: 0;
75+
width: 100vw;
76+
height: calc(100vh - 56px);
77+
78+
// Desktop: static side-by-side layout
79+
@media (min-width: 782px) {
80+
position: static;
81+
top: auto;
82+
left: auto;
83+
right: auto;
84+
bottom: auto;
85+
width: auto;
86+
height: auto;
87+
}
88+
}
89+
90+
.stage-region {
91+
z-index: 2;
92+
93+
@media (min-width: 782px) {
94+
z-index: auto;
95+
flex: 1 1 auto;
96+
min-width: 0;
97+
}
3798
}
3899

39100
.inspector-region {
40-
width: var(--sw-inspector-width);
41-
flex-shrink: 0;
42-
display: flex;
43-
flex-direction: column;
101+
z-index: 3; // Stacks on top of stage on mobile
102+
103+
@media (min-width: 782px) {
104+
z-index: auto;
105+
flex: 0 0 var(--sw-inspector-width, 380px);
106+
}
44107
}
45108

46109
// Position the SnackbarList at the bottom left of the viewport
@@ -51,18 +114,8 @@
51114
z-index: 100000; // High z-index to ensure it appears above other content
52115
}
53116

54-
// Responsive
117+
// Mobile snackbar adjustments
55118
@media (max-width: 782px) {
56-
.app-content {
57-
flex-direction: column;
58-
}
59-
60-
.sidebar-region,
61-
.inspector-region {
62-
width: 100%;
63-
}
64-
65-
// Adjust snackbar position on mobile
66119
.components-snackbar-list {
67120
left: 10px;
68121
right: 10px;
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
.panel {
2-
margin: var(--wpds-spacing-40, 16px) var(--wpds-spacing-40, 16px) var(--wpds-spacing-40, 16px) 0;
32
flex: 1;
43
min-height: 0;
54
display: flex;
65
flex-direction: column;
6+
7+
// Mobile: no margin (full-screen content)
8+
margin: 0;
9+
10+
// Desktop: restore margin
11+
@media (min-width: 782px) {
12+
margin: var(--wpds-spacing-40, 16px) var(--wpds-spacing-40, 16px) var(--wpds-spacing-40, 16px) 0;
13+
}
714
}

src/social-web/components/sidebar/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ import './style.scss';
2828
* Menu item configuration for sidebar navigation.
2929
* Each item maps to a route path.
3030
*/
31-
interface MenuItemConfig {
31+
export interface MenuItemConfig {
3232
id: string;
3333
path: string;
3434
label: string;
3535
icon: typeof postList;
3636
}
3737

38-
const menuItems: MenuItemConfig[] = [ { id: 'feed', path: '/', label: __( 'Feed', 'activitypub' ), icon: postList } ];
38+
export const menuItems: MenuItemConfig[] = [
39+
{ id: 'feed', path: '/', label: __( 'Feed', 'activitypub' ), icon: postList },
40+
];
3941

4042
export default function Sidebar(): JSX.Element {
4143
const { adminUrl } = useSettings();

src/social-web/components/site-hub/index.tsx

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
/**
22
* Site Hub Component
33
*
4-
* Displays site icon, title, and command palette toggle
4+
* Displays site icon, title, and command palette toggle.
5+
* SiteHubMobile provides a mobile-specific header with back navigation and menu button.
56
*/
67

78
/**
89
* WordPress dependencies
910
*/
1011
import { useSelect, useDispatch } from '@wordpress/data';
1112
import { Button, __experimentalHStack as HStack, VisuallyHidden } from '@wordpress/components';
12-
import { __ } from '@wordpress/i18n';
13+
import { __, isRTL } from '@wordpress/i18n';
1314
import { store as coreStore } from '@wordpress/core-data';
1415
import { decodeEntities } from '@wordpress/html-entities';
15-
import { search } from '@wordpress/icons';
16+
import { search, chevronLeft, chevronRight, menu } from '@wordpress/icons';
1617
import { store as commandsStore } from '@wordpress/commands';
1718
import { displayShortcut } from '@wordpress/keycodes';
1819
import { filterURLForDisplay } from '@wordpress/url';
20+
import { forwardRef } from '@wordpress/element';
1921
import type { UnstableBase } from '@wordpress/core-data';
2022

2123
/**
2224
* Internal dependencies
2325
*/
2426
import SiteIcon from '../site-icon';
2527
import './style.scss';
28+
import { ForwardedRef, ForwardRefExoticComponent } from 'react';
2629

2730
function SiteHub() {
2831
const { homeUrl, siteTitle } = useSelect( ( select ) => {
@@ -82,4 +85,47 @@ function SiteHub() {
8285
);
8386
}
8487

88+
/**
89+
* Mobile Site Hub props.
90+
*/
91+
interface SiteHubMobileProps {
92+
onMenuClick: () => void;
93+
title?: string;
94+
}
95+
96+
/**
97+
* Mobile Site Hub Component
98+
*
99+
* Provides a mobile-specific header with:
100+
* - Back button (chevron) that navigates to dashboard
101+
* - Title showing the current navigation context
102+
* - Menu button (hamburger) to open sidebar drawer
103+
*/
104+
export const SiteHubMobile: ForwardRefExoticComponent< SiteHubMobileProps > = forwardRef<
105+
HTMLDivElement,
106+
SiteHubMobileProps
107+
>( function SiteHubMobile( { onMenuClick, title }: SiteHubMobileProps, ref: ForwardedRef< HTMLDivElement > ) {
108+
return (
109+
<div className="site-hub-mobile" ref={ ref }>
110+
<HStack spacing={ 2 } justify="flex-start">
111+
<Button
112+
icon={ isRTL() ? chevronRight : chevronLeft }
113+
href="/wp-admin/"
114+
label={ __( 'Go to the Dashboard', 'activitypub' ) }
115+
className="site-hub-mobile__button"
116+
size="compact"
117+
/>
118+
<span className="site-hub-mobile__title">{ title || __( 'Social Web', 'activitypub' ) }</span>
119+
</HStack>
120+
<Button
121+
icon={ menu }
122+
onClick={ onMenuClick }
123+
label={ __( 'Open menu', 'activitypub' ) }
124+
className="site-hub-mobile__button"
125+
size="compact"
126+
/>
127+
</div>
128+
);
129+
} );
130+
85131
export default SiteHub;

0 commit comments

Comments
 (0)