@wordpress/boot 0.2.0 → 0.2.1-next.dc3f6d3c1.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 (80) hide show
  1. package/build-module/components/app/index.js +6 -1
  2. package/build-module/components/app/index.js.map +2 -2
  3. package/build-module/components/app/router.js +42 -27
  4. package/build-module/components/app/router.js.map +2 -2
  5. package/build-module/components/canvas/back-button.js +1 -1
  6. package/build-module/components/canvas/back-button.js.map +1 -1
  7. package/build-module/components/canvas/index.js +42 -17
  8. package/build-module/components/canvas/index.js.map +2 -2
  9. package/build-module/components/navigation/dropdown-item/index.js +1 -1
  10. package/build-module/components/navigation/dropdown-item/index.js.map +1 -1
  11. package/build-module/components/navigation/navigation-item/index.js +1 -1
  12. package/build-module/components/navigation/navigation-item/index.js.map +1 -1
  13. package/build-module/components/navigation/navigation-screen/index.js +1 -1
  14. package/build-module/components/navigation/navigation-screen/index.js.map +1 -1
  15. package/build-module/components/navigation/router-link-item.js +3 -1
  16. package/build-module/components/navigation/router-link-item.js.map +2 -2
  17. package/build-module/components/navigation/use-sidebar-parent.js +3 -1
  18. package/build-module/components/navigation/use-sidebar-parent.js.map +2 -2
  19. package/build-module/components/root/index.js +6 -9
  20. package/build-module/components/root/index.js.map +2 -2
  21. package/build-module/components/root/single-page.js +6 -9
  22. package/build-module/components/root/single-page.js.map +2 -2
  23. package/build-module/components/save-button/index.js +116 -0
  24. package/build-module/components/save-button/index.js.map +7 -0
  25. package/build-module/components/sidebar/index.js +8 -2
  26. package/build-module/components/sidebar/index.js.map +2 -2
  27. package/build-module/components/site-hub/index.js +1 -1
  28. package/build-module/components/site-hub/index.js.map +1 -1
  29. package/build-module/components/site-icon/index.js +1 -1
  30. package/build-module/components/site-icon/index.js.map +1 -1
  31. package/build-module/components/site-icon-link/index.js +4 -2
  32. package/build-module/components/site-icon-link/index.js.map +2 -2
  33. package/build-module/index.js +75 -53
  34. package/build-module/index.js.map +2 -2
  35. package/build-module/store/actions.js +9 -1
  36. package/build-module/store/actions.js.map +2 -2
  37. package/build-module/store/reducer.js +11 -0
  38. package/build-module/store/reducer.js.map +2 -2
  39. package/build-style/style-rtl.css +71 -51
  40. package/build-style/style.css +71 -51
  41. package/build-types/components/app/index.d.ts +2 -1
  42. package/build-types/components/app/index.d.ts.map +1 -1
  43. package/build-types/components/app/router.d.ts +3 -0
  44. package/build-types/components/app/router.d.ts.map +1 -1
  45. package/build-types/components/canvas/index.d.ts.map +1 -1
  46. package/build-types/components/navigation/router-link-item.d.ts +1 -3
  47. package/build-types/components/navigation/router-link-item.d.ts.map +1 -1
  48. package/build-types/components/navigation/use-sidebar-parent.d.ts.map +1 -1
  49. package/build-types/components/root/index.d.ts.map +1 -1
  50. package/build-types/components/root/single-page.d.ts.map +1 -1
  51. package/build-types/components/save-button/index.d.ts +6 -0
  52. package/build-types/components/save-button/index.d.ts.map +1 -0
  53. package/build-types/components/sidebar/index.d.ts.map +1 -1
  54. package/build-types/components/site-icon-link/index.d.ts.map +1 -1
  55. package/build-types/index.d.ts +1 -0
  56. package/build-types/index.d.ts.map +1 -1
  57. package/build-types/store/actions.d.ts +6 -1
  58. package/build-types/store/actions.d.ts.map +1 -1
  59. package/build-types/store/reducer.d.ts.map +1 -1
  60. package/build-types/store/types.d.ts +23 -3
  61. package/build-types/store/types.d.ts.map +1 -1
  62. package/package.json +21 -22
  63. package/src/components/app/index.tsx +7 -0
  64. package/src/components/app/router.tsx +67 -35
  65. package/src/components/canvas/index.tsx +35 -11
  66. package/src/components/navigation/router-link-item.tsx +8 -1
  67. package/src/components/navigation/use-sidebar-parent.ts +8 -5
  68. package/src/components/root/index.tsx +4 -8
  69. package/src/components/root/single-page.tsx +4 -8
  70. package/src/components/save-button/index.tsx +121 -0
  71. package/src/components/save-button/style.scss +3 -0
  72. package/src/components/sidebar/index.tsx +4 -0
  73. package/src/components/sidebar/style.scss +4 -0
  74. package/src/components/site-icon-link/index.tsx +5 -2
  75. package/src/index.tsx +1 -0
  76. package/src/store/actions.ts +9 -0
  77. package/src/store/reducer.ts +12 -0
  78. package/src/store/types.ts +25 -3
  79. package/tsconfig.json +1 -0
  80. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/boot",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-next.dc3f6d3c1.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,26 +34,25 @@
