@wordpress/boot 0.3.0 → 0.3.1-next.738bb1424.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/build-module/components/app/index.js +3 -3
  2. package/build-module/components/app/router.js +68 -67
  3. package/build-module/components/app/router.js.map +2 -2
  4. package/build-module/components/app/use-route-title.js +50 -0
  5. package/build-module/components/app/use-route-title.js.map +7 -0
  6. package/build-module/components/canvas/back-button.js +8 -4
  7. package/build-module/components/canvas/back-button.js.map +2 -2
  8. package/build-module/components/canvas/index.js +1 -1
  9. package/build-module/components/canvas-renderer/index.js +35 -0
  10. package/build-module/components/canvas-renderer/index.js.map +7 -0
  11. package/build-module/components/navigation/drilldown-item/index.js +1 -1
  12. package/build-module/components/navigation/dropdown-item/index.js +3 -3
  13. package/build-module/components/navigation/index.js +6 -6
  14. package/build-module/components/navigation/navigation-item/index.js +4 -4
  15. package/build-module/components/navigation/navigation-item/index.js.map +1 -1
  16. package/build-module/components/navigation/router-link-item.js +1 -1
  17. package/build-module/components/navigation/use-sidebar-parent.js +3 -3
  18. package/build-module/components/root/index.js +276 -21
  19. package/build-module/components/root/index.js.map +2 -2
  20. package/build-module/components/root/single-page.js +181 -19
  21. package/build-module/components/root/single-page.js.map +2 -2
  22. package/build-module/components/save-button/index.js +1 -1
  23. package/build-module/components/save-panel/index.js +1 -1
  24. package/build-module/components/sidebar/index.js +3 -3
  25. package/build-module/components/site-hub/index.js +30 -12
  26. package/build-module/components/site-hub/index.js.map +2 -2
  27. package/build-module/components/site-icon/index.js +2 -2
  28. package/build-module/components/site-icon/index.js.map +1 -1
  29. package/build-module/components/site-icon-link/index.js +5 -5
  30. package/build-module/components/site-icon-link/index.js.map +1 -1
  31. package/build-module/components/user-theme-provider/index.js +33 -0
  32. package/build-module/components/user-theme-provider/index.js.map +7 -0
  33. package/build-module/index.js +298 -75
  34. package/build-module/index.js.map +4 -4
  35. package/build-module/store/index.js +3 -3
  36. package/build-style/style-rtl.css +95 -75
  37. package/build-style/style.css +95 -75
  38. package/build-style/view-transitions-rtl.css +199 -0
  39. package/build-style/view-transitions.css +199 -0
  40. package/build-types/components/app/router.d.ts.map +1 -1
  41. package/build-types/components/app/use-route-title.d.ts +8 -0
  42. package/build-types/components/app/use-route-title.d.ts.map +1 -0
  43. package/build-types/components/canvas-renderer/index.d.ts +27 -0
  44. package/build-types/components/canvas-renderer/index.d.ts.map +1 -0
  45. package/build-types/components/root/index.d.ts.map +1 -1
  46. package/build-types/components/root/single-page.d.ts.map +1 -1
  47. package/build-types/components/site-hub/index.d.ts.map +1 -1
  48. package/build-types/components/user-theme-provider/index.d.ts +6 -0
  49. package/build-types/components/user-theme-provider/index.d.ts.map +1 -0
  50. package/build-types/index.d.ts +1 -0
  51. package/build-types/index.d.ts.map +1 -1
  52. package/build-types/store/index.d.ts +1 -1
  53. package/build-types/store/index.d.ts.map +1 -1
  54. package/build-types/store/types.d.ts +64 -7
  55. package/build-types/store/types.d.ts.map +1 -1
  56. package/package.json +30 -21
  57. package/src/components/app/router.tsx +93 -111
  58. package/src/components/app/use-route-title.ts +80 -0
  59. package/src/components/canvas/back-button.scss +7 -2
  60. package/src/components/canvas-renderer/index.tsx +72 -0
  61. package/src/components/navigation/navigation-item/style.scss +1 -1
  62. package/src/components/root/index.tsx +154 -27
  63. package/src/components/root/single-page.tsx +24 -10
  64. package/src/components/root/style.scss +123 -4
  65. package/src/components/site-hub/index.tsx +6 -1
  66. package/src/components/site-hub/style.scss +23 -8
  67. package/src/components/site-icon/style.scss +1 -1
  68. package/src/components/site-icon-link/style.scss +2 -2
  69. package/src/components/user-theme-provider/index.tsx +35 -0
  70. package/src/components/user-theme-provider/test/index.test.ts +11 -0
  71. package/src/index.tsx +1 -0
  72. package/src/store/types.ts +71 -7
  73. package/src/style.scss +19 -33
  74. package/src/view-transitions.scss +239 -0
  75. package/tsconfig.json +0 -26
  76. package/tsconfig.tsbuildinfo +0 -1
