mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
tour: add new tour component
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,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());
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,4 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
export { start as tourStart, stop as tourStop } from './redux/tour';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import tour from './redux/tour';
|
||||
|
||||
export default tour;
|
||||
@@ -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;
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user