@wordpress/boot 0.3.1-next.8b30e05b0.0 → 0.4.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 (44) hide show
  1. package/build-module/components/app/router.js +25 -21
  2. package/build-module/components/app/router.js.map +2 -2
  3. package/build-module/components/app/use-route-title.js +50 -0
  4. package/build-module/components/app/use-route-title.js.map +7 -0
  5. package/build-module/components/canvas/back-button.js +3 -3
  6. package/build-module/components/canvas/back-button.js.map +1 -1
  7. package/build-module/components/navigation/navigation-item/index.js +2 -2
  8. package/build-module/components/navigation/navigation-item/index.js.map +1 -1
  9. package/build-module/components/root/index.js +291 -37
  10. package/build-module/components/root/index.js.map +2 -2
  11. package/build-module/components/root/single-page.js +164 -7
  12. package/build-module/components/root/single-page.js.map +2 -2
  13. package/build-module/components/site-hub/index.js +3 -3
  14. package/build-module/components/site-hub/index.js.map +1 -1
  15. package/build-module/components/site-icon/index.js +2 -2
  16. package/build-module/components/site-icon/index.js.map +1 -1
  17. package/build-module/components/site-icon-link/index.js +3 -3
  18. package/build-module/components/site-icon-link/index.js.map +1 -1
  19. package/build-module/index.js +108 -46
  20. package/build-module/index.js.map +2 -2
  21. package/build-style/style-rtl.css +107 -45
  22. package/build-style/style.css +107 -45
  23. package/build-types/components/app/router.d.ts.map +1 -1
  24. package/build-types/components/app/use-route-title.d.ts +8 -0
  25. package/build-types/components/app/use-route-title.d.ts.map +1 -0
  26. package/build-types/components/root/index.d.ts.map +1 -1
  27. package/build-types/components/root/single-page.d.ts.map +1 -1
  28. package/build-types/store/types.d.ts +64 -7
  29. package/build-types/store/types.d.ts.map +1 -1
  30. package/package.json +22 -21
  31. package/src/components/app/router.tsx +39 -38
  32. package/src/components/app/use-route-title.ts +80 -0
  33. package/src/components/canvas/back-button.scss +2 -2
  34. package/src/components/navigation/navigation-item/style.scss +1 -1
  35. package/src/components/root/index.tsx +148 -31
  36. package/src/components/root/single-page.tsx +3 -0
  37. package/src/components/root/style.scss +122 -4
  38. package/src/components/site-hub/style.scss +2 -2
  39. package/src/components/site-icon/style.scss +1 -1
  40. package/src/components/site-icon-link/style.scss +2 -2
  41. package/src/store/types.ts +71 -7
  42. package/src/style.scss +1 -0
  43. package/tsconfig.json +1 -0
  44. package/tsconfig.tsbuildinfo +1 -1
@@ -55,6 +55,68 @@ export interface CanvasData {
55
55
  */
56
56
  editLink?: string;
57
57
  }