@@ -7,20 +7,21 @@ import type { ComponentType } from 'react';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { __ } from '@wordpress/i18n';
10
- import { useState, useEffect } from '@wordpress/element';
10
+ import { useMemo } from '@wordpress/element';
11
11
  import { Page } from '@wordpress/admin-ui';
12
12
  import {
13
13
  privateApis as routePrivateApis,
14
14
  type AnyRoute,
15
15
  } from '@wordpress/route';
16
+ import { resolveSelect } from '@wordpress/data';
17
+ import { store as coreStore } from '@wordpress/core-data';
16
18
 
17
19
  /**
18
20
  * Internal dependencies
19
21
  */
20
22
  import Root from '../root';
21
- import type { CanvasData, Route, RouteLoaderContext } from '../../store/types';
23
+ import type { Route, RouteConfig, RouteLoaderContext } from '../../store/types';
22
24
  import { unlock } from '../../lock-unlock';
23
- import Canvas from '../canvas';
24
25
 
25
26
  const {
26
27
  createLazyRoute,
@@ -30,7 +31,7 @@ const {
30
31
  RouterProvider,
31
32
  createBrowserHistory,
32
33
  parseHref,
33
- useMatches,
34
+ useLoaderData,
34
35
  } = unlock( routePrivateApis );
35
36
 
36
37
  // Not found component
@@ -44,51 +45,6 @@ function NotFoundComponent() {
44
45
  );
45
46
  }
46
47
 
47
- function RouteComponent( {
48
- stage: Stage,
49
- inspector: Inspector,
50
- canvas: CustomCanvas,
51
- }: {
52
- stage?: ComponentType;
53
- inspector?: ComponentType;
54
- canvas?: ComponentType;
55
- } ) {
56
- // Get canvas data from the current route's loader
57
- const matches = useMatches();
58
- const currentMatch = matches[ matches.length - 1 ];
59
- const canvasData = ( currentMatch?.loaderData as any )?.canvas as
60
- | CanvasData
61
- | null
62
- | undefined;
63
-
64
- return (
65
- <>
66
- { Stage && (
67
- <div className="boot-layout__stage">
68
- <Stage />
69
- </div>
70
- ) }
71
- { Inspector && (
72
- <div className="boot-layout__inspector">
73
- <Inspector />
74
- </div>
75
- ) }
76
- { /* Render custom canvas when canvas() returns null */ }
77
- { canvasData === null && CustomCanvas && (
78
- <div className="boot-layout__canvas">
79
- <CustomCanvas />
80
- </div>
81
- ) }
82
- { /* Render default canvas when canvas() returns CanvasData */ }
83
- { canvasData && (
84
- <div className="boot-layout__canvas">
85
- <Canvas canvas={ canvasData } />
86
- </div>
87
- ) }
88
- </>
89
- );
90
- }
91
-
92
48
  /**
93
49
  * Creates a TanStack route from a Route definition.
94
50
  *
@@ -96,52 +52,65 @@ function RouteComponent( {
96
52
  * @param parentRoute Parent route.
97
53
  * @return Tanstack Route.
98
54
  */