34
34
  "wpScriptModuleExports": "./build-module/index.js",
35
35
  "types": "build-types",
36
36
  "dependencies": {
37
- "@tanstack/history": "^1.133.28",
38
- "@tanstack/react-router": "^1.120.5",
39
- "@wordpress/admin-ui": "^1.3.0",
40
- "@wordpress/commands": "^1.35.0",
41
- "@wordpress/components": "^30.8.0",
42
- "@wordpress/compose": "^7.35.0",
43
- "@wordpress/core-data": "^7.35.0",
44
- "@wordpress/data": "^10.35.0",
45
- "@wordpress/editor": "^14.35.0",
46
- "@wordpress/element": "^6.35.0",
47
- "@wordpress/html-entities": "^4.35.0",
48
- "@wordpress/i18n": "^6.8.0",
49
- "@wordpress/icons": "^11.2.0",
50
- "@wordpress/keyboard-shortcuts": "^5.35.0",
51
- "@wordpress/keycodes": "^4.35.0",
52
- "@wordpress/lazy-editor": "^1.1.0",
53
- "@wordpress/primitives": "^4.35.0",
54
- "@wordpress/private-apis": "^1.35.0",
55
- "@wordpress/theme": "^0.2.0",
56
- "@wordpress/url": "^4.35.0",
37
+ "@wordpress/admin-ui": "^1.3.1-next.dc3f6d3c1.0",
38
+ "@wordpress/commands": "^1.35.1-next.dc3f6d3c1.0",
39
+ "@wordpress/components": "^30.8.2-next.dc3f6d3c1.0",
40
+ "@wordpress/compose": "^7.35.1-next.dc3f6d3c1.0",
41
+ "@wordpress/core-data": "^7.35.1-next.dc3f6d3c1.0",
42
+ "@wordpress/data": "^10.35.1-next.dc3f6d3c1.0",
43
+ "@wordpress/editor": "^14.35.2-next.dc3f6d3c1.0",
44
+ "@wordpress/element": "^6.35.1-next.dc3f6d3c1.0",
45
+ "@wordpress/html-entities": "^4.35.1-next.dc3f6d3c1.0",
46
+ "@wordpress/i18n": "^6.8.1-next.dc3f6d3c1.0",
47
+ "@wordpress/icons": "^11.2.1-next.dc3f6d3c1.0",
48
+ "@wordpress/keyboard-shortcuts": "^5.35.1-next.dc3f6d3c1.0",
49
+ "@wordpress/keycodes": "^4.35.1-next.dc3f6d3c1.0",
50
+ "@wordpress/lazy-editor": "^1.1.1-next.dc3f6d3c1.0",
51
+ "@wordpress/primitives": "^4.35.1-next.dc3f6d3c1.0",
52
+ "@wordpress/private-apis": "^1.35.1-next.dc3f6d3c1.0",
53
+ "@wordpress/route": "^0.1.1-next.dc3f6d3c1.0",
54
+ "@wordpress/theme": "^0.2.1-next.dc3f6d3c1.0",
55
+ "@wordpress/url": "^4.35.1-next.dc3f6d3c1.0",
57
56
  "clsx": "^2.1.1"
58
57
  },
