tour: add new tour component

This commit is contained in:
David Lechner
2022-07-27 15:19:36 -05:00
parent 0414e08e4f
commit 8bece7d957
23 changed files with 552 additions and 22 deletions
+1
View File
@@ -8,6 +8,7 @@
- Added better error message when no files to backup ([support#681]).
- Added multi-step firmware flashing dialog.
- Added support for flashing firmware via USB DFU.
- Added an interactive introductory tour of the app.
### Fixed
- Fixed deleting files that are not open in the editor.
+1
View File
@@ -91,6 +91,7 @@
"react-dev-utils": "^12.0.1",
"react-dom": "^16.13.1",
"react-dropzone": "^14.2.2",
"react-joyride": "^2.5.0",
"react-monaco-editor": "^0.49.0",
"react-popper": "^2.3.0",
"react-redux": "^8.0.2",
+2 -1
View File
@@ -4,7 +4,8 @@
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../test';
import Activities, { Activity } from './Activities';
import Activities from './Activities';
import { Activity } from './hooks';
afterEach(() => {
cleanup();
+4 -16
View File
@@ -4,32 +4,18 @@
import './activities.scss';
import { Icon, Tab, Tabs } from '@blueprintjs/core';
import React, { useCallback, useEffect, useRef } from 'react';
import { useLocalStorage } from 'usehooks-ts';
import Explorer from '../explorer/Explorer';
import Settings from '../settings/Settings';
import { Activity, useActivitiesSelectedActivity } from './hooks';
import { useI18n } from './i18n';
/** Indicates the selected activity. */
export enum Activity {
/** No activity is selected. */
None = 'activity.none',
/** The explorer activity is selected. */
Explorer = 'activity.explorer',
/** The settings activity is selected. */
Settings = 'activity.settings',
}
/**
* React component that acts as a tab control to select activities.
*/
const Activities: React.VoidFunctionComponent = () => {
const [selectedActivity, setSelectedActivity] = useActivitiesSelectedActivity();
const i18n = useI18n();
const [selectedActivity, setSelectedActivity] = useLocalStorage(
'activities.selectedActivity',
Activity.Explorer,
);
const handleAction = useCallback(
(newActivity: Activity) => {
// if activity is already selected, select none
@@ -110,6 +96,7 @@ const Activities: React.VoidFunctionComponent = () => {
ref={tabsRef}
>
<Tab
itemID="pb-activities-explorer-tab"
aria-label={i18n.translate('explorer')}
className="pb-activities-tablist-tab"
id={Activity.Explorer}
@@ -125,6 +112,7 @@ const Activities: React.VoidFunctionComponent = () => {
onMouseDown={(e) => e.stopPropagation()}
/>
<Tab
itemID="pb-activities-settings-tab"
aria-label={i18n.translate('settings')}
className="pb-activities-tablist-tab"
id={Activity.Settings}
+22
View File
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { useLocalStorage } from 'usehooks-ts';
/** Indicates the selected activity. */
export enum Activity {
/** No activity is selected. */
None = 'activity.none',
/** The explorer activity is selected. */
Explorer = 'activity.explorer',
/** The settings activity is selected. */
Settings = 'activity.settings',
}
/**
* React hook for getting and setting the selected activity in the activity panel.
* @returns a tuple of the current state and the setter function (like useState()).
*/
export function useActivitiesSelectedActivity() {
return useLocalStorage('activities.selectedActivity', Activity.Explorer);
}
+2
View File
@@ -14,6 +14,7 @@ import { useSettingIsShowDocsEnabled } from '../settings/hooks';
import StatusBar from '../status-bar/StatusBar';
import Terminal from '../terminal/Terminal';
import Toolbar from '../toolbar/Toolbar';
import Tour from '../tour/Tour';
import { isMacOS } from '../utils/os';
const Docs: React.VFC = () => {
@@ -196,6 +197,7 @@ const App: React.VFC = () => {
</div>
</div>
<StatusBar />
<Tour />
</div>
);
};
+4
View File
@@ -8,6 +8,8 @@ import React, { useRef } from 'react';
import { FocusRing, useButton } from 'react-aria';
type ButtonProps = {
/** Optional DOM ID for the button. */
id?: string;
/** The label for the button. */
label: string;
/** If true, the label will not be visible (will use aria-label instead). */
@@ -28,6 +30,7 @@ type ButtonProps = {
/** Similar to Blueprint.js button with better accessibility. */
export const Button: React.VoidFunctionComponent<ButtonProps> = ({
id,
label,
hideLabel,
description,
@@ -59,6 +62,7 @@ export const Button: React.VoidFunctionComponent<ButtonProps> = ({
{/* useButton() breaks the native browser :focus-visible :-/ */}
<FocusRing focusRingClass="pb-focus-ring">
<button
id={id}
className={classNames(Classes.BUTTON, 'pb-focus-managed', {
[Classes.ACTIVE]: isPressed,
[Classes.LOADING]: loading,
+4 -2
View File
@@ -156,10 +156,12 @@ const FileActionButtonGroup: React.VoidFunctionComponent<ActionButtonGroupProps>
);
};
// matches ID in tour component
const archiveButtonId = 'pb-explorer-archive-button';
const newButtonId = 'pb-explorer-add-button';
const Header: React.VoidFunctionComponent = () => {
const archiveButtonId = useId();
const exportButtonId = useId();
const newButtonId = useId();
const dispatch = useDispatch();
const i18n = useI18n();
+2
View File
@@ -11,6 +11,7 @@ import fileStorage from './fileStorage/reducers';
import firmware from './firmware/reducers';
import hub from './hub/reducers';
import bootloader from './lwp3-bootloader/reducers';
import tour from './tour/reducers';
/**
* Root reducer for redux store.
@@ -24,6 +25,7 @@ export const rootReducer = combineReducers({
fileStorage,
firmware,
hub,
tour,
});
/**
+2
View File
@@ -96,6 +96,7 @@ const Settings: React.VoidFunctionComponent = () => {
</FormGroup>
<FormGroup label={i18n.translate('firmware.title')}>
<Button
id="pb-settings-flash-pybricks-button"
minimal={true}
icon="download"
label={i18n.translate('firmware.flashPybricksButton.label')}
@@ -103,6 +104,7 @@ const Settings: React.VoidFunctionComponent = () => {
/>
<InstallPybricksDialog />
<Button
id="pb-settings-flash-official-button"
minimal={true}
icon="download"
label={i18n.translate('firmware.flashLegoButton.label')}
+9 -2
View File
@@ -9,13 +9,17 @@ import BluetoothButton from './buttons/bluetooth/BluetoothButton';
import ReplButton from './buttons/repl/ReplButton';
import RunButton from './buttons/run/RunButton';
import StopButton from './buttons/stop/StopButton';
import TourButton from './buttons/tour/TourButton';
import './toolbar.scss';
// matches ID in tour component
const bluetoothButtonId = 'pb-toolbar-bluetooth-button';
const runButtonId = 'pb-toolbar-run-button';
const tourButtonId = 'pb-toolbar-tour-button';
const Toolbar: React.VFC = () => {
const flashButtonId = useId();
const bluetoothButtonId = useId();
const runButtonId = useId();
const stopButtonId = useId();
const replButtonId = useId();
@@ -29,6 +33,9 @@ const Toolbar: React.VFC = () => {
<StopButton id={stopButtonId} />
<ReplButton id={replButtonId} />
</ButtonGroup>
<ButtonGroup className="pb-toolbar-group pb-align-right">
<TourButton id={tourButtonId} />
</ButtonGroup>
</UtilsToolbar>
);
};
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../../../test';
import { HubRuntimeState } from '../../../hub/reducers';
import { tourStart } from '../../../tour/actions';
import TourButton from './TourButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', async () => {
const [user, button, dispatch] = testRender(<TourButton id="test-tour-button" />, {
hub: { runtime: HubRuntimeState.Running },
});
await user.click(button.getByRole('button', { name: 'Tour Pybricks Code' }));
expect(dispatch).toHaveBeenCalledWith(tourStart());
});
+29
View File
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import React from 'react';
import { useDispatch } from 'react-redux';
import { appName } from '../../../app/constants';
import { tourStart } from '../../../tour/actions';
import ActionButton, { ActionButtonProps } from '../../ActionButton';
import { useI18n } from './i18n';
import icon from './icon.svg';
type TourButtonProps = Pick<ActionButtonProps, 'id'>;
const TourButton: React.VoidFunctionComponent<TourButtonProps> = ({ id }) => {
const i18n = useI18n();
const dispatch = useDispatch();
return (
<ActionButton
id={id}
label={i18n.translate('label', { appName })}
tooltip={i18n.translate('tooltip', { appName })}
icon={icon}
onAction={() => dispatch(tourStart())}
/>
);
};
export default TourButton;
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { useI18n as useShopifyI18n } from '@shopify/react-i18n';
import type { TypedI18n } from '../../../i18n';
import type translations from './translations/en.json';
export function useI18n(): TypedI18n<typeof translations> {
// istanbul ignore next: babel-loader rewrites this line
const [i18n] = useShopifyI18n();
return i18n;
}
+1
View File
@@ -0,0 +1 @@
<svg width="50" height="50" version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -270.54)"><path d="m1.9234 276.13c-1.0655 0-1.9234 0.85655-1.9234 1.9213v10.588c0 1.0648 0.85787 1.9213 1.9234 1.9213h3.3755c1.0655 0 1.9234-0.85653 1.9234-1.9213v-0.47646h12.014v0.47646c0 1.0648 0.85787 1.9213 1.9234 1.9213h3.3755c1.0655 0 1.9234-0.85653 1.9234-1.9213v-10.588c0-1.0648-0.85786-1.9213-1.9234-1.9213h-3.3755c-5.4826 0.0358-11.132 0.0125-15.861 0zm0.49609 2.4123h21.644v9.617h-2.3978v-2.3916h-2.0691v-0.89193h-0.74311v0.89193h-1.664v-0.89193h-0.74001v0.89193h-1.664v-0.89193h-0.74001v0.89193h-1.664v-0.89193h-0.74311v0.89193h-1.664v-0.89193h-0.74104v0.89193h-1.664v-0.89193h-0.68316v0.89193h-2.0691v2.3916h-2.3978v-3.2835z" fill="#fcfcfc"/><ellipse cx="8.4499" cy="281.03" rx="1.7076" ry="1.7064" fill="#fff"/><ellipse cx="18.033" cy="281.03" rx="1.7076" ry="1.7064" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 937 B

@@ -0,0 +1,4 @@
{
"label": "Tour {appName}",
"tooltip": "Take a quick tour of {appName}"
}
+255
View File
@@ -0,0 +1,255 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { Icon } from '@blueprintjs/core';
import React, { useCallback, useMemo, useState } from 'react';
import Joyride, {
ACTIONS,
CallBackProps,
EVENTS,
Locale,
STATUS,
Step,
Styles,
} from 'react-joyride';
import { useDispatch } from 'react-redux';
import { useEffectOnce, useLocalStorage, useTernaryDarkMode } from 'usehooks-ts';
import { Activity, useActivitiesSelectedActivity } from '../activities/hooks';
import { appName } from '../app/constants';
import { useSelector } from '../reducers';
import { tourStart, tourStop } from './actions';
import { useI18n } from './i18n';
const WelcomeStep = React.memo(function WelcomeStep() {
const i18n = useI18n();
return (
<>
<p>{i18n.translate('steps.welcome.message', { appName })}</p>
<p>
{i18n.translate('steps.welcome.action', {
next: <strong>{i18n.translate('next')}</strong>,
})}
</p>
</>
);
});
const AddFileStep = React.memo(function AddFileStep() {
const i18n = useI18n();
return (
<p>
{i18n.translate('steps.newFile.message', {
icon: <Icon icon="plus" style={{ verticalAlign: 'text-top' }} />,
})}
</p>
);
});
const BackupFilesStep = React.memo(function BackupFilesStep() {
const i18n = useI18n();
return (
<>
<p>{i18n.translate('steps.backupFiles.message')}</p>
<p>
{i18n.translate('steps.backupFiles.action', {
icon: <Icon icon="archive" style={{ verticalAlign: 'text-top' }} />,
})}
</p>
</>
);
});
const FlashPybricksFirmwareStep = React.memo(function FlashPybricksFirmwareStep() {
const i18n = useI18n();
return (
<p>
{i18n.translate('steps.flashPybricksFirmware.message', {
appName,
})}
</p>
);
});
const RestoreOfficialFirmwareStep = React.memo(function RestoreOfficialFirmwareStep() {
const i18n = useI18n();
return <p>{i18n.translate('steps.restoreOfficialFirmware.message')}</p>;
});
const ConnectToHubStep = React.memo(function ConnectToHubStep() {
const i18n = useI18n();
return <p>{i18n.translate('steps.connectToHub.message')}</p>;
});
const DownloadAndRunStep = React.memo(function DownloadAndRunStep() {
const i18n = useI18n();
return (
<p>
{i18n.translate('steps.downloadAndRun.message', {
icon: <Icon icon="play" style={{ verticalAlign: 'text-top' }} />,
})}
</p>
);
});
const Tour: React.VoidFunctionComponent = () => {
const [selectedActivity, setSelectedActivity] = useActivitiesSelectedActivity();
const [showOnStartup, setShowOnStartup] = useLocalStorage(
'tour.showOnStartup',
true,
);
const [stepIndex, setStepIndex] = useState(0);
const { isRunning } = useSelector((s) => s.tour);
const { isDarkMode } = useTernaryDarkMode();
const dispatch = useDispatch();
const i18n = useI18n();
const steps = useMemo<Step[]>(
() => [
{
target: '#pb-toolbar-tour-button',
content: <WelcomeStep />,
disableBeacon: true,
},
{
target:
selectedActivity === Activity.Explorer
? '#pb-explorer-add-button'
: '[itemId=pb-activities-explorer-tab]',
content: <AddFileStep />,
disableBeacon: selectedActivity === Activity.Explorer,
},
{
target:
selectedActivity === Activity.Explorer
? '#pb-explorer-archive-button'
: '[itemId=pb-activities-explorer-tab]',
content: <BackupFilesStep />,
disableBeacon: selectedActivity === Activity.Explorer,
},
{
target:
selectedActivity === Activity.Settings
? '#pb-settings-flash-pybricks-button'
: '[itemId=pb-activities-settings-tab]',
content: <FlashPybricksFirmwareStep />,
disableBeacon: selectedActivity === Activity.Settings,
},
{
target:
selectedActivity === Activity.Settings
? '#pb-settings-flash-official-button'
: '[itemId=pb-activities-settings-tab]',
content: <RestoreOfficialFirmwareStep />,
disableBeacon: selectedActivity === Activity.Settings,
},
{
target: '#pb-toolbar-bluetooth-button',
content: <ConnectToHubStep />,
disableBeacon: true,
},
{
target: '#pb-toolbar-run-button',
content: <DownloadAndRunStep />,
disableBeacon: true,
},
],
[selectedActivity, i18n],
);
const styles = useMemo<Styles>(
() => ({
// colors come from variables.scss
options: {
// $pybricks-blue
primaryColor: '#0088ce',
// $pt-dark-text-color / $pt-text-color
textColor: isDarkMode ? '#f6f7f9' : '#1c2127',
// $pt-dark-app-background-color / $pt-app-background-color
backgroundColor: isDarkMode ? '#252a31' : '#f6f7f9',
arrowColor: isDarkMode ? '#252a31' : '#f6f7f9',
},
}),
[isDarkMode],
);
const locale = useMemo<Locale>(
() => ({
back: i18n.translate('back'),
close: i18n.translate('close'),
last: i18n.translate('last'),
next: i18n.translate('next'),
open: i18n.translate('open'),
skip: i18n.translate('skip'),
}),
[i18n],
);
const callback = useCallback(
(event: CallBackProps) => {
if (event.action === ACTIONS.CLOSE || event.status === STATUS.FINISHED) {
dispatch(tourStop());
setStepIndex(0);
}
if (event.type === EVENTS.STEP_AFTER) {
const nextIndex = stepIndex + (event.action === ACTIONS.PREV ? -1 : 1);
const nextStep = steps.at(nextIndex);
// Some components may not be mounted when the next/back button
// is pressed. If this is the case, first target the tab, then
// request to activate that tab. When the tab panel is mounted,
// the target will automatically update.
if (typeof nextStep?.target === 'string') {
if (nextStep.target.startsWith('[itemId=pb-activities-explorer-')) {
setSelectedActivity(Activity.Explorer);
} else if (
nextStep.target.startsWith('[itemId=pb-activities-settings-')
) {
setSelectedActivity(Activity.Settings);
}
}
setStepIndex(nextIndex);
}
},
[
dispatch,
selectedActivity,
setSelectedActivity,
stepIndex,
setStepIndex,
steps,
],
);
// automatically show the tour on the first run only
useEffectOnce(() => {
if (showOnStartup) {
dispatch(tourStart());
setShowOnStartup(false);
}
});
return (
<Joyride
run={isRunning}
stepIndex={stepIndex}
continuous={true}
showProgress={true}
steps={steps}
styles={styles}
locale={locale}
callback={callback}
/>
);
};
export default Tour;
+4
View File
@@ -0,0 +1,4 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
export { start as tourStart, stop as tourStop } from './redux/tour';
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { useI18n as useShopifyI18n } from '@shopify/react-i18n';
import type { TypedI18n } from '../i18n';
import type translations from './translations/en.json';
export function useI18n(): TypedI18n<typeof translations> {
// istanbul ignore next: babel-loader rewrites this line
const [i18n] = useShopifyI18n();
return i18n;
}
+6
View File
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import tour from './redux/tour';
export default tour;
+28
View File
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { createSlice } from '@reduxjs/toolkit';
type State = {
isRunning: boolean;
};
const initialState: State = {
isRunning: false,
};
const slice = createSlice({
name: 'tour',
initialState,
reducers: {
start(state) {
state.isRunning = true;
},
stop(state) {
state.isRunning = false;
},
},
});
export const { start, stop } = slice.actions;
export default slice.reducer;
+33
View File
@@ -0,0 +1,33 @@
{
"back": "Back",
"close": "Close",
"last": "Last",
"next": "Next",
"open": "Open the dialog",
"skip": "Skip",
"steps": {
"welcome": {
"message": "Welcome to {appName}.",
"action": "Click {next} to discover key features."
},
"newFile": {
"message": "Click the {icon} button to create a new program."
},
"backupFiles": {
"message": "Don't lose your work.",
"action": "Click the {icon} button to backup all of your programs."
},
"flashPybricksFirmware": {
"message": "{appName} requires a special firmware to be flashed to your hub in order to run the program on the hub."
},
"restoreOfficialFirmware": {
"message": "You can always restore the official LEGO® firmware after using the special Pybricks firmware."
},
"connectToHub": {
"message": "One you have flashed the Pybricks firmware, click the Bluetooth button to connect to the hub."
},
"downloadAndRun": {
"message": "After you are connected to the hub, click the {icon} button to download and run your program."
}
}
}
+92 -1
View File
@@ -1802,6 +1802,13 @@ __metadata:
languageName: node
linkType: hard
"@gilbarbara/deep-equal@npm:^0.1.1":
version: 0.1.1
resolution: "@gilbarbara/deep-equal@npm:0.1.1"
checksum: d904761847320587cbb2a76310acc3344d241201e4e6521e9f88b2f4eb7d8a13aed2081fca1cad625ef78390651e5bbfe914ff5d6925a9f40cf305676a9e1f20
languageName: node
linkType: hard
"@humanwhocodes/config-array@npm:^0.9.2":
version: 0.9.5
resolution: "@humanwhocodes/config-array@npm:0.9.5"
@@ -2410,6 +2417,7 @@ __metadata:
react-dev-utils: ^12.0.1
react-dom: ^16.13.1
react-dropzone: ^14.2.2
react-joyride: ^2.5.0
react-monaco-editor: ^0.49.0
react-popper: ^2.3.0
react-redux: ^8.0.2
@@ -8000,6 +8008,13 @@ __metadata:
languageName: node
linkType: hard
"exenv@npm:^1.2.2":
version: 1.2.2
resolution: "exenv@npm:1.2.2"
checksum: a894f3b60ab8419e0b6eec99c690a009c8276b4c90655ccaf7d28faba2de3a6b93b3d92210f9dc5efd36058d44f04098f6bbccef99859151104bfd49939904dc
languageName: node
linkType: hard
"exit@npm:^0.1.2":
version: 0.1.2
resolution: "exit@npm:0.1.2"
@@ -9271,6 +9286,13 @@ __metadata:
languageName: node
linkType: hard
"is-lite@npm:^0.8.2":
version: 0.8.2
resolution: "is-lite@npm:0.8.2"
checksum: 0ee62cb238c2a044f58d1cd139fb0b48026c407ec8625ee6572b417f164e17ec937f0a0785f466e320749a796c316a3b78dcb4b520f7ddd4b9de38ad5a23d70f
languageName: node
linkType: hard
"is-module@npm:^1.0.0":
version: 1.0.0
resolution: "is-module@npm:1.0.0"
@@ -11478,7 +11500,7 @@ __metadata:
languageName: node
linkType: hard
"popper.js@npm:^1.14.4, popper.js@npm:^1.16.1":
"popper.js@npm:^1.14.4, popper.js@npm:^1.16.0, popper.js@npm:^1.16.1":
version: 1.16.1
resolution: "popper.js@npm:1.16.1"
checksum: c56ae5001ec50a77ee297a8061a0221d99d25c7348d2e6bcd3e45a0d0f32a1fd81bca29d46cb0d4bdf13efb77685bd6a0ce93f9eb3c608311a461f945fffedbe
@@ -12699,6 +12721,24 @@ __metadata:
languageName: node
linkType: hard
"react-floater@npm:^0.7.5":
version: 0.7.5
resolution: "react-floater@npm:0.7.5"
dependencies:
deepmerge: ^4.2.2
exenv: ^1.2.2
is-lite: ^0.8.2
popper.js: ^1.16.0
react-proptype-conditional-require: ^1.0.4
tree-changes: ^0.9.1
peerDependencies:
prop-types: ^15.7.2
react: 15 - 18
react-dom: 15 - 18
checksum: 72d0ccd15d93e08885e9d5ff59be757df198fa48e796b3ce48138e8ba0f393df4b246c18e4887ad62214607f75041366b34c0d4f0edc7e70e774eb236c377698
languageName: node
linkType: hard
"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
@@ -12720,6 +12760,26 @@ __metadata:
languageName: node
linkType: hard
"react-joyride@npm:^2.5.0":
version: 2.5.0
resolution: "react-joyride@npm:2.5.0"
dependencies:
deepmerge: ^4.2.2
exenv: ^1.2.2
is-lite: ^0.8.2
react-floater: ^0.7.5
react-is: ^16.13.1
scroll: ^3.0.1
scrollparent: ^2.0.1
tree-changes: ^0.9.1
peerDependencies:
prop-types: ^15.0.0
react: 15 - 18
react-dom: 15 - 18
checksum: 2d15d1b6a6e6e830af0e594b53b5318dd91a3b57f183157404375d1760a36b4f91acf89c69e73fed7ba5d9d0c13bdc23b2ea426a64259635321a4aaa020aae9d
languageName: node
linkType: hard
"react-monaco-editor@npm:^0.49.0":
version: 0.49.0
resolution: "react-monaco-editor@npm:0.49.0"
@@ -12764,6 +12824,13 @@ __metadata:
languageName: node
linkType: hard
"react-proptype-conditional-require@npm:^1.0.4":
version: 1.0.4
resolution: "react-proptype-conditional-require@npm:1.0.4"
checksum: 78f82d15b2c77c14fd8fbcbbed279850df3a856984aacd519ee6c2162e034b114b8ac47c00157b84ef7c98c0711d933a0177d9d54555629cf381f54341bb0e8f
languageName: node
linkType: hard
"react-redux@npm:^8.0.2":
version: 8.0.2
resolution: "react-redux@npm:8.0.2"
@@ -13385,6 +13452,20 @@ __metadata:
languageName: node
linkType: hard
"scroll@npm:^3.0.1":
version: 3.0.1
resolution: "scroll@npm:3.0.1"
checksum: e6b045347adace30035073882e6ef2af7e1c81dd611faf3a578ca8cd0d1a3a9da54932dd97ed6fd99c9573351c758fa50e6d8ed4afb5bd3a33794b6c48d25922
languageName: node
linkType: hard
"scrollparent@npm:^2.0.1":
version: 2.0.1
resolution: "scrollparent@npm:2.0.1"
checksum: 9ff2b29c1233431ebd0910e5f8db4a058836ee0292d1bb18042fbb4d2175c3ed73fa28a71a96074ac6848173185238ad1139bdefc3863be4808df981175cb807
languageName: node
linkType: hard
"select-hose@npm:^2.0.0":
version: 2.0.0
resolution: "select-hose@npm:2.0.0"
@@ -14415,6 +14496,16 @@ __metadata:
languageName: node
linkType: hard
"tree-changes@npm:^0.9.1":
version: 0.9.1
resolution: "tree-changes@npm:0.9.1"
dependencies:
"@gilbarbara/deep-equal": ^0.1.1
is-lite: ^0.8.2
checksum: 6749f0213dbbfda592326250cdbbc9bb0f03c6c22e042e7089b1c4d6ab2fc1aa5cde0e02d31d5d7a32c901301146a01ed0c9b90a94007798681aa6af76bd6428
languageName: node
linkType: hard
"tryer@npm:^1.0.1":
version: 1.0.1
resolution: "tryer@npm:1.0.1"