99
- async function createRouteFromDefinition(
100
- route: Route,
101
- parentRoute: AnyRoute
102
- ) {
103
- // Load route module for lifecycle functions if specified
104
- let routeConfig: {
105
- beforeLoad?: ( context: RouteLoaderContext ) => void | Promise< void >;
106
- loader?: ( context: RouteLoaderContext ) => Promise< unknown >;
107
- canvas?: ( context: RouteLoaderContext ) => Promise< any >;
108
- } = {};
109
-
110
- if ( route.route_module ) {
111
- const module = await import( route.route_module );
112
- routeConfig = module.route || {};
113
- }
114
-
55
+ function createRouteFromDefinition( route: Route, parentRoute: AnyRoute ) {
115
56
  // Create route without component initially
116
57
  let tanstackRoute = createRoute( {
117
58
  getParentRoute: () => parentRoute,
118
59
  path: route.path,
119
- beforeLoad: routeConfig.beforeLoad
120
- ? ( opts: any ) =>
121
- routeConfig.beforeLoad!( {
60
+ beforeLoad: async ( opts: any ) => {
61
+ // Import route module here (lazy)
62
+ if ( route.route_module ) {
63
+ const module = await import( route.route_module );
64
+ const routeConfig = module.route || {};
65
+
66
+ if ( routeConfig.beforeLoad ) {
67
+ return routeConfig.beforeLoad( {
122
68
  params: opts.params || {},
123
69
  search: opts.search || {},
124
- } )
125
- : undefined,
70
+ } );
71
+ }
72
+ }
73
+ },
126
74
  loader: async ( opts: any ) => {
75
+ // Import route module here (lazy)
76
+ let routeConfig: RouteConfig = {};
77
+ if ( route.route_module ) {
78
+ const module = await import( route.route_module );
79
+ routeConfig = module.route || {};
80
+ }
81
+
127
82
  const context: RouteLoaderContext = {
128
83
  params: opts.params || {},
129
84
  search: opts.deps || {},
130
85
  };
131
86
 
132
- // Call both loader and canvas functions if they exist
133
- const [ loaderData, canvasData ] = await Promise.all( [
87
+ const [ , loaderData, canvasData, titleData ] = await Promise.all( [
88
+ resolveSelect( coreStore ).getEntityRecord(
89
+ 'root',
90
+ '__unstableBase'
91
+ ),
134
92
  routeConfig.loader
135
93
  ? routeConfig.loader( context )
136
94
  : Promise.resolve( undefined ),
137
95
  routeConfig.canvas
138
96
  ? routeConfig.canvas( context )
139
97
  : Promise.resolve( undefined ),
98
+ routeConfig.title
99
+ ? routeConfig.title( context )
100
+ : Promise.resolve( undefined ),
140
101
  ] );
141
102
 
103
+ let inspector = true;
104
+ if ( routeConfig.inspector ) {
105
+ inspector = await routeConfig.inspector( context );
106
+ }
107
+
142
108
  return {
143
109
  ...( loaderData as any ),
144
110
  canvas: canvasData,
111
+ inspector,
112
+ title: titleData,
113
+ routeContentModule: route.content_module,
145
114
  };
146
115
  },
147
116
  loaderDeps: ( opts: any ) => opts.search,
@@ -153,14 +122,27 @@ async function createRouteFromDefinition(
153
122
  ? await import( route.content_module )
154
123
  : {};
155
124
 
125
+ const Stage = module.stage;
126
+ const Inspector = module.inspector;
127
+
156
128
  return createLazyRoute( route.path )( {
157
- component: function Component() {
129
+ component: function RouteComponent() {
130
+ const { inspector: showInspector } =
131
+ useLoaderData( { from: route.path } ) ?? {};
132
+
158
133
  return (
159
- <RouteComponent
160
- stage={ module.stage }
161
- inspector={ module.inspector }
162
- canvas={ module.canvas }
163
- />
134
+ <>
135
+ { Stage && (
136
+ <div className="boot-layout__stage">
137
+ <Stage />
138
+ </div>
139
+ ) }
140
+ { Inspector && showInspector && (
141
+ <div className="boot-layout__inspector">
142
+ <Inspector />
143
+ </div>
144
+ ) }
145
+ </>
164
146
  );
165
147
  },
166
148
  } );
@@ -176,7 +158,7 @@ async function createRouteFromDefinition(
176
158
  * @param rootComponent Root component to use for the router.
177
159
  * @return Router tree.
178
160
  */
179
- async function createRouteTree(
161
+ function createRouteTree(
180
162
  routes: Route[],
181
163
  rootComponent: ComponentType = Root
182
164
  ) {
@@ -185,9 +167,9 @@ async function createRouteTree(
185
167
  context: () => ( {} ),
186
168
  } );
187
169
 
188
- // Create routes from definitions
189
- const dynamicRoutes = await Promise.all(
190
- routes.map( ( route ) => createRouteFromDefinition( route, rootRoute ) )
170
+ // Create routes from definitions (now synchronous)
171
+ const dynamicRoutes = routes.map( ( route ) =>
172
+ createRouteFromDefinition( route, rootRoute )
191
173
  );
192
174
 
193
175
  return rootRoute.addChildren( dynamicRoutes );
@@ -219,36 +201,36 @@ export default function Router( {
219
201
  routes,
220
202
  rootComponent = Root,
221
203
  }: RouterProps ) {
222
- const [ router, setRouter ] = useState< any >( null );
223
-
224
- useEffect( () => {
225
- let cancelled = false;
226
-
227
- async function initializeRouter() {
228
- const history = createPathHistory();
229
- const routeTree = await createRouteTree( routes, rootComponent );
230
-
231
- if ( ! cancelled ) {
232
- const newRouter = createRouter( {
233
- history,
234
- routeTree,
235
- defaultPreload: 'intent',
236
- defaultNotFoundComponent: NotFoundComponent,
237
- } );
238
- setRouter( newRouter );
239
- }
240
- }
241
-
242
- initializeRouter();
243
-
244
- return () => {
245
- cancelled = true;
246
- };
204
+ const router = useMemo( () => {
205
+ const history = createPathHistory();
206
+ const routeTree = createRouteTree( routes, rootComponent );
207
+
208
+ return createRouter( {
209
+ history,
210
+ routeTree,
211
+ defaultPreload: 'intent',
212
+ defaultNotFoundComponent: NotFoundComponent,
213
+ defaultViewTransition: {
214
+ types: ( {
215
+ fromLocation,
216
+ }: {
217
+ fromLocation?: unknown;
218
+ toLocation: unknown;
219
+ pathChanged: boolean;
220
+ hrefChanged: boolean;
221
+ hashChanged: boolean;
222
+ } ) => {
223
+ // Disable view transition on initial navigation (no previous location)
224
+ if ( ! fromLocation ) {
225
+ return false;
226
+ }
227
+
228
+ // Enable with navigation type for subsequent navigations
229
+ return [ 'navigate' ];
230
+ },
231
+ },
232
+ } );
247
233
  }, [ routes, rootComponent ] );
248
234
 
249
- if ( ! router ) {
250
- return <div>Loading routes...</div>;
251
- }
252
-
253
235
  return <RouterProvider router={ router } />;
254
236
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useEffect, useRef } from '@wordpress/element';
5
+ import { useSelect } from '@wordpress/data';
6
+ import { store as coreStore, type UnstableBase } from '@wordpress/core-data';
7
+ import { __, sprintf } from '@wordpress/i18n';
8
+ import { speak } from '@wordpress/a11y';
9
+ import { decodeEntities } from '@wordpress/html-entities';
10
+ import { privateApis as routePrivateApis } from '@wordpress/route';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { unlock } from '../../lock-unlock';
16
+
17
+ const { useLocation, useMatches } = unlock( routePrivateApis );
18
+
19
+ /**
20
+ * Hook that manages document title updates based on route changes.
21
+ * Formats titles with WordPress conventions and announces them to screen readers.
22
+ *
23
+ * This hook should be called from the Root component to ensure it runs on every route.
24
+ */
25
+ export default function useRouteTitle() {
26
+ const location = useLocation();
27
+ const matches = useMatches();
28
+ const currentMatch = matches[ matches.length - 1 ];
29
+ const routeTitle = ( currentMatch?.loaderData as any )?.title as
30
+ | string
31
+ | undefined;
32
+
33
+ const siteTitle = useSelect(
34
+ ( select ) =>
35
+ select( coreStore ).getEntityRecord< UnstableBase >(
36
+ 'root',
37
+ '__unstableBase'
38
+ )?.name,
39
+ []
40
+ );
41
+
42
+ const isInitialLocationRef = useRef( true );
43
+
44
+ useEffect( () => {
45
+ isInitialLocationRef.current = false;
46
+ }, [ location ] );
47
+
48
+ useEffect( () => {
49
+ // Don't update or announce the title for initial page load.
50
+ if ( isInitialLocationRef.current ) {
51
+ return;
52
+ }
53
+
54
+ if (
55
+ routeTitle &&
56
+ typeof routeTitle === 'string' &&
57
+ siteTitle &&
58
+ typeof siteTitle === 'string'
59
+ ) {
60
+ // Decode entities for display
61
+ const decodedRouteTitle = decodeEntities( routeTitle );
62
+ const decodedSiteTitle = decodeEntities( siteTitle );
63
+
64
+ // Format title following WordPress admin conventions
65
+ const formattedTitle = sprintf(
66
+ /* translators: Admin document title. 1: Admin screen name, 2: Site name. */
67
+ __( '%1$s ‹ %2$s — WordPress' ),
68
+ decodedRouteTitle,
69
+ decodedSiteTitle
70
+ );
71
+
72
+ document.title = formattedTitle;
73
+
74
+ // Announce title on route change for screen readers.
75
+ if ( decodedRouteTitle ) {
76
+ speak( decodedRouteTitle, 'assertive' );
77
+ }
78
+ }
79
+ }, [ routeTitle, siteTitle, location ] );
80
+ }
@@ -32,9 +32,9 @@
32
32
 
33
33
  &:focus:not(:active) {
34
34
  outline:
35
- var(--wpds-border-width-focus) solid
35
+ var(--wpds-border-width-interactive-focus) solid
36
36
  var(--wpds-color-stroke-focus-brand);
37
- outline-offset: calc(-1 * var(--wpds-border-width-focus));
37
+ outline-offset: calc(-1 * var(--wpds-border-width-interactive-focus));
38
38
  }
39
39
  }
40
40
 
@@ -60,3 +60,8 @@
60
60
  backdrop-filter: saturate(180%) blur(15px);
61
61
  }
62
62
  }
63
+
64
+ // Remove the header slide-in animation so the back logo does not move.
65
+ .interface-interface-skeleton__header {
66
+ margin-top: 0 !important;
67
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useState, useEffect } from '@wordpress/element';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import Canvas from '../canvas';
10
+ import type { CanvasData } from '../../store/types';
11
+
12
+ interface CanvasRendererProps {
13
+ canvas: CanvasData | null | undefined;
14
+ routeContentModule?: string;
15
+ }
16
+
17
+ /**
18
+ * CanvasRenderer handles rendering of both default and custom canvas components.
19
+ * The logic here would have been way simpler if we just render the canvas within
20
+ * the RouteComponent like the other surfaces.
21
+ * The issue is that doing so forces the canvas to remount on every route change,
22
+ * which is not desirable for smooth transitions.
23
+ *
24
+ * - When canvas is undefined: No canvas is rendered
25
+ * - When canvas is null: Loads and renders custom canvas from contentModulePath
26
+ * - When canvas is CanvasData: Renders default Canvas component with editor
27
+ *
28
+ * This component is designed to be rendered at the Root level to prevent
29
+ * remounting when navigating between routes.
30
+ *
31
+ * @param props Component props
32
+ * @param props.canvas Canvas data from route loader
33
+ * @param props.routeContentModule Path to content module for custom canvas
34
+ * @return Canvas renderer
35
+ */
36
+ export default function CanvasRenderer( {
37
+ canvas,
38
+ routeContentModule,
39
+ }: CanvasRendererProps ) {
40
+ const [ CustomCanvas, setCustomCanvas ] = useState< any >( null );
41
+
42
+ useEffect( () => {
43
+ if ( canvas === null && routeContentModule ) {
44
+ import( routeContentModule )
45
+ .then( ( module ) => {
46
+ setCustomCanvas( () => module.canvas );
47
+ } )
48
+ .catch( ( error ) => {
49
+ // eslint-disable-next-line no-console
50
+ console.error( 'Failed to load custom canvas:', error );
51
+ } );
52
+ } else {
53
+ setCustomCanvas( null );
54
+ }
55
+ }, [ canvas, routeContentModule ] );
56
+
57
+ // No canvas
58
+ if ( canvas === undefined ) {
59
+ return null;
60
+ }
61
+
62
+ // Custom canvas
63
+ if ( canvas === null ) {
64
+ if ( ! CustomCanvas ) {
65
+ return null; // Still loading
66
+ }
67
+ return <CustomCanvas />;
68
+ }
69
+
70
+ // Default canvas
71
+ return <Canvas canvas={ canvas } />;
72
+ }
@@ -19,7 +19,7 @@
19
19
  }
20
20
 
21
21
  // Rounded focus ring
22
- border-radius: var(--wpds-border-radius-small, 2px);
22
+ border-radius: var(--wpds-border-radius-surface-sm, 2px);
23
23
 
24
24
  &.active,
25
25
  &:hover,
@@ -9,56 +9,183 @@ import clsx from 'clsx';
9
9
  import { privateApis as routePrivateApis } from '@wordpress/route';
10
10
  // @ts-expect-error Commands is not typed properly.
11
11
  import { CommandMenu } from '@wordpress/commands';
12
- import { privateApis as themePrivateApis } from '@wordpress/theme';
13
12
  import { EditorSnackbars } from '@wordpress/editor';
13
+ import { useViewportMatch, useReducedMotion } from '@wordpress/compose';
14
+ import {
15
+ __unstableMotion as motion,
16
+ __unstableAnimatePresence as AnimatePresence,
17
+ Button,
18
+ SlotFillProvider,
19
+ } from '@wordpress/components';
20
+ import { menu } from '@wordpress/icons';
21
+ import { useState, useEffect } from '@wordpress/element';
22
+ import { __ } from '@wordpress/i18n';
23
+ import { Page } from '@wordpress/admin-ui';
14
24
 
15
25
  /**
16
26
  * Internal dependencies
17
27
  */
18
28
  import Sidebar from '../sidebar';
19
29
  import SavePanel from '../save-panel';
30
+ import CanvasRenderer from '../canvas-renderer';
31
+ import useRouteTitle from '../app/use-route-title';
20
32
  import { unlock } from '../../lock-unlock';
21
33
  import type { CanvasData } from '../../store/types';
22
34
  import './style.scss';
35
+ import { UserThemeProvider } from '../user-theme-provider';
23
36
 
24
- const { ThemeProvider } = unlock( themePrivateApis );
25
- const { useMatches, Outlet } = unlock( routePrivateApis );
37
+ const { useLocation, useMatches, Outlet } = unlock( routePrivateApis );
26
38
 
27
39
  export default function Root() {
28
40
  const matches = useMatches();
41
+ const location = useLocation();
29
42
  const currentMatch = matches[ matches.length - 1 ];
30
43
  const canvas = ( currentMatch?.loaderData as any )?.canvas as
31
44
  | CanvasData
32
45
  | null
33
46
  | undefined;
47
+ const routeContentModule = ( currentMatch?.loaderData as any )
48
+ ?.routeContentModule as string | undefined;
34
49
  const isFullScreen = canvas && ! canvas.isPreview;
35
50
 
51
+ useRouteTitle();
52
+
53
+ // Mobile sidebar state
54
+ const isMobileViewport = useViewportMatch( 'medium', '<' );
55
+ const [ isMobileSidebarOpen, setIsMobileSidebarOpen ] = useState( false );
56
+ const disableMotion = useReducedMotion();
57
+ // Close mobile sidebar on viewport resize and path change
58
+ useEffect( () => {
59
+ setIsMobileSidebarOpen( false );
60
+ }, [ location.pathname, isMobileViewport ] );
61
+
36
62
  return (
37
- <ThemeProvider isRoot color={ { bg: '#f8f8f8', primary: '#3858e9' } }>
38
- <ThemeProvider color={ { bg: '#1d2327', primary: '#3858e9' } }>
39
- <div
40
- className={ clsx( 'boot-layout', {
41
- 'has-canvas': !! canvas || canvas === null,
42
- 'has-full-canvas': isFullScreen,
43
- } ) }
44
- >
45
- <CommandMenu />
46
- <SavePanel />
47
- <EditorSnackbars />
48
- { ! isFullScreen && (
49
- <div className="boot-layout__sidebar">
50
- <Sidebar />
63
+ <SlotFillProvider>
64
+ <UserThemeProvider isRoot color={ { bg: '#f8f8f8' } }>
65
+ <UserThemeProvider color={ { bg: '#1d2327' } }>
66
+ <div
67
+ className={ clsx( 'boot-layout', {
68
+ 'has-canvas': !! canvas || canvas === null,
69
+ 'has-full-canvas': isFullScreen,
70
+ } ) }
71
+ >
72
+ <CommandMenu />
73
+ <SavePanel />
74
+ <EditorSnackbars />
75
+ { isMobileViewport && (
76
+ <Page.SidebarToggleFill>
77
+ <Button
78
+ icon={ menu }
79
+ onClick={ () =>
80
+ setIsMobileSidebarOpen( true )
81
+ }
82
+ label={ __( 'Open navigation panel' ) }
83
+ size="compact"
84
+ />
85
+ </Page.SidebarToggleFill>
86
+ ) }
87
+ { /* Mobile Sidebar Backdrop */ }
88
+ <AnimatePresence>
89
+ { isMobileViewport &&
90
+ isMobileSidebarOpen &&
91
+ ! isFullScreen && (
92
+ <motion.div
93
+ initial={ { opacity: 0 } }
94
+ animate={ { opacity: 1 } }
95
+ exit={ { opacity: 0 } }
96
+ transition={ {
97
+ type: 'tween',
98
+ duration: disableMotion ? 0 : 0.2,
99
+ ease: 'easeOut',
100
+ } }
101
+ className="boot-layout__sidebar-backdrop"
102
+ onClick={ () =>
103
+ setIsMobileSidebarOpen( false )
104
+ }
105
+ onKeyDown={ ( event ) => {
106
+ if ( event.key === 'Escape' ) {
107
+ setIsMobileSidebarOpen( false );
108
+ }
109
+ } }
110
+ role="button"
111
+ tabIndex={ -1 }
112
+ aria-label={ __(
113
+ 'Close navigation panel'
114
+ ) }
115
+ />
116
+ ) }
117
+ </AnimatePresence>
118
+ { /* Mobile Sidebar */ }
119
+ <AnimatePresence>
120
+ { isMobileViewport &&
121
+ isMobileSidebarOpen &&
122
+ ! isFullScreen && (
123
+ <motion.div
124
+ initial={ { x: '-100%' } }
125
+ animate={ { x: 0 } }
126
+ exit={ { x: '-100%' } }
127
+ transition={ {
128
+ type: 'tween',
129
+ duration: disableMotion ? 0 : 0.2,
130
+ ease: 'easeOut',
131
+ } }
132
+ className="boot-layout__sidebar is-mobile"
133
+ >
134
+ <Sidebar />
135
+ </motion.div>
136
+ ) }
137
+ </AnimatePresence>
138
+ { /* Desktop Sidebar */ }
139
+ { ! isMobileViewport && ! isFullScreen && (
140
+ <div className="boot-layout__sidebar">
141
+ <Sidebar />
142
+ </div>
143
+ ) }
144
+ <div className="boot-layout__surfaces">
145
+ <UserThemeProvider color={ { bg: '#ffffff' } }>
146
+ <Outlet />
147
+ { /* Render Canvas in Root to prevent remounting on route changes */ }
148
+ { ( canvas || canvas === null ) && (
149
+ <div
150
+ className={ clsx(
151
+ 'boot-layout__canvas',
152
+ {
153
+ 'has-mobile-drawer':
154
+ canvas?.isPreview &&
155
+ isMobileViewport,
156
+ }
157
+ ) }
158
+ >
159
+ { canvas?.isPreview &&
160
+ isMobileViewport && (
161
+ <div className="boot-layout__mobile-sidebar-drawer">
162
+ <Button
163
+ icon={ menu }
164
+ onClick={ () =>
165
+ setIsMobileSidebarOpen(
166
+ true
167
+ )
168
+ }
169
+ label={ __(
170
+ 'Open navigation panel'
171
+ ) }
172
+ size="compact"
173
+ />
174
+ </div>
175
+ ) }
176
+ <CanvasRenderer
177
+ canvas={ canvas }
178
+ routeContentModule={
179
+ routeContentModule
180
+ }
181
+ />
182
+ </div>
183
+ ) }
184
+ </UserThemeProvider>
51
185
  </div>
52
- ) }
53
- <div className="boot-layout__surfaces">
54
- <ThemeProvider
55
- color={ { bg: '#ffffff', primary: '#3858e9' } }
56
- >
57
- <Outlet />
58
- </ThemeProvider>
59
186
  </div>
60
- </div>
61
- </ThemeProvider>
62
- </ThemeProvider>
187
+ </UserThemeProvider>
188
+ </UserThemeProvider>
189
+ </SlotFillProvider>
63
190
  );
64
191
  }