59
58
  "peerDependencies": {
@@ -63,5 +62,5 @@
63
62
  "publishConfig": {
64
63
  "access": "public"
65
64
  },
66
- "gitHead": "77aa1f194edceafe8ac2a1b9438bf84b557e76e3"
65
+ "gitHead": "f73b5e69b34fbaccfb8c47783f4f993059ff1a41"
67
66
  }
@@ -22,10 +22,12 @@ export async function init( {
22
22
  mountId,
23
23
  menuItems,
24
24
  routes,
25
+ initModules,
25
26
  }: {
26
27
  mountId: string;
27
28
  menuItems?: MenuItem[];
28
29
  routes?: Route[];
30
+ initModules?: string[];
29
31
  } ) {
30
32
  ( menuItems ?? [] ).forEach( ( menuItem ) => {
31
33
  dispatch( store ).registerMenuItem( menuItem.id, menuItem );
@@ -35,6 +37,11 @@ export async function init( {
35
37
  dispatch( store ).registerRoute( route );
36
38
  } );
37
39
 
40
+ for ( const moduleId of initModules ?? [] ) {
41
+ const module = await import( moduleId );
42
+ await module.init();
43
+ }
44
+
38
45
  // Render the app
39
46
  const rootElement = document.getElementById( mountId );
40
47
  if ( rootElement ) {
@@ -1,30 +1,37 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import {
5
- createRouter,
6
- createRootRoute,
7
- createRoute,
8
- RouterProvider,
9
- createBrowserHistory,
10
- type AnyRoute,
11
- redirect,
12
- } from '@tanstack/react-router';
13
- import { parseHref } from '@tanstack/history';
14
4
  import type { ComponentType } from 'react';
15
5
 
16
6
  /**
17
7
  * WordPress dependencies
18
8
  */
19
9
  import { __ } from '@wordpress/i18n';
20
- import { lazy, useState, useEffect } from '@wordpress/element';
10
+ import { useState, useEffect } from '@wordpress/element';
21
11
  import { Page } from '@wordpress/admin-ui';
12
+ import {
13
+ privateApis as routePrivateApis,
14
+ type AnyRoute,
15
+ } from '@wordpress/route';
22
16
 
23
17
  /**
24
18
  * Internal dependencies
25
19
  */
26
20
  import Root from '../root';
27
- import type { Route, RouteLoaderContext } from '../../store/types';
21
+ import type { CanvasData, Route, RouteLoaderContext } from '../../store/types';
22
+ import { unlock } from '../../lock-unlock';
23
+ import Canvas from '../canvas';
24
+
25
+ const {
26
+ createLazyRoute,
27
+ createRouter,
28
+ createRootRoute,
29
+ createRoute,
30
+ RouterProvider,
31
+ createBrowserHistory,
32
+ parseHref,
33
+ useMatches,
34
+ } = unlock( routePrivateApis );
28
35
 
29
36
  // Not found component
30
37
  function NotFoundComponent() {
@@ -40,10 +47,20 @@ function NotFoundComponent() {
40
47
  function RouteComponent( {
41
48
  stage: Stage,
42
49
  inspector: Inspector,
50
+ canvas: CustomCanvas,
43
51
  }: {
44
52
  stage?: ComponentType;
45
53
  inspector?: ComponentType;
54
+ canvas?: ComponentType;
46
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
+
47
64
  return (
48
65
  <>
49
66
  { Stage && (
@@ -56,6 +73,18 @@ function RouteComponent( {
56
73
  <Inspector />
57
74
  </div>
58
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
+ ) }
59
88
  </>
60
89
  );
61
90
  }
@@ -71,27 +100,9 @@ async function createRouteFromDefinition(
71
100
  route: Route,
72
101
  parentRoute: AnyRoute
73
102
  ) {
74
- // Create lazy components for stage and inspector surfaces
75
- const SurfacesModule = route.content_module
76
- ? lazy( async () => {
77
- const module = await import( route.content_module! );
78
- // Return a component that renders the surfaces
79
- return {
80
- default: () => (
81
- <RouteComponent
82
- stage={ module.stage }
83
- inspector={ module.inspector }
84
- />
85
- ),
86
- };
87
- } )
88
- : () => null;
89
-
90
103
  // Load route module for lifecycle functions if specified
91
104
  let routeConfig: {
92
- beforeLoad?: (
93
- context: RouteLoaderContext & { redirect: Function }
94
- ) => void | Promise< void >;
105
+ beforeLoad?: ( context: RouteLoaderContext ) => void | Promise< void >;
95
106
  loader?: ( context: RouteLoaderContext ) => Promise< unknown >;
96
107
  canvas?: ( context: RouteLoaderContext ) => Promise< any >;
97
108
  } = {};
@@ -101,7 +112,8 @@ async function createRouteFromDefinition(
101
112
  routeConfig = module.route || {};
102
113
  }
103
114
 
104
- return createRoute( {
115
+ // Create route without component initially
116
+ let tanstackRoute = createRoute( {
105
117
  getParentRoute: () => parentRoute,
106
118
  path: route.path,
107
119
  beforeLoad: routeConfig.beforeLoad
@@ -109,7 +121,6 @@ async function createRouteFromDefinition(
109
121
  routeConfig.beforeLoad!( {
110
122
  params: opts.params || {},
111
123
  search: opts.search || {},
112
- redirect,
113
124
  } )
114
125
  : undefined,
115
126
  loader: async ( opts: any ) => {
@@ -134,8 +145,28 @@ async function createRouteFromDefinition(
134
145
  };
135
146
  },
136
147
  loaderDeps: ( opts: any ) => opts.search,
137
- component: SurfacesModule,
138
148
  } );
149
+
150
+ // Chain .lazy() to preload content module on intent
151
+ tanstackRoute = tanstackRoute.lazy( async () => {
152
+ const module = route.content_module
153
+ ? await import( route.content_module )
154
+ : {};
155
+
156
+ return createLazyRoute( route.path )( {
157
+ component: function Component() {
158
+ return (
159
+ <RouteComponent
160
+ stage={ module.stage }
161
+ inspector={ module.inspector }
162
+ canvas={ module.canvas }
163
+ />
164
+ );
165
+ },
166
+ } );
167
+ } );
168
+
169
+ return tanstackRoute;
139
170
  }
140
171
 
141
172
  /**
@@ -171,7 +202,7 @@ function createPathHistory() {
171
202
  const pathHref = `${ path }${ url.hash }`;
172
203
  return parseHref( pathHref, window.history.state );
173
204
  },
174
- createHref: ( href ) => {
205
+ createHref: ( href: string ) => {
175
206
  const searchParams = new URLSearchParams( window.location.search );
176
207
  searchParams.set( 'p', href );
177
208
  return `${ window.location.pathname }?${ searchParams }`;
@@ -201,6 +232,7 @@ export default function Router( {
201
232
  const newRouter = createRouter( {
202
233
  history,
203
234
  routeTree,
235
+ defaultPreload: 'intent',
204
236
  defaultNotFoundComponent: NotFoundComponent,
205
237
  } );
206
238
  setRouter( newRouter );
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { useState, useEffect } from '@wordpress/element';
5
5
  import { Spinner } from '@wordpress/components';
6
+ import { useNavigate } from '@wordpress/route';
6
7
 
7
8
  /**
8
9
  * Internal dependencies
@@ -23,6 +24,7 @@ interface CanvasProps {
23
24
  */
24
25
  export default function Canvas( { canvas }: CanvasProps ) {
25
26
  const [ Editor, setEditor ] = useState< any >( null );
27
+ const navigate = useNavigate();
26
28
 
27
29
  useEffect( () => {
28
30
  // Dynamically import the lazy-editor module
@@ -63,17 +65,39 @@ export default function Canvas( { canvas }: CanvasProps ) {
63
65
 
64
66
  // Render the editor with canvas data
65
67
  return (
66
- <div
67
- style={ { height: '100%' } }
68
- // @ts-expect-error inert untyped properly.
69
- inert={ canvas.isPreview ? 'true' : undefined }
70
- >
71
- <Editor
72
- postType={ canvas.postType }
73
- postId={ canvas.postId }
74
- settings={ { isPreviewMode: canvas.isPreview } }
75
- backButton={ backButton }
76
- />
68
+ <div style={ { height: '100%', position: 'relative' } }>
69
+ <div
70
+ style={ { height: '100%' } }
71
+ // @ts-expect-error inert not typed properly
72
+ inert={ canvas.isPreview ? 'true' : undefined }
73
+ >
74
+ <Editor
75
+ postType={ canvas.postType }
76
+ postId={ canvas.postId }
77
+ settings={ { isPreviewMode: canvas.isPreview } }
78
+ backButton={ backButton }
79
+ />
80
+ </div>
81
+ { canvas.isPreview && canvas.editLink && (
82
+ <div
83
+ onClick={ () => navigate( { to: canvas.editLink } ) }
84
+ onKeyDown={ ( e ) => {
85
+ if ( e.key === 'Enter' || e.key === ' ' ) {
86
+ e.preventDefault();
87
+ navigate( { to: canvas.editLink } );
88
+ }
89
+ } }
90
+ style={ {
91
+ position: 'absolute',
92
+ inset: 0,
93
+ cursor: 'pointer',
94
+ zIndex: 1,
95
+ } }
96
+ role="button"
97
+ tabIndex={ 0 }
98
+ aria-label="Click to edit"
99
+ />
100
+ ) }
77
101
  </div>
78
102
  );
79
103
  }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { createLink } from '@tanstack/react-router';
5
4
  import type { ForwardedRef } from 'react';
6
5
 
7
6
  /**
@@ -9,6 +8,14 @@ import type { ForwardedRef } from 'react';
9
8
  */
10
9
  import { forwardRef } from '@wordpress/element';
11
10
  import { __experimentalItem as Item } from '@wordpress/components';
11
+ import { privateApis as routePrivateApis } from '@wordpress/route';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import { unlock } from '../../lock-unlock';
17
+
18
+ const { createLink } = unlock( routePrivateApis );
12
19
 
13
20
  function AnchorOnlyItem(
14
21
  props: React.ComponentProps< typeof Item >,
@@ -1,14 +1,17 @@
1
- /**
2
- * External dependencies
3
- */
4
- import { useRouter, useMatches } from '@tanstack/react-router';
5
-
6
1
  /**
7
2
  * WordPress dependencies
8
3
  */
4
+ import { privateApis as routePrivateApis } from '@wordpress/route';
9
5
  import { useEffect, useState } from '@wordpress/element';
10
6
  import { useSelect } from '@wordpress/data';
11
7
 
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import { unlock } from '../../lock-unlock';
12
+
13
+ const { useRouter, useMatches } = unlock( routePrivateApis );
14
+
12
15
  /**
13
16
  * Internal dependencies
14
17
  */
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { Outlet, useMatches } from '@tanstack/react-router';
5
4
  import clsx from 'clsx';
6
5
 
7
6
  /**
8
7
  * WordPress dependencies
9
8
  */
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
12
  import { privateApis as themePrivateApis } from '@wordpress/theme';
@@ -15,19 +15,20 @@ import { privateApis as themePrivateApis } from '@wordpress/theme';
15
15
  * Internal dependencies
16
16
  */
17
17
  import Sidebar from '../sidebar';
18
- import Canvas from '../canvas';
19
18
  import SavePanel from '../save-panel';
20
19
  import { unlock } from '../../lock-unlock';
21
20
  import type { CanvasData } from '../../store/types';
22
21
  import './style.scss';
23
22
 
24
23
  const { ThemeProvider } = unlock( themePrivateApis );
24
+ const { useMatches, Outlet } = unlock( routePrivateApis );
25
25
 
26
26
  export default function Root() {
27
27
  const matches = useMatches();
28
28
  const currentMatch = matches[ matches.length - 1 ];
29
29
  const canvas = ( currentMatch?.loaderData as any )?.canvas as
30
30
  | CanvasData
31
+ | null
31
32
  | undefined;
32
33
  const isFullScreen = canvas && ! canvas.isPreview;
33
34
 
@@ -36,7 +37,7 @@ export default function Root() {
36
37
  <ThemeProvider color={ { bg: '#1d2327', primary: '#3858e9' } }>
37
38
  <div
38
39
  className={ clsx( 'boot-layout', {
39
- 'has-canvas': !! canvas,
40
+ 'has-canvas': !! canvas || canvas === null,
40
41
  'has-full-canvas': isFullScreen,
41
42
  } ) }
42
43
  >
@@ -52,11 +53,6 @@ export default function Root() {
52
53
  color={ { bg: '#ffffff', primary: '#3858e9' } }
53
54
  >
54
55
  <Outlet />
55
- { canvas && (
56
- <div className="boot-layout__canvas">
57
- <Canvas canvas={ canvas } />
58
- </div>
59
- ) }
60
56
  </ThemeProvider>
61
57
  </div>
62
58
  </div>
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { Outlet, useMatches } from '@tanstack/react-router';
5
4
  import clsx from 'clsx';
6
5
 
7
6
  /**
8
7
  * WordPress dependencies
9
8
  */
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
12
  import { privateApis as themePrivateApis } from '@wordpress/theme';
@@ -14,12 +14,12 @@ import { privateApis as themePrivateApis } from '@wordpress/theme';
14
14
  /**
15
15
  * Internal dependencies
16
16
  */
17
- import Canvas from '../canvas';
18
17
  import SavePanel from '../save-panel';
19
18
  import { unlock } from '../../lock-unlock';
20
19
  import type { CanvasData } from '../../store/types';
21
20
  import './style.scss';
22
21
 
22
+ const { useMatches, Outlet } = unlock( routePrivateApis );
23
23
  const { ThemeProvider } = unlock( themePrivateApis );
24
24
 
25
25
  /**
@@ -31,6 +31,7 @@ export default function RootSinglePage() {
31
31
  const currentMatch = matches[ matches.length - 1 ];
32
32
  const canvas = ( currentMatch?.loaderData as any )?.canvas as
33
33
  | CanvasData
34
+ | null
34
35
  | undefined;
35
36
  const isFullScreen = canvas && ! canvas.isPreview;
36
37
 
@@ -39,7 +40,7 @@ export default function RootSinglePage() {
39
40
  <ThemeProvider color={ { bg: '#1d2327', primary: '#3858e9' } }>
40
41
  <div
41
42
  className={ clsx( 'boot-layout boot-layout--single-page', {
42
- 'has-canvas': !! canvas,
43
+ 'has-canvas': !! canvas || canvas === null,
43
44
  'has-full-canvas': isFullScreen,
44
45
  } ) }
45
46
  >
@@ -50,11 +51,6 @@ export default function RootSinglePage() {
50
51
  color={ { bg: '#ffffff', primary: '#3858e9' } }
51
52
  >
52
53
  <Outlet />
53
- { canvas && (
54
- <div className="boot-layout__canvas">
55
- <Canvas canvas={ canvas } />
56
- </div>
57
- ) }
58
54
  </ThemeProvider>
59
55
  </div>
60
56
  </div>
@@ -0,0 +1,121 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useEffect, useState } from '@wordpress/element';
5
+ import { useSelect } from '@wordpress/data';
6
+ import { _n, __, sprintf } from '@wordpress/i18n';
7
+ import { store as coreStore } from '@wordpress/core-data';
8
+ import { displayShortcut, rawShortcut } from '@wordpress/keycodes';
9
+ import { check } from '@wordpress/icons';
10
+ import { EntitiesSavedStates } from '@wordpress/editor';
11
+ import { Button, Modal, Tooltip } from '@wordpress/components';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import './style.scss';
17
+ import useSaveShortcut from '../save-panel/use-save-shortcut';
18
+
19
+ export default function SaveButton() {
20
+ const [ isSaveViewOpen, setIsSaveViewOpened ] = useState( false );
21
+ const { isSaving, dirtyEntityRecordsCount } = useSelect( ( select ) => {
22
+ const { isSavingEntityRecord, __experimentalGetDirtyEntityRecords } =
23
+ select( coreStore );
24
+ const dirtyEntityRecords = __experimentalGetDirtyEntityRecords();
25
+ return {
26
+ isSaving: dirtyEntityRecords.some( ( record ) =>
27
+ isSavingEntityRecord( record.kind, record.name, record.key )
28
+ ),
29
+ dirtyEntityRecordsCount: dirtyEntityRecords.length,
30
+ };
31
+ }, [] );
32
+ const [ showSavedState, setShowSavedState ] = useState( false );
33
+
34
+ useEffect( () => {
35
+ if ( isSaving ) {
36
+ // Proactively expect to show saved state. This is done once save
37
+ // starts to avoid race condition where setting it after would cause
38
+ // the button to be unmounted before state is updated.
39
+ setShowSavedState( true );
40
+ }
41
+ }, [ isSaving ] );
42
+
43
+ const hasChanges = dirtyEntityRecordsCount > 0;
44
+
45
+ // Handle save failure case: If we were showing saved state but saving
46
+ // failed, reset to show changes again.
47
+ useEffect( () => {
48
+ if ( ! isSaving && hasChanges ) {
49
+ setShowSavedState( false );
50
+ }
51
+ }, [ isSaving, hasChanges ] );
52
+
53
+ function hideSavedState() {
54
+ if ( showSavedState ) {
55
+ setShowSavedState( false );
56
+ }
57
+ }
58
+
59
+ const shouldShowButton = hasChanges || showSavedState;
60
+
61
+ useSaveShortcut( { openSavePanel: () => setIsSaveViewOpened( true ) } );
62
+
63
+ if ( ! shouldShowButton ) {
64
+ return null;
65
+ }
66
+
67
+ const isInSavedState = showSavedState && ! hasChanges;
68
+ const disabled = isSaving || isInSavedState;
69
+
70
+ const getLabel = () => {
71
+ if ( isInSavedState ) {
72
+ return __( 'Saved' );
73
+ }
74
+ return sprintf(
75
+ // translators: %d: number of unsaved changes (number).
76
+ _n(
77
+ 'Review %d change…',
78
+ 'Review %d changes…',
79
+ dirtyEntityRecordsCount
80
+ ),
81
+ dirtyEntityRecordsCount
82
+ );
83
+ };
84
+ const label = getLabel();
85
+
86
+ return (
87
+ <>
88
+ <Tooltip
89
+ text={ hasChanges ? label : undefined }
90
+ shortcut={ displayShortcut.primary( 's' ) }
91
+ >
92
+ <Button
93
+ variant="primary"
94
+ size="compact"
95
+ onClick={ () => setIsSaveViewOpened( true ) }
96
+ onBlur={ hideSavedState }
97
+ disabled={ disabled }
98
+ accessibleWhenDisabled
99
+ isBusy={ isSaving }
100
+ aria-keyshortcuts={ rawShortcut.primary( 's' ) }
101
+ className="boot-save-button"
102
+ icon={ isInSavedState ? check : undefined }
103
+ >
104
+ { label }
105
+ </Button>
106
+ </Tooltip>
107
+ { isSaveViewOpen && (
108
+ <Modal
109
+ title={ __( 'Review changes' ) }
110
+ onRequestClose={ () => setIsSaveViewOpened( false ) }
111
+ size="small"
112
+ >
113
+ <EntitiesSavedStates
114
+ close={ () => setIsSaveViewOpened( false ) }
115
+ variant="inline"
116
+ />
117
+ </Modal>
118
+ ) }
119
+ </>
120
+ );
121
+ }
@@ -0,0 +1,3 @@
1
+ .boot-save-button {
2
+ width: 100%;
3
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import SiteHub from '../site-hub';
5
5
  import Navigation from '../navigation';
6
+ import SaveButton from '../save-button';
6
7
  import './style.scss';
7
8
 
8
9
  export default function Sidebar() {
@@ -12,6 +13,9 @@ export default function Sidebar() {
12
13
  <div className="boot-sidebar__content">
13
14
  <Navigation />
14
15
  </div>
16
+ <div className="boot-sidebar__footer">
17
+ <SaveButton />
18
+ </div>
15
19
  </div>
16
20
  );
17
21
  }
@@ -13,3 +13,7 @@
13
13
  contain: content;
14
14
  position: relative;
15
15
  }
16
+
17
+ .boot-sidebar__footer {
18
+ padding: variables.$grid-unit-20 variables.$grid-unit-10 variables.$grid-unit-10 variables.$grid-unit-20;
19
+ }
@@ -1,14 +1,17 @@
1
1
  /**
2
- * External dependencies
2
+ * WordPress dependencies
3
3
  */
4
- import { Link, useCanGoBack, useRouter } from '@tanstack/react-router';
4
+ import { Link, privateApis as routePrivateApis } from '@wordpress/route';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
+ import { unlock } from '../../lock-unlock';
9
10
  import SiteIcon from '../site-icon';
10
11
  import './style.scss';
11
12
 
13
+ const { useCanGoBack, useRouter } = unlock( routePrivateApis );
14
+
12
15
  function SiteIconLink( {
13
16
  to,
14
17
  isBackButton,
package/src/index.tsx CHANGED
@@ -3,3 +3,4 @@
3
3
  */
4
4
  import './style.scss';
5
5
  export { init, initSinglePage } from './components/app';
6
+ export { store } from './store';