@wordpress/boot 0.1.1-next.2f1c7c01b.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 (130) hide show
  1. package/LICENSE.md +788 -0
  2. package/build-module/components/app/index.js +32 -0
  3. package/build-module/components/app/index.js.map +7 -0
  4. package/build-module/components/app/router.js +119 -0
  5. package/build-module/components/app/router.js.map +7 -0
  6. package/build-module/components/navigation/drilldown-item/index.js +49 -0
  7. package/build-module/components/navigation/drilldown-item/index.js.map +7 -0
  8. package/build-module/components/navigation/dropdown-item/index.js +162 -0
  9. package/build-module/components/navigation/dropdown-item/index.js.map +7 -0
  10. package/build-module/components/navigation/index.js +101 -0
  11. package/build-module/components/navigation/index.js.map +7 -0
  12. package/build-module/components/navigation/items.js +60 -0
  13. package/build-module/components/navigation/items.js.map +7 -0
  14. package/build-module/components/navigation/navigation-item/index.js +180 -0
  15. package/build-module/components/navigation/navigation-item/index.js.map +7 -0
  16. package/build-module/components/navigation/navigation-screen/index.js +196 -0
  17. package/build-module/components/navigation/navigation-screen/index.js.map +7 -0
  18. package/build-module/components/navigation/path-matching.js +78 -0
  19. package/build-module/components/navigation/path-matching.js.map +7 -0
  20. package/build-module/components/navigation/router-link-item.js +14 -0
  21. package/build-module/components/navigation/router-link-item.js.map +7 -0
  22. package/build-module/components/navigation/use-sidebar-parent.js +52 -0
  23. package/build-module/components/navigation/use-sidebar-parent.js.map +7 -0
  24. package/build-module/components/root/index.js +115 -0
  25. package/build-module/components/root/index.js.map +7 -0
  26. package/build-module/components/sidebar/index.js +78 -0
  27. package/build-module/components/sidebar/index.js.map +7 -0
  28. package/build-module/components/site-hub/index.js +153 -0
  29. package/build-module/components/site-hub/index.js.map +7 -0
  30. package/build-module/components/site-icon/index.js +115 -0
  31. package/build-module/components/site-icon/index.js.map +7 -0
  32. package/build-module/components/site-icon-link/index.js +101 -0
  33. package/build-module/components/site-icon-link/index.js.map +7 -0
  34. package/build-module/index.js +622 -0
  35. package/build-module/index.js.map +7 -0
  36. package/build-module/lock-unlock.js +11 -0
  37. package/build-module/lock-unlock.js.map +7 -0
  38. package/build-module/store/actions.js +19 -0
  39. package/build-module/store/actions.js.map +7 -0
  40. package/build-module/store/index.js +17 -0
  41. package/build-module/store/index.js.map +7 -0
  42. package/build-module/store/reducer.js +27 -0
  43. package/build-module/store/reducer.js.map +7 -0
  44. package/build-module/store/selectors.js +12 -0
  45. package/build-module/store/selectors.js.map +7 -0
  46. package/build-module/store/types.js +1 -0
  47. package/build-module/store/types.js.map +7 -0
  48. package/build-style/style-rtl.css +612 -0
  49. package/build-style/style.css +612 -0
  50. package/build-types/components/app/index.d.ts +6 -0
  51. package/build-types/components/app/index.d.ts.map +1 -0
  52. package/build-types/components/app/router.d.ts +7 -0
  53. package/build-types/components/app/router.d.ts.map +1 -0
  54. package/build-types/components/navigation/drilldown-item/index.d.ts +34 -0
  55. package/build-types/components/navigation/drilldown-item/index.d.ts.map +1 -0
  56. package/build-types/components/navigation/dropdown-item/index.d.ts +36 -0
  57. package/build-types/components/navigation/dropdown-item/index.d.ts.map +1 -0
  58. package/build-types/components/navigation/index.d.ts +3 -0
  59. package/build-types/components/navigation/index.d.ts.map +1 -0
  60. package/build-types/components/navigation/items.d.ts +16 -0
  61. package/build-types/components/navigation/items.d.ts.map +1 -0
  62. package/build-types/components/navigation/navigation-item/index.d.ts +28 -0
  63. package/build-types/components/navigation/navigation-item/index.d.ts.map +1 -0
  64. package/build-types/components/navigation/navigation-screen/index.d.ts +24 -0
  65. package/build-types/components/navigation/navigation-screen/index.d.ts.map +1 -0
  66. package/build-types/components/navigation/path-matching.d.ts +30 -0
  67. package/build-types/components/navigation/path-matching.d.ts.map +1 -0
  68. package/build-types/components/navigation/router-link-item.d.ts +5 -0
  69. package/build-types/components/navigation/router-link-item.d.ts.map +1 -0
  70. package/build-types/components/navigation/use-sidebar-parent.d.ts +12 -0
  71. package/build-types/components/navigation/use-sidebar-parent.d.ts.map +1 -0
  72. package/build-types/components/root/index.d.ts +3 -0
  73. package/build-types/components/root/index.d.ts.map +1 -0
  74. package/build-types/components/sidebar/index.d.ts +3 -0
  75. package/build-types/components/sidebar/index.d.ts.map +1 -0
  76. package/build-types/components/site-hub/index.d.ts +4 -0
  77. package/build-types/components/site-hub/index.d.ts.map +1 -0
  78. package/build-types/components/site-icon/index.d.ts +9 -0
  79. package/build-types/components/site-icon/index.d.ts.map +1 -0
  80. package/build-types/components/site-icon-link/index.d.ts +8 -0
  81. package/build-types/components/site-icon-link/index.d.ts.map +1 -0
  82. package/build-types/index.d.ts +6 -0
  83. package/build-types/index.d.ts.map +1 -0
  84. package/build-types/lock-unlock.d.ts +2 -0
  85. package/build-types/lock-unlock.d.ts.map +1 -0
  86. package/build-types/store/actions.d.ts +15 -0
  87. package/build-types/store/actions.d.ts.map +1 -0
  88. package/build-types/store/index.d.ts +6 -0
  89. package/build-types/store/index.d.ts.map +1 -0
  90. package/build-types/store/reducer.d.ts +7 -0
  91. package/build-types/store/reducer.d.ts.map +1 -0
  92. package/build-types/store/selectors.d.ts +7 -0
  93. package/build-types/store/selectors.d.ts.map +1 -0
  94. package/build-types/store/types.d.ts +63 -0
  95. package/build-types/store/types.d.ts.map +1 -0
  96. package/package.json +64 -0
  97. package/src/components/app/index.tsx +45 -0
  98. package/src/components/app/router.tsx +198 -0
  99. package/src/components/navigation/drilldown-item/index.tsx +88 -0
  100. package/src/components/navigation/dropdown-item/index.tsx +134 -0
  101. package/src/components/navigation/dropdown-item/style.scss +23 -0
  102. package/src/components/navigation/index.tsx +126 -0
  103. package/src/components/navigation/items.tsx +93 -0
  104. package/src/components/navigation/navigation-item/index.tsx +88 -0
  105. package/src/components/navigation/navigation-item/style.scss +52 -0
  106. package/src/components/navigation/navigation-screen/index.tsx +147 -0
  107. package/src/components/navigation/navigation-screen/style.scss +34 -0
  108. package/src/components/navigation/path-matching.ts +149 -0
  109. package/src/components/navigation/router-link-item.tsx +22 -0
  110. package/src/components/navigation/use-sidebar-parent.ts +77 -0
  111. package/src/components/root/index.tsx +42 -0
  112. package/src/components/root/style.scss +41 -0
  113. package/src/components/sidebar/index.tsx +17 -0
  114. package/src/components/sidebar/style.scss +15 -0
  115. package/src/components/site-hub/index.tsx +67 -0
  116. package/src/components/site-hub/style.scss +54 -0
  117. package/src/components/site-icon/index.tsx +60 -0
  118. package/src/components/site-icon/style.scss +19 -0
  119. package/src/components/site-icon-link/index.tsx +43 -0
  120. package/src/components/site-icon-link/style.scss +24 -0
  121. package/src/index.tsx +5 -0
  122. package/src/lock-unlock.ts +9 -0
  123. package/src/store/actions.ts +23 -0
  124. package/src/store/index.ts +23 -0
  125. package/src/store/reducer.ts +31 -0
  126. package/src/store/selectors.ts +12 -0
  127. package/src/store/types.ts +70 -0
  128. package/src/style.scss +2 -0
  129. package/tsconfig.json +23 -0
  130. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+ import type { ReactNode } from 'react';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import {
11
+ FlexBlock,
12
+ __experimentalItem as Item,
13
+ // @ts-ignore
14
+ __experimentalHStack as HStack,
15
+ } from '@wordpress/components';
16
+
17
+ /**
18
+ * Internal dependencies
19
+ */
20
+ import RouterLinkItem from '../router-link-item';
21
+ import { wrapIcon } from '../items';
22
+ import type { IconType } from '../../../store/types';
23
+ import './style.scss';
24
+
25
+ interface NavigationItemProps {
26
+ /**
27
+ * Optional CSS class name.
28
+ */
29
+ className?: string;
30
+ /**
31
+ * Icon to display with the navigation item.
32
+ */
33
+ icon?: IconType;
34
+ /**
35
+ * Whether to show placeholder icons for alignment.
36
+ */
37
+ shouldShowPlaceholder?: boolean;
38
+ /**
39
+ * Content to display inside the navigation item.
40
+ */
41
+ children: ReactNode;
42
+ /**
43
+ * The path to navigate to.
44
+ */
45
+ to: string;
46
+ }
47
+
48
+ export default function NavigationItem( {
49
+ className,
50
+ icon,
51
+ shouldShowPlaceholder = true,
52
+ children,
53
+ to,
54
+ }: NavigationItemProps ) {
55
+ // Check if the 'to' prop is an external URL
56
+ const isExternal = ! String(
57
+ new URL( to, window.location.origin )
58
+ ).startsWith( window.location.origin );
59
+
60
+ const content = (
61
+ <HStack justify="flex-start" spacing={ 2 } style={ { flexGrow: '1' } }>
62
+ { wrapIcon( icon, shouldShowPlaceholder ) }
63
+ <FlexBlock>{ children }</FlexBlock>
64
+ </HStack>
65
+ );
66
+
67
+ if ( isExternal ) {
68
+ // Render as a regular anchor tag for external URLs
69
+ return (
70
+ <Item
71
+ as="a"
72
+ href={ to }
73
+ className={ clsx( 'boot-navigation-item', className ) }
74
+ >
75
+ { content }
76
+ </Item>
77
+ );
78
+ }
79
+
80
+ return (
81
+ <RouterLinkItem
82
+ to={ to }
83
+ className={ clsx( 'boot-navigation-item', className ) }
84
+ >
85
+ { content }
86
+ </RouterLinkItem>
87
+ );
88
+ }
@@ -0,0 +1,52 @@
1
+ @use "@wordpress/base-styles/variables";
2
+ @use "@wordpress/base-styles/mixins";
3
+
4
+ .boot-navigation-item.components-item {
5
+ color: var(--wpds-color-fg-interactive-neutral, #1e1e1e);
6
+ padding-inline: variables.$grid-unit-05;
7
+ padding-block: 0;
8
+ margin-inline: variables.$grid-unit-15;
9
+ margin-block-end: variables.$grid-unit-05;
10
+ width: calc(100% - variables.$grid-unit-15 * 2);
11
+ border: none;
12
+ min-height: variables.$grid-unit-40;
13
+ display: flex;
14
+ align-items: center;
15
+ @include mixins.body-medium();
16
+
17
+ .boot-dropdown-item__children & {
18
+ min-height: variables.$grid-unit-30;
19
+ }
20
+
21
+ // Rounded focus ring
22
+ border-radius: var(--wpds-border-radius-small, 2px);
23
+
24
+ &.active,
25
+ &:hover,
26
+ &:focus,
27
+ &[aria-current="true"] {
28
+ color: var(--wpds-color-fg-interactive-brand-active, #0073aa);
29
+ }
30
+
31
+ &.active {
32
+ font-weight: variables.$font-weight-medium;
33
+ }
34
+
35
+ svg:last-child {
36
+ padding: variables.$grid-unit-05;
37
+ }
38
+
39
+ &[aria-current="true"] {
40
+ color: var(--wpds-color-fg-interactive-brand-active, #0073aa);
41
+ font-weight: variables.$font-weight-medium;
42
+ }
43
+
44
+ // Make sure the focus style is drawn on top of the current item background.
45
+ &:focus-visible {
46
+ transform: translateZ(0);
47
+ }
48
+
49
+ &.with-suffix {
50
+ padding-right: variables.$grid-unit-20;
51
+ }
52
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { ReactNode, RefObject } from 'react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import {
10
+ __experimentalHeading as Heading,
11
+ __unstableMotion as motion,
12
+ __unstableAnimatePresence as AnimatePresence,
13
+ Button,
14
+ __experimentalHStack as HStack,
15
+ } from '@wordpress/components';
16
+ import { isRTL, __ } from '@wordpress/i18n';
17
+ import { chevronRight, chevronLeft } from '@wordpress/icons';
18
+ import { useReducedMotion } from '@wordpress/compose';
19
+
20
+ /**
21
+ * Internal dependencies
22
+ */
23
+ import './style.scss';
24
+
25
+ const ANIMATION_DURATION = 0.3;
26
+ const slideVariants = {
27
+ initial: ( direction: 'forward' | 'backward' ) => ( {
28
+ x: direction === 'forward' ? 100 : -100,
29
+ opacity: 0,
30
+ } ),
31
+ animate: {
32
+ x: 0,
33
+ opacity: 1,
34
+ },
35
+ exit: ( direction: 'forward' | 'backward' ) => ( {
36
+ x: direction === 'forward' ? 100 : -100,
37
+ opacity: 0,
38
+ } ),
39
+ };
40
+
41
+ export default function NavigationScreen( {
42
+ isRoot,
43
+ title,
44
+ actions,
45
+ content,
46
+ description,
47
+ animationDirection,
48
+ backMenuItem,
49
+ backButtonRef,
50
+ navigationKey,
51
+ onNavigate,
52
+ }: {
53
+ isRoot?: boolean;
54
+ title: string;
55
+ actions?: ReactNode;
56
+ content: ReactNode;
57
+ description?: ReactNode;
58
+ backMenuItem?: string;
59
+ backButtonRef?: RefObject< HTMLButtonElement >;
60
+ animationDirection?: 'forward' | 'backward';
61
+ navigationKey?: string;
62
+ onNavigate: ( {
63
+ id,
64
+ direction,
65
+ }: {
66
+ id?: string;
67
+ direction: 'forward' | 'backward';
68
+ } ) => void;
69
+ } ) {
70
+ const icon = isRTL() ? chevronRight : chevronLeft;
71
+ const disableMotion = useReducedMotion();
72
+
73
+ const handleBackClick = ( e: React.MouseEvent ) => {
74
+ e.preventDefault();
75
+ onNavigate( { id: backMenuItem, direction: 'backward' } );
76
+ };
77
+
78
+ return (
79
+ <div
80
+ className="boot-navigation-screen"
81
+ style={ {
82
+ overflow: 'hidden',
83
+ position: 'relative',
84
+ display: 'grid',
85
+ gridTemplateColumns: '1fr',
86
+ gridTemplateRows: '1fr',
87
+ } }
88
+ >
89
+ <AnimatePresence initial={ false }>
90
+ <motion.div
91
+ key={ navigationKey }
92
+ custom={ animationDirection }
93
+ variants={ slideVariants }
94
+ initial="initial"
95
+ animate="animate"
96
+ exit="exit"
97
+ transition={ {
98
+ type: 'tween',
99
+ duration: disableMotion ? 0 : ANIMATION_DURATION,
100
+ ease: [ 0.33, 0, 0, 1 ],
101
+ } }
102
+ style={ {
103
+ width: '100%',
104
+ gridColumn: '1',
105
+ gridRow: '1',
106
+ } }
107
+ >
108
+ <HStack
109
+ spacing={ 2 }
110
+ className="boot-navigation-screen__title-icon"
111
+ >
112
+ { ! isRoot && (
113
+ <Button
114
+ ref={ backButtonRef }
115
+ icon={ icon }
116
+ onClick={ handleBackClick }
117
+ label={ __( 'Back' ) }
118
+ size="small"
119
+ variant="tertiary"
120
+ />
121
+ ) }
122
+ <Heading
123
+ className="boot-navigation-screen__title"
124
+ level={ 1 }
125
+ size="15px"
126
+ >
127
+ { title }
128
+ </Heading>
129
+ { actions && (
130
+ <div className="boot-navigation-screen__actions">
131
+ { actions }
132
+ </div>
133
+ ) }
134
+ </HStack>
135
+
136
+ { description && (
137
+ <div className="boot-navigation-screen__description">
138
+ { description }
139
+ </div>
140
+ ) }
141
+
142
+ { content }
143
+ </motion.div>
144
+ </AnimatePresence>
145
+ </div>
146
+ );
147
+ }
@@ -0,0 +1,34 @@
1
+ @use "@wordpress/base-styles/variables";
2
+
3
+ .boot-navigation-screen {
4
+ // Avoid cutting off focus ring of the last menu item
5
+ padding-block-end: variables.$grid-unit-05;
6
+ }
7
+
8
+ .boot-navigation-screen .components-text {
9
+ color: var(--wpds-color-fg-content-neutral, #1e1e1e);
10
+ }
11
+
12
+ .boot-navigation-screen__title-icon {
13
+ position: sticky;
14
+ top: 0;
15
+ padding:
16
+ variables.$grid-unit-15 variables.$grid-unit-20
17
+ variables.$grid-unit-10 variables.$grid-unit-20;
18
+ }
19
+
20
+ .boot-navigation-screen__title {
21
+ flex-grow: 1;
22
+ overflow-wrap: break-word;
23
+
24
+ &#{&},
25
+ &#{&} .boot-navigation-screen__title {
26
+ line-height: variables.$font-line-height-x-large;
27
+ color: var(--wpds-color-fg-content-neutral, #1e1e1e);
28
+ }
29
+ }
30
+
31
+ .boot-navigation-screen__actions {
32
+ display: flex;
33
+ flex-shrink: 0;
34
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import type { MenuItem } from '../../store/types';
5
+
6
+ /**
7
+ * Checks if a menu path is a valid parent path of the current path.
8
+ * A valid parent path must be a complete path prefix, not just share segments.
9
+ *
10
+ * @param currentPath - Current page path
11
+ * @param menuPath - Menu item path to check as potential parent
12
+ * @return True if menuPath is a parent of currentPath
13
+ */
14
+ const isValidParentPath = (
15
+ currentPath: string,
16
+ menuPath: string
17
+ ): boolean => {
18
+ if ( ! menuPath || menuPath === currentPath ) {
19
+ return false;
20
+ }
21
+
22
+ // Normalize paths by removing trailing slashes and ensuring leading slash
23
+ const normalizePath = ( path: string ) => {
24
+ const normalized = path.startsWith( '/' ) ? path : '/' + path;
25
+ return normalized.endsWith( '/' ) && normalized.length > 1
26
+ ? normalized.slice( 0, -1 )
27
+ : normalized;
28
+ };
29
+
30
+ const normalizedCurrent = normalizePath( currentPath );
31
+ const normalizedMenu = normalizePath( menuPath );
32
+
33
+ // Menu path must be shorter and current path must start with menu path + '/'
34
+ return (
35
+ normalizedCurrent.startsWith( normalizedMenu ) &&
36
+ ( normalizedCurrent[ normalizedMenu.length ] === '/' ||
37
+ normalizedMenu === '/' )
38
+ );
39
+ };
40
+
41
+ /**
42
+ * Finds the menu item that is the closest parent of the current path.
43
+ * Only considers menu items that have a 'to' path defined and are valid parents.
44
+ *
45
+ * @param currentPath - Current page path
46
+ * @param menuItems - Array of all menu items
47
+ * @return Menu item that is the closest parent, or null if no valid parent found
48
+ */
49
+ export const findClosestMenuItem = (
50
+ currentPath: string,
51
+ menuItems: MenuItem[]
52
+ ): MenuItem | null => {
53
+ const exactMatch = menuItems.find( ( item ) => item.to === currentPath );
54
+ if ( exactMatch ) {
55
+ return exactMatch;
56
+ }
57
+
58
+ let bestMatch: MenuItem | null = null;
59
+ let bestPathLength = 0;
60
+
61
+ for ( const item of menuItems ) {
62
+ if ( ! item.to ) {
63
+ continue;
64
+ }
65
+
66
+ // Only consider items that are valid parents of the current path
67
+ if ( isValidParentPath( currentPath, item.to ) ) {
68
+ // Prefer the longest parent path (most specific)
69
+ if ( item.to.length > bestPathLength ) {
70
+ bestMatch = item;
71
+ bestPathLength = item.to.length;
72
+ }
73
+ }
74
+ }
75
+
76
+ return bestMatch;
77
+ };
78
+
79
+ /**
80
+ * Finds the drilldown parent of a menu item by traversing up the menu tree.
81
+ *
82
+ * @param id - The ID of the menu item to find the drilldown parent for
83
+ * @param menuItems - Array of all menu items
84
+ * @return The ID of the drilldown parent, or undefined if none found
85
+ */
86
+ export const findDrilldownParent = (
87
+ id: string | undefined,
88
+ menuItems: MenuItem[]
89
+ ): string | undefined => {
90
+ if ( ! id ) {
91
+ return undefined;
92
+ }
93
+
94
+ const currentItem = menuItems.find( ( item ) => item.id === id );
95
+ if ( ! currentItem ) {
96
+ return undefined;
97
+ }
98
+
99
+ // If the item has a parent, check if that parent is a drilldown
100
+ if ( currentItem.parent ) {
101
+ const parentItem = menuItems.find(
102
+ ( item ) => item.id === currentItem.parent
103
+ );
104
+
105
+ if ( parentItem?.parent_type === 'drilldown' ) {
106
+ return parentItem.id;
107
+ }
108
+
109
+ if ( parentItem ) {
110
+ return findDrilldownParent( parentItem.id, menuItems );
111
+ }
112
+ }
113
+
114
+ return undefined;
115
+ };
116
+
117
+ /**
118
+ * Finds the dropdown parent of a menu item.
119
+ *
120
+ * @param id - The ID of the menu item to find the dropdown parent for
121
+ * @param menuItems - Array of all menu items
122
+ * @return The ID of the dropdown parent, or undefined if none found
123
+ */
124
+ export const findDropdownParent = (
125
+ id: string | undefined,
126
+ menuItems: MenuItem[]
127
+ ): string | undefined => {
128
+ if ( ! id ) {
129
+ return undefined;
130
+ }
131
+
132
+ const currentItem = menuItems.find( ( item ) => item.id === id );
133
+ if ( ! currentItem ) {
134
+ return undefined;
135
+ }
136
+
137
+ // If the item has a parent, check if that parent is a dropdown
138
+ if ( currentItem.parent ) {
139
+ const parentItem = menuItems.find(
140
+ ( item ) => item.id === currentItem.parent
141
+ );
142
+
143
+ if ( parentItem?.parent_type === 'dropdown' ) {
144
+ return parentItem.id;
145
+ }
146
+ }
147
+
148
+ return undefined;
149
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { createLink } from '@tanstack/react-router';
5
+ import type { ForwardedRef } from 'react';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { forwardRef } from '@wordpress/element';
11
+ import { __experimentalItem as Item } from '@wordpress/components';
12
+
13
+ function AnchorOnlyItem(
14
+ props: React.ComponentProps< typeof Item >,
15
+ forwardedRef: ForwardedRef< HTMLAnchorElement >
16
+ ) {
17
+ return <Item as="a" ref={ forwardedRef } { ...props } />;
18
+ }
19
+
20
+ const RouterLinkItem = createLink( forwardRef( AnchorOnlyItem ) );
21
+
22
+ export default RouterLinkItem;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useRouter, useMatches } from '@tanstack/react-router';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useEffect, useState } from '@wordpress/element';
10
+ import { useSelect } from '@wordpress/data';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { STORE_NAME } from '../../store';
16
+ import {
17
+ findDrilldownParent,
18
+ findDropdownParent,
19
+ findClosestMenuItem,
20
+ } from './path-matching';
21
+
22
+ /**
23
+ * The `useSidebarParent` hook returns the ID of the parent menu item
24
+ * to render in the sidebar based on the current route.
25
+ *
26
+ * - It finds the closest matching menu item when exact path matches fail
27
+ * - It allows the user to navigate in the sidebar (local state) without changing the URL.
28
+ * - If the URL changes, it will update the parent ID to ensure the correct drilldown level is displayed.
29
+ *
30
+ * @return The ID of the parent menu item to render in the sidebar.
31
+ */
32
+ export function useSidebarParent() {
33
+ const matches = useMatches();
34
+ const router = useRouter();
35
+ const menuItems = useSelect(
36
+ ( select ) =>
37
+ // @ts-ignore
38
+ select( STORE_NAME ).getMenuItems(),
39
+ []
40
+ );
41
+
42
+ const currentPath = matches[ matches.length - 1 ].pathname.slice(
43
+ router.options.basepath?.length ?? 0
44
+ );
45
+
46
+ const currentMenuItem = findClosestMenuItem( currentPath, menuItems );
47
+ const [ parentId, setParentId ] = useState< string | undefined >(
48
+ findDrilldownParent( currentMenuItem?.id, menuItems )
49
+ );
50
+ const [ parentDropdownId, setParentDropdownId ] = useState<
51
+ string | undefined
52
+ >( findDropdownParent( currentMenuItem?.id, menuItems ) );
53
+
54
+ // Effect to update parent IDs when URL or menu items change
55
+ useEffect( () => {
56
+ const matchedMenuItem = findClosestMenuItem( currentPath, menuItems );
57
+ // Find the appropriate parents for the current route
58
+ const updatedParentId = findDrilldownParent(
59
+ matchedMenuItem?.id,
60
+ menuItems
61
+ );
62
+ const updatedDropdownParent = findDropdownParent(
63
+ matchedMenuItem?.id,
64
+ menuItems
65
+ );
66
+
67
+ setParentId( updatedParentId );
68
+ setParentDropdownId( updatedDropdownParent );
69
+ }, [ currentPath, menuItems ] );
70
+
71
+ return [
72
+ parentId,
73
+ setParentId,
74
+ parentDropdownId,
75
+ setParentDropdownId,
76
+ ] as const;
77
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Outlet } from '@tanstack/react-router';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ // @ts-expect-error Commands is not typed properly.
10
+ import { CommandMenu } from '@wordpress/commands';
11
+ import { privateApis as themePrivateApis } from '@wordpress/theme';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import Sidebar from '../sidebar';
17
+ import { unlock } from '../../lock-unlock';
18
+ import './style.scss';
19
+
20
+ const { ThemeProvider } = unlock( themePrivateApis );
21
+
22
+ export default function Root() {
23
+ return (
24
+ <ThemeProvider isRoot color={ { bg: '#f8f8f8', primary: '#3858e9' } }>
25
+ <ThemeProvider color={ { bg: '#1e1e1e', primary: '#3858e9' } }>
26
+ <div className="boot-layout">
27
+ <CommandMenu />
28
+ <div className="boot-layout__sidebar">
29
+ <Sidebar />
30
+ </div>
31
+ <div className="boot-layout__surfaces">
32
+ <ThemeProvider
33
+ color={ { bg: '#ffffff', primary: '#3858e9' } }
34
+ >
35
+ <Outlet />
36
+ </ThemeProvider>
37
+ </div>
38
+ </div>
39
+ </ThemeProvider>
40
+ </ThemeProvider>
41
+ );
42
+ }
@@ -0,0 +1,41 @@
1
+ @use "@wordpress/base-styles/variables";
2
+
3
+ .boot-layout {
4
+ height: 100%;
5
+ width: 100%;
6
+ display: flex;
7
+ flex-direction: row;
8
+ color: var(--wpds-color-fg-content-neutral, #1e1e1e);
9
+ isolation: isolate;
10
+ background: var(--wpds-color-bg-surface-neutral-weak, #f0f0f0);
11
+ }
12
+
13
+ .boot-layout__sidebar {
14
+ height: 100%;
15
+ flex-shrink: 0;
16
+ width: 240px;
17
+ position: relative;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .boot-layout__surfaces {
22
+ display: flex;
23
+ flex-grow: 1;
24
+ margin: variables.$grid-unit-10;
25
+ gap: variables.$grid-unit-10;
26
+ }
27
+
28
+ .boot-layout__stage,
29
+ .boot-layout__inspector {
30
+ flex: 1;
31
+ overflow-y: auto;
32
+ background: var(--wpds-color-bg-surface-neutral, #fff);
33
+ color: var(--wpds-color-fg-content-neutral, #1e1e1e);
34
+ border-radius: 8px;
35
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
36
+ border: 1px solid var(--wpds-color-stroke-surface-neutral-weak, #ddd);
37
+ }
38
+
39
+ .boot-layout__inspector {
40
+ max-width: 400px;
41
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import SiteHub from '../site-hub';
5
+ import Navigation from '../navigation';
6
+ import './style.scss';
7
+
8
+ export default function Sidebar() {
9
+ return (
10
+ <div className="boot-sidebar__scrollable">
11
+ <SiteHub />
12
+ <div className="boot-sidebar__content">
13
+ <Navigation />
14
+ </div>
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,15 @@
1
+ @use "@wordpress/base-styles/variables";
2
+
3
+ .boot-sidebar__scrollable {
4
+ overflow: auto;
5
+ height: 100%;
6
+ position: relative;
7
+ display: flex;
8
+ flex-direction: column;
9
+ }
10
+
11
+ .boot-sidebar__content {
12
+ flex-grow: 1;
13
+ contain: content;
14
+ position: relative;
15
+ }