@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.
- package/LICENSE.md +788 -0
- package/build-module/components/app/index.js +32 -0
- package/build-module/components/app/index.js.map +7 -0
- package/build-module/components/app/router.js +119 -0
- package/build-module/components/app/router.js.map +7 -0
- package/build-module/components/navigation/drilldown-item/index.js +49 -0
- package/build-module/components/navigation/drilldown-item/index.js.map +7 -0
- package/build-module/components/navigation/dropdown-item/index.js +162 -0
- package/build-module/components/navigation/dropdown-item/index.js.map +7 -0
- package/build-module/components/navigation/index.js +101 -0
- package/build-module/components/navigation/index.js.map +7 -0
- package/build-module/components/navigation/items.js +60 -0
- package/build-module/components/navigation/items.js.map +7 -0
- package/build-module/components/navigation/navigation-item/index.js +180 -0
- package/build-module/components/navigation/navigation-item/index.js.map +7 -0
- package/build-module/components/navigation/navigation-screen/index.js +196 -0
- package/build-module/components/navigation/navigation-screen/index.js.map +7 -0
- package/build-module/components/navigation/path-matching.js +78 -0
- package/build-module/components/navigation/path-matching.js.map +7 -0
- package/build-module/components/navigation/router-link-item.js +14 -0
- package/build-module/components/navigation/router-link-item.js.map +7 -0
- package/build-module/components/navigation/use-sidebar-parent.js +52 -0
- package/build-module/components/navigation/use-sidebar-parent.js.map +7 -0
- package/build-module/components/root/index.js +115 -0
- package/build-module/components/root/index.js.map +7 -0
- package/build-module/components/sidebar/index.js +78 -0
- package/build-module/components/sidebar/index.js.map +7 -0
- package/build-module/components/site-hub/index.js +153 -0
- package/build-module/components/site-hub/index.js.map +7 -0
- package/build-module/components/site-icon/index.js +115 -0
- package/build-module/components/site-icon/index.js.map +7 -0
- package/build-module/components/site-icon-link/index.js +101 -0
- package/build-module/components/site-icon-link/index.js.map +7 -0
- package/build-module/index.js +622 -0
- package/build-module/index.js.map +7 -0
- package/build-module/lock-unlock.js +11 -0
- package/build-module/lock-unlock.js.map +7 -0
- package/build-module/store/actions.js +19 -0
- package/build-module/store/actions.js.map +7 -0
- package/build-module/store/index.js +17 -0
- package/build-module/store/index.js.map +7 -0
- package/build-module/store/reducer.js +27 -0
- package/build-module/store/reducer.js.map +7 -0
- package/build-module/store/selectors.js +12 -0
- package/build-module/store/selectors.js.map +7 -0
- package/build-module/store/types.js +1 -0
- package/build-module/store/types.js.map +7 -0
- package/build-style/style-rtl.css +612 -0
- package/build-style/style.css +612 -0
- package/build-types/components/app/index.d.ts +6 -0
- package/build-types/components/app/index.d.ts.map +1 -0
- package/build-types/components/app/router.d.ts +7 -0
- package/build-types/components/app/router.d.ts.map +1 -0
- package/build-types/components/navigation/drilldown-item/index.d.ts +34 -0
- package/build-types/components/navigation/drilldown-item/index.d.ts.map +1 -0
- package/build-types/components/navigation/dropdown-item/index.d.ts +36 -0
- package/build-types/components/navigation/dropdown-item/index.d.ts.map +1 -0
- package/build-types/components/navigation/index.d.ts +3 -0
- package/build-types/components/navigation/index.d.ts.map +1 -0
- package/build-types/components/navigation/items.d.ts +16 -0
- package/build-types/components/navigation/items.d.ts.map +1 -0
- package/build-types/components/navigation/navigation-item/index.d.ts +28 -0
- package/build-types/components/navigation/navigation-item/index.d.ts.map +1 -0
- package/build-types/components/navigation/navigation-screen/index.d.ts +24 -0
- package/build-types/components/navigation/navigation-screen/index.d.ts.map +1 -0
- package/build-types/components/navigation/path-matching.d.ts +30 -0
- package/build-types/components/navigation/path-matching.d.ts.map +1 -0
- package/build-types/components/navigation/router-link-item.d.ts +5 -0
- package/build-types/components/navigation/router-link-item.d.ts.map +1 -0
- package/build-types/components/navigation/use-sidebar-parent.d.ts +12 -0
- package/build-types/components/navigation/use-sidebar-parent.d.ts.map +1 -0
- package/build-types/components/root/index.d.ts +3 -0
- package/build-types/components/root/index.d.ts.map +1 -0
- package/build-types/components/sidebar/index.d.ts +3 -0
- package/build-types/components/sidebar/index.d.ts.map +1 -0
- package/build-types/components/site-hub/index.d.ts +4 -0
- package/build-types/components/site-hub/index.d.ts.map +1 -0
- package/build-types/components/site-icon/index.d.ts +9 -0
- package/build-types/components/site-icon/index.d.ts.map +1 -0
- package/build-types/components/site-icon-link/index.d.ts +8 -0
- package/build-types/components/site-icon-link/index.d.ts.map +1 -0
- package/build-types/index.d.ts +6 -0
- package/build-types/index.d.ts.map +1 -0
- package/build-types/lock-unlock.d.ts +2 -0
- package/build-types/lock-unlock.d.ts.map +1 -0
- package/build-types/store/actions.d.ts +15 -0
- package/build-types/store/actions.d.ts.map +1 -0
- package/build-types/store/index.d.ts +6 -0
- package/build-types/store/index.d.ts.map +1 -0
- package/build-types/store/reducer.d.ts +7 -0
- package/build-types/store/reducer.d.ts.map +1 -0
- package/build-types/store/selectors.d.ts +7 -0
- package/build-types/store/selectors.d.ts.map +1 -0
- package/build-types/store/types.d.ts +63 -0
- package/build-types/store/types.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/components/app/index.tsx +45 -0
- package/src/components/app/router.tsx +198 -0
- package/src/components/navigation/drilldown-item/index.tsx +88 -0
- package/src/components/navigation/dropdown-item/index.tsx +134 -0
- package/src/components/navigation/dropdown-item/style.scss +23 -0
- package/src/components/navigation/index.tsx +126 -0
- package/src/components/navigation/items.tsx +93 -0
- package/src/components/navigation/navigation-item/index.tsx +88 -0
- package/src/components/navigation/navigation-item/style.scss +52 -0
- package/src/components/navigation/navigation-screen/index.tsx +147 -0
- package/src/components/navigation/navigation-screen/style.scss +34 -0
- package/src/components/navigation/path-matching.ts +149 -0
- package/src/components/navigation/router-link-item.tsx +22 -0
- package/src/components/navigation/use-sidebar-parent.ts +77 -0
- package/src/components/root/index.tsx +42 -0
- package/src/components/root/style.scss +41 -0
- package/src/components/sidebar/index.tsx +17 -0
- package/src/components/sidebar/style.scss +15 -0
- package/src/components/site-hub/index.tsx +67 -0
- package/src/components/site-hub/style.scss +54 -0
- package/src/components/site-icon/index.tsx +60 -0
- package/src/components/site-icon/style.scss +19 -0
- package/src/components/site-icon-link/index.tsx +43 -0
- package/src/components/site-icon-link/style.scss +24 -0
- package/src/index.tsx +5 -0
- package/src/lock-unlock.ts +9 -0
- package/src/store/actions.ts +23 -0
- package/src/store/index.ts +23 -0
- package/src/store/reducer.ts +31 -0
- package/src/store/selectors.ts +12 -0
- package/src/store/types.ts +70 -0
- package/src/style.scss +2 -0
- package/tsconfig.json +23 -0
- 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
|
+
}
|