58
+ /**
59
+ * Route lifecycle configuration exported from route_module.
60
+ * The module should export a named `route` object with these optional functions.
61
+ */
62
+ export interface RouteConfig {
63
+ /**
64
+ * Pre-navigation hook for authentication, validation, or redirects.
65
+ * Called before the route is loaded.
66
+ */
67
+ beforeLoad?: (context: RouteLoaderContext) => void | Promise<void>;
68
+ /**
69
+ * Data preloading function.
70
+ * Called when the route is being loaded.
71
+ */
72
+ loader?: (context: RouteLoaderContext) => Promise<unknown>;
73
+ /**
74
+ * Function that returns canvas data for rendering.
75
+ * - Returns CanvasData to use default editor canvas
76
+ * - Returns null to use custom canvas component from content_module
77
+ * - Returns undefined to show no canvas
78
+ */
79
+ canvas?: (context: RouteLoaderContext) => Promise<CanvasData | null | undefined>;
80
+ /**
81
+ * Function that determines whether to show the inspector panel.
82
+ * When not defined, defaults to true (always show inspector if component exists).
83
+ * When it returns false, the inspector is hidden even if an inspector component is exported.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * export const route = {
88
+ * inspector: ({ search }) => {
89
+ * // Only show inspector when items are selected
90
+ * return search.selectedIds?.length > 0;
91
+ * },
92
+ * };
93
+ * ```
94
+ */
95
+ inspector?: (context: RouteLoaderContext) => boolean | Promise<boolean>;
96
+ /**
97
+ * Function that returns the document title for the route.
98
+ * The returned title will be formatted as: "{title} ‹ {siteTitle} — WordPress"
99
+ * and announced to screen readers for accessibility.
100
+ *
101
+ * @param context Route context with params and search
102
+ * @return The document title string or a Promise resolving to a string
103
+ *
104
+ * @example
105
+ * ```tsx
106
+ * export const route = {
107
+ * title: async ({ params }) => {
108
+ * const post = await resolveSelect(coreStore).getEntityRecord(
109
+ * 'postType',
110
+ * params.type,
111
+ * params.id
112
+ * );
113
+ * return post?.title?.rendered || 'Edit Post';
114
+ * },
115
+ * };
116
+ * ```
117
+ */
118
+ title?: (context: RouteLoaderContext) => string | Promise<string>;
119
+ }
58
120
  /**
59
121
  * Route configuration interface.
60
122
  * Routes specify content_module for surfaces and optionally route_module for lifecycle functions.
@@ -75,13 +137,8 @@ export interface Route {
75
137
  content_module?: string;
76
138
  /**
77
139
  * Module path for route lifecycle functions.
78
- * The module should export a named export `route` containing:
79
- * - beforeLoad?: Pre-navigation hook (authentication, validation, redirects)
80
- * - loader?: Data preloading function
81
- * - canvas?: Function that returns canvas data for rendering
82
- * - Returns CanvasData to use default editor canvas
83
- * - Returns null to use custom canvas component from content_module
84
- * - Returns undefined to show no canvas
140
+ * The module should export a named `route` object implementing RouteConfig.
141
+ * @see RouteConfig for available lifecycle functions.
85
142
  */
86
143
  route_module?: string;
87
144
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,GAAG,CAAC,OAAO,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;CACvC;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,SAAS,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,MAAM,CAAE,MAAM,EAAE,MAAM,CAAE,CAAC;IACjC,MAAM,EAAE,MAAM,CAAE,MAAM,EAAE,OAAO,CAAE,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,KAAK;IACrB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACrB,SAAS,EAAE,MAAM,CAAE,MAAM,EAAE,QAAQ,CAAE,CAAC;IACtC,MAAM,EAAE,KAAK,EAAE,CAAC;CAChB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,GAAG,CAAC,OAAO,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;CACvC;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,SAAS,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,MAAM,CAAE,MAAM,EAAE,MAAM,CAAE,CAAC;IACjC,MAAM,EAAE,MAAM,CAAE,MAAM,EAAE,OAAO,CAAE,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAE,OAAO,EAAE,kBAAkB,KAAM,IAAI,GAAG,OAAO,CAAE,IAAI,CAAE,CAAC;IAEvE;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAE,OAAO,EAAE,kBAAkB,KAAM,OAAO,CAAE,OAAO,CAAE,CAAC;IAE/D;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CACR,OAAO,EAAE,kBAAkB,KACvB,OAAO,CAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAE,CAAC;IAE9C;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,CAAE,OAAO,EAAE,kBAAkB,KAAM,OAAO,GAAG,OAAO,CAAE,OAAO,CAAE,CAAC;IAE5E;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAK,CAAC,EAAE,CAAE,OAAO,EAAE,kBAAkB,KAAM,MAAM,GAAG,OAAO,CAAE,MAAM,CAAE,CAAC;CACtE;AAED;;;GAGG;AACH,MAAM,WAAW,KAAK;IACrB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACrB,SAAS,EAAE,MAAM,CAAE,MAAM,EAAE,QAAQ,CAAE,CAAC;IACtC,MAAM,EAAE,KAAK,EAAE,CAAC;CAChB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/boot",
3
- "version": "0.3.1-next.8b30e05b0.0",
3
+ "version": "0.4.0",
4
4
  "description": "Minimal boot package for WordPress admin pages.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -34,25 +34,26 @@
34
34
  "wpScriptModuleExports": "./build-module/index.js",
35
35
  "types": "build-types",
