@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.
- package/build-module/components/app/index.js +3 -3
- package/build-module/components/app/router.js +68 -67
- package/build-module/components/app/router.js.map +2 -2
- package/build-module/components/app/use-route-title.js +50 -0
- package/build-module/components/app/use-route-title.js.map +7 -0
- package/build-module/components/canvas/back-button.js +8 -4
- package/build-module/components/canvas/back-button.js.map +2 -2
- package/build-module/components/canvas/index.js +1 -1
- package/build-module/components/canvas-renderer/index.js +35 -0
- package/build-module/components/canvas-renderer/index.js.map +7 -0
- package/build-module/components/navigation/drilldown-item/index.js +1 -1
- package/build-module/components/navigation/dropdown-item/index.js +3 -3
- package/build-module/components/navigation/index.js +6 -6
- package/build-module/components/navigation/navigation-item/index.js +4 -4
- package/build-module/components/navigation/navigation-item/index.js.map +1 -1
- package/build-module/components/navigation/router-link-item.js +1 -1
- package/build-module/components/navigation/use-sidebar-parent.js +3 -3
- package/build-module/components/root/index.js +276 -21
- package/build-module/components/root/index.js.map +2 -2
- package/build-module/components/root/single-page.js +181 -19
- package/build-module/components/root/single-page.js.map +2 -2
- package/build-module/components/save-button/index.js +1 -1
- package/build-module/components/save-panel/index.js +1 -1
- package/build-module/components/sidebar/index.js +3 -3
- package/build-module/components/site-hub/index.js +30 -12
- package/build-module/components/site-hub/index.js.map +2 -2
- package/build-module/components/site-icon/index.js +2 -2
- package/build-module/components/site-icon/index.js.map +1 -1
- package/build-module/components/site-icon-link/index.js +5 -5
- package/build-module/components/site-icon-link/index.js.map +1 -1
- package/build-module/components/user-theme-provider/index.js +33 -0
- package/build-module/components/user-theme-provider/index.js.map +7 -0
- package/build-module/index.js +298 -75
- package/build-module/index.js.map +4 -4
- package/build-module/store/index.js +3 -3
- package/build-style/style-rtl.css +95 -75
- package/build-style/style.css +95 -75
- package/build-style/view-transitions-rtl.css +199 -0
- package/build-style/view-transitions.css +199 -0
- package/build-types/components/app/router.d.ts.map +1 -1
- package/build-types/components/app/use-route-title.d.ts +8 -0
- package/build-types/components/app/use-route-title.d.ts.map +1 -0
- package/build-types/components/canvas-renderer/index.d.ts +27 -0
- package/build-types/components/canvas-renderer/index.d.ts.map +1 -0
- package/build-types/components/root/index.d.ts.map +1 -1
- package/build-types/components/root/single-page.d.ts.map +1 -1
- package/build-types/components/site-hub/index.d.ts.map +1 -1
- package/build-types/components/user-theme-provider/index.d.ts +6 -0
- package/build-types/components/user-theme-provider/index.d.ts.map +1 -0
- package/build-types/index.d.ts +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/store/index.d.ts +1 -1
- package/build-types/store/index.d.ts.map +1 -1
- package/build-types/store/types.d.ts +64 -7
- package/build-types/store/types.d.ts.map +1 -1
- package/package.json +30 -21
- package/src/components/app/router.tsx +93 -111
- package/src/components/app/use-route-title.ts +80 -0
- package/src/components/canvas/back-button.scss +7 -2
- package/src/components/canvas-renderer/index.tsx +72 -0
- package/src/components/navigation/navigation-item/style.scss +1 -1
- package/src/components/root/index.tsx +154 -27
- package/src/components/root/single-page.tsx +24 -10
- package/src/components/root/style.scss +123 -4
- package/src/components/site-hub/index.tsx +6 -1
- package/src/components/site-hub/style.scss +23 -8
- package/src/components/site-icon/style.scss +1 -1
- package/src/components/site-icon-link/style.scss +2 -2
- package/src/components/user-theme-provider/index.tsx +35 -0
- package/src/components/user-theme-provider/test/index.test.ts +11 -0
- package/src/index.tsx +1 -0
- package/src/store/types.ts +71 -7
- package/src/style.scss +19 -33
- package/src/view-transitions.scss +239 -0
- package/tsconfig.json +0 -26
- 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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
129
|
+
component: function RouteComponent() {
|
|
130
|
+
const { inspector: showInspector } =
|
|
131
|
+
useLoaderData( { from: route.path } ) ?? {};
|
|
132
|
+
|
|
158
133
|
return (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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 =
|
|
190
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
@@ -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 {
|
|
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
|
-
<
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
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
|
-
</
|
|
61
|
-
</
|
|
62
|
-
</
|
|
187
|
+
</UserThemeProvider>
|
|
188
|
+
</UserThemeProvider>
|
|
189
|
+
</SlotFillProvider>
|
|
63
190
|
);
|
|
64
191
|
}
|