36
36
  "dependencies": {
37
- "@wordpress/admin-ui": "^1.4.1-next.8b30e05b0.0",
38
- "@wordpress/commands": "^1.36.1-next.8b30e05b0.0",
39
- "@wordpress/components": "^30.9.1-next.8b30e05b0.0",
40
- "@wordpress/compose": "^7.36.1-next.8b30e05b0.0",
41
- "@wordpress/core-data": "^7.36.1-next.8b30e05b0.0",
42
- "@wordpress/data": "^10.36.1-next.8b30e05b0.0",
43
- "@wordpress/editor": "^14.36.1-next.8b30e05b0.0",
44
- "@wordpress/element": "^6.36.1-next.8b30e05b0.0",
45
- "@wordpress/html-entities": "^4.36.1-next.8b30e05b0.0",
46
- "@wordpress/i18n": "^6.9.1-next.8b30e05b0.0",
47
- "@wordpress/icons": "^11.3.1-next.8b30e05b0.0",
48
- "@wordpress/keyboard-shortcuts": "^5.36.1-next.8b30e05b0.0",
49
- "@wordpress/keycodes": "^4.36.1-next.8b30e05b0.0",
50
- "@wordpress/lazy-editor": "^1.2.1-next.8b30e05b0.0",
51
- "@wordpress/primitives": "^4.36.1-next.8b30e05b0.0",
52
- "@wordpress/private-apis": "^1.36.1-next.8b30e05b0.0",
53
- "@wordpress/route": "^0.2.1-next.8b30e05b0.0",
54
- "@wordpress/theme": "^0.3.1-next.8b30e05b0.0",
55
- "@wordpress/url": "^4.36.1-next.8b30e05b0.0",
37
+ "@wordpress/a11y": "^4.37.0",
38
+ "@wordpress/admin-ui": "^1.5.0",
39
+ "@wordpress/commands": "^1.37.0",
40
+ "@wordpress/components": "^31.0.0",
41
+ "@wordpress/compose": "^7.37.0",
42
+ "@wordpress/core-data": "^7.37.0",
43
+ "@wordpress/data": "^10.37.0",
44
+ "@wordpress/editor": "^14.37.0",
45
+ "@wordpress/element": "^6.37.0",
46
+ "@wordpress/html-entities": "^4.37.0",
47
+ "@wordpress/i18n": "^6.10.0",
48
+ "@wordpress/icons": "^11.4.0",
49
+ "@wordpress/keyboard-shortcuts": "^5.37.0",
50
+ "@wordpress/keycodes": "^4.37.0",
51
+ "@wordpress/lazy-editor": "^1.3.0",
52
+ "@wordpress/primitives": "^4.37.0",
53
+ "@wordpress/private-apis": "^1.37.0",
54
+ "@wordpress/route": "^0.3.0",
55
+ "@wordpress/theme": "^0.4.0",
56
+ "@wordpress/url": "^4.37.0",
56
57
  "clsx": "^2.1.1"
57
58
  },
58
59
  "peerDependencies": {
@@ -62,5 +63,5 @@
62
63
  "publishConfig": {
63
64
  "access": "public"
64
65
  },
65
- "gitHead": "2466f6bc223f8be98c55e1ac7270e8c3e413eaaf"
66
+ "gitHead": "2cf13ec6cf86153c9b3cf369bf5c59046f5cd950"
66
67
  }
@@ -13,12 +13,14 @@ 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 { Route, RouteLoaderContext } from '../../store/types';
23
+ import type { Route, RouteConfig, RouteLoaderContext } from '../../store/types';
22
24
  import { unlock } from '../../lock-unlock';
23
25
 
24
26
  const {
@@ -29,6 +31,7 @@ const {
29
31
  RouterProvider,
30
32
  createBrowserHistory,
31
33
  parseHref,
34
+ useLoaderData,
32
35
  } = unlock( routePrivateApis );
33
36
 
34
37
  // Not found component
@@ -42,29 +45,6 @@ function NotFoundComponent() {
42
45
  );
43
46
  }
44
47
 
45
- function RouteComponent( {
46
- stage: Stage,
47
- inspector: Inspector,
48
- }: {
49
- stage?: ComponentType;
50
- inspector?: ComponentType;
51
- } ) {
52
- return (
53
- <>
54
- { Stage && (
55
- <div className="boot-layout__stage">
56
- <Stage />
57
- </div>
58
- ) }
59
- { Inspector && (
60
- <div className="boot-layout__inspector">
61
- <Inspector />
62
- </div>
63
- ) }
64
- </>
65
- );
66
- }
67
-
68
48
  /**
69
49
  * Creates a TanStack route from a Route definition.
70
50
  *
@@ -76,12 +56,7 @@ async function createRouteFromDefinition(
76
56
  route: Route,
77
57
  parentRoute: AnyRoute
78
58
  ) {
79
- // Load route module for lifecycle functions if specified
80
- let routeConfig: {
81
- beforeLoad?: ( context: RouteLoaderContext ) => void | Promise< void >;
82
- loader?: ( context: RouteLoaderContext ) => Promise< unknown >;
83
- canvas?: ( context: RouteLoaderContext ) => Promise< any >;
84
- } = {};
59
+ let routeConfig: RouteConfig = {};
85
60
 
86
61
  if ( route.route_module ) {
87
62
  const module = await import( route.route_module );
@@ -105,20 +80,32 @@ async function createRouteFromDefinition(
105
80
  search: opts.deps || {},
106
81
  };
107
82
 
108
- // Call both loader and canvas functions if they exist
109
- const [ loaderData, canvasData ] = await Promise.all( [
83
+ const [ , loaderData, canvasData, titleData ] = await Promise.all( [
84
+ resolveSelect( coreStore ).getEntityRecord(
85
+ 'root',
86
+ '__unstableBase'
87
+ ),
110
88
  routeConfig.loader
111
89
  ? routeConfig.loader( context )
112
90
  : Promise.resolve( undefined ),
113
91
  routeConfig.canvas
114
92
  ? routeConfig.canvas( context )
115
93
  : Promise.resolve( undefined ),
94
+ routeConfig.title
95
+ ? routeConfig.title( context )
96
+ : Promise.resolve( undefined ),
116
97
  ] );
117
98
 
99
+ let inspector = true;
100
+ if ( routeConfig.inspector ) {
101
+ inspector = await routeConfig.inspector( context );
102
+ }
103
+
118
104
  return {
119
105
  ...( loaderData as any ),
120
106
  canvas: canvasData,
121
- // Include content module path so Root can load custom canvas
107
+ inspector,
108
+ title: titleData,
122
109
  routeContentModule: route.content_module,
123
110
  };
124
111
  },
@@ -131,13 +118,27 @@ async function createRouteFromDefinition(
131
118
  ? await import( route.content_module )
132
119
  : {};
133
120
 
121
+ const Stage = module.stage;
122
+ const Inspector = module.inspector;
123
+
134
124
  return createLazyRoute( route.path )( {
135
- component: function Component() {
125
+ component: function RouteComponent() {
126
+ const { inspector: showInspector } =
127
+ useLoaderData( { from: route.path } ) ?? {};
128
+
136
129
  return (
137
- <RouteComponent
138
- stage={ module.stage }
139
- inspector={ module.inspector }
140
- />
130
+ <>
131
+ { Stage && (
132
+ <div className="boot-layout__stage">
133
+ <Stage />
134
+ </div>
135
+ ) }
136
+ { Inspector && showInspector && (
137
+ <div className="boot-layout__inspector">
138
+ <Inspector />
139
+ </div>
140
+ ) }
141
+ </>
141
142
  );
142
143
  },
143
144
  } );
@@ -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
 
@@ -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,
@@ -11,6 +11,17 @@ import { privateApis as routePrivateApis } from '@wordpress/route';
11
11
  import { CommandMenu } from '@wordpress/commands';
12
12
  import { privateApis as themePrivateApis } from '@wordpress/theme';
13
13
  import { EditorSnackbars } from '@wordpress/editor';
14
+ import { useViewportMatch, useReducedMotion } from '@wordpress/compose';
15
+ import {
16
+ __unstableMotion as motion,
17
+ __unstableAnimatePresence as AnimatePresence,
18
+ Button,
19
+ SlotFillProvider,
20
+ } from '@wordpress/components';
21
+ import { menu } from '@wordpress/icons';
22
+ import { useState, useEffect } from '@wordpress/element';
23
+ import { __ } from '@wordpress/i18n';
24
+ import { Page } from '@wordpress/admin-ui';
14
25
 
15
26
  /**
16
27
  * Internal dependencies
@@ -18,15 +29,17 @@ import { EditorSnackbars } from '@wordpress/editor';
18
29
  import Sidebar from '../sidebar';
19
30
  import SavePanel from '../save-panel';
20
31
  import CanvasRenderer from '../canvas-renderer';
32
+ import useRouteTitle from '../app/use-route-title';
21
33
  import { unlock } from '../../lock-unlock';
22
34
  import type { CanvasData } from '../../store/types';
23
35
  import './style.scss';
24
36
 
25
37
  const { ThemeProvider } = unlock( themePrivateApis );
26
- const { useMatches, Outlet } = unlock( routePrivateApis );
38
+ const { useLocation, useMatches, Outlet } = unlock( routePrivateApis );
27
39
 
28
40
  export default function Root() {
29
41
  const matches = useMatches();
42
+ const location = useLocation();
30
43
  const currentMatch = matches[ matches.length - 1 ];
31
44
  const canvas = ( currentMatch?.loaderData as any )?.canvas as
32
45
  | CanvasData
@@ -36,41 +49,145 @@ export default function Root() {
36
49
  ?.routeContentModule as string | undefined;
37
50
  const isFullScreen = canvas && ! canvas.isPreview;
38
51
 
52
+ useRouteTitle();
53
+
54
+ // Mobile sidebar state
55
+ const isMobileViewport = useViewportMatch( 'medium', '<' );
56
+ const [ isMobileSidebarOpen, setIsMobileSidebarOpen ] = useState( false );
57
+ const disableMotion = useReducedMotion();
58
+ // Close mobile sidebar on viewport resize and path change
59
+ useEffect( () => {
60
+ setIsMobileSidebarOpen( false );
61
+ }, [ location.pathname, isMobileViewport ] );
62
+
39
63
  return (
40
- <ThemeProvider isRoot color={ { bg: '#f8f8f8', primary: '#3858e9' } }>
41
- <ThemeProvider color={ { bg: '#1d2327', primary: '#3858e9' } }>
42
- <div
43
- className={ clsx( 'boot-layout', {
44
- 'has-canvas': !! canvas || canvas === null,
45
- 'has-full-canvas': isFullScreen,
46
- } ) }
47
- >
48
- <CommandMenu />
49
- <SavePanel />
50
- <EditorSnackbars />
51
- { ! isFullScreen && (
52
- <div className="boot-layout__sidebar">
53
- <Sidebar />
54
- </div>
55
- ) }
56
- <div className="boot-layout__surfaces">
57
- <ThemeProvider
58
- color={ { bg: '#ffffff', primary: '#3858e9' } }
59
- >
60
- <Outlet />
61
- </ThemeProvider>
62
- { /* Render Canvas in Root to prevent remounting on route changes */ }
63
- { ( canvas || canvas === null ) && (
64
- <div className="boot-layout__canvas">
65
- <CanvasRenderer
66
- canvas={ canvas }
67
- routeContentModule={ routeContentModule }
64
+ <SlotFillProvider>
65
+ <ThemeProvider
66
+ isRoot
67
+ color={ { bg: '#f8f8f8', primary: '#3858e9' } }
68
+ >
69
+ <ThemeProvider color={ { bg: '#1d2327', primary: '#3858e9' } }>
70
+ <div
71
+ className={ clsx( 'boot-layout', {
72
+ 'has-canvas': !! canvas || canvas === null,
73
+ 'has-full-canvas': isFullScreen,
74
+ } ) }
75
+ >
76
+ <CommandMenu />
77
+ <SavePanel />
78
+ <EditorSnackbars />
79
+ { isMobileViewport && (
80
+ <Page.SidebarToggleFill>
81
+ <Button
82
+ icon={ menu }
83
+ onClick={ () =>
84
+ setIsMobileSidebarOpen( true )
85
+ }
86
+ label={ __( 'Open navigation panel' ) }
87
+ size="compact"
68
88
  />
89
+ </Page.SidebarToggleFill>
90
+ ) }
91
+ { /* Mobile Sidebar Backdrop */ }
92
+ <AnimatePresence>
93
+ { isMobileViewport &&
94
+ isMobileSidebarOpen &&
95
+ ! isFullScreen && (
96
+ <motion.div
97
+ initial={ { opacity: 0 } }
98
+ animate={ { opacity: 1 } }
99
+ exit={ { opacity: 0 } }
100
+ transition={ {
101
+ type: 'tween',
102
+ duration: disableMotion ? 0 : 0.2,
103
+ ease: 'easeOut',
104
+ } }
105
+ className="boot-layout__sidebar-backdrop"
106
+ onClick={ () =>
107
+ setIsMobileSidebarOpen( false )
108
+ }
109
+ onKeyDown={ ( event ) => {
110
+ if ( event.key === 'Escape' ) {
111
+ setIsMobileSidebarOpen( false );
112
+ }
113
+ } }
114
+ role="button"
115
+ tabIndex={ -1 }
116
+ aria-label={ __(
117
+ 'Close navigation panel'
118
+ ) }
119
+ />
120
+ ) }
121
+ </AnimatePresence>
122
+ { /* Mobile Sidebar */ }
123
+ <AnimatePresence>
124
+ { isMobileViewport &&
125
+ isMobileSidebarOpen &&
126
+ ! isFullScreen && (
127
+ <motion.div
128
+ initial={ { x: '-100%' } }
129
+ animate={ { x: 0 } }
130
+ exit={ { x: '-100%' } }
131
+ transition={ {
132
+ type: 'tween',
133
+ duration: disableMotion ? 0 : 0.2,
134
+ ease: 'easeOut',
135
+ } }
136
+ className="boot-layout__sidebar is-mobile"
137
+ >
138
+ <Sidebar />
139
+ </motion.div>
140
+ ) }
141
+ </AnimatePresence>
142
+ { /* Desktop Sidebar */ }
143
+ { ! isMobileViewport && ! isFullScreen && (
144
+ <div className="boot-layout__sidebar">
145
+ <Sidebar />
69
146
  </div>
70
147
  ) }
148
+ <div className="boot-layout__surfaces">
149
+ <ThemeProvider
150
+ color={ { bg: '#ffffff', primary: '#3858e9' } }
151
+ >
152
+ <Outlet />
153
+ </ThemeProvider>
154
+ { /* Render Canvas in Root to prevent remounting on route changes */ }
155
+ { ( canvas || canvas === null ) && (
156
+ <div
157
+ className={ clsx( 'boot-layout__canvas', {
158
+ 'has-mobile-drawer':
159
+ canvas?.isPreview &&
160
+ isMobileViewport,
161
+ } ) }
162
+ >
163
+ { canvas?.isPreview && isMobileViewport && (
164
+ <div className="boot-layout__mobile-sidebar-drawer">
165
+ <Button
166
+ icon={ menu }
167
+ onClick={ () =>
168
+ setIsMobileSidebarOpen(
169
+ true
170
+ )
171
+ }
172
+ label={ __(
173
+ 'Open navigation panel'
174
+ ) }
175
+ size="compact"
176
+ />
177
+ </div>
178
+ ) }
179
+ <CanvasRenderer
180
+ canvas={ canvas }
181
+ routeContentModule={
182
+ routeContentModule
183
+ }
184
+ />
185
+ </div>
186
+ ) }
187
+ </div>
71
188
  </div>
72
- </div>
189
+ </ThemeProvider>
73
190
  </ThemeProvider>
74
- </ThemeProvider>
191
+ </SlotFillProvider>
75
192
  );
76
193
  }
@@ -20,6 +20,7 @@ import CanvasRenderer from '../canvas-renderer';
20
20
  import { unlock } from '../../lock-unlock';
21
21
  import type { CanvasData } from '../../store/types';
22
22
  import './style.scss';
23
+ import useRouteTitle from '../app/use-route-title';
23
24
 
24
25
  const { useMatches, Outlet } = unlock( routePrivateApis );
25
26
  const { ThemeProvider } = unlock( themePrivateApis );
@@ -39,6 +40,8 @@ export default function RootSinglePage() {
39
40
  ?.routeContentModule as string | undefined;
40
41
  const isFullScreen = canvas && ! canvas.isPreview;
41
42
 
43
+ useRouteTitle();
44
+
42
45
  return (
43
46
  <ThemeProvider isRoot color={ { bg: '#f8f8f8', primary: '#3858e9' } }>
44
47
  <ThemeProvider color={ { bg: '#1d2327', primary: '#3858e9' } }>