diff --git a/src/activities/Activities.test.tsx b/src/activities/Activities.test.tsx new file mode 100644 index 00000000..8b25ebfa --- /dev/null +++ b/src/activities/Activities.test.tsx @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { useIsFirstRender } from 'usehooks-ts'; +import { testRender } from '../../test'; +import { Activity, useActivities } from './Activities'; + +afterEach(() => { + cleanup(); + jest.resetAllMocks(); + localStorage.clear(); +}); + +type TestActivityProps = { + expectedActivity: Activity; +}; + +const TestActivity: React.VoidFunctionComponent = ({ + expectedActivity, +}) => { + const [selectedActivity, activitiesComponent] = useActivities(); + + if (useIsFirstRender()) { + expect(selectedActivity).toBe(expectedActivity); + } + + return activitiesComponent; +}; + +describe('Activities', () => { + it('should select explorer by default', () => { + const [activities] = testRender( + , + ); + + const tab = activities.getByRole('tab', { name: 'File Explorer' }); + + expect(tab).toHaveAttribute('aria-selected', 'true'); + }); + + it('should use localStorage for default value', () => { + localStorage.setItem( + 'activities.selectedActivity', + JSON.stringify(Activity.Settings), + ); + + const [activities] = testRender( + , + ); + + const tab = activities.getByRole('tab', { name: 'Settings & Help' }); + + expect(tab).toHaveAttribute('aria-selected', 'true'); + }); + + it('should select none when clicking already selected tab', () => { + const [activities] = testRender( + , + ); + + const explorerTab = activities.getByRole('tab', { name: 'File Explorer' }); + + for (const tab of activities.getAllByRole('tab')) { + expect(tab).toHaveAttribute( + 'aria-selected', + tab === explorerTab ? 'true' : 'false', + ); + } + + userEvent.click(explorerTab); + + for (const tab of activities.getAllByRole('tab')) { + expect(tab).toHaveAttribute('aria-selected', 'false'); + } + }); + + it('should select new tab when clicking not already selected tab', () => { + const [activities] = testRender( + , + ); + + const explorerTab = activities.getByRole('tab', { name: 'File Explorer' }); + const settingsTab = activities.getByRole('tab', { name: 'Settings & Help' }); + + for (const tab of activities.getAllByRole('tab')) { + expect(tab).toHaveAttribute( + 'aria-selected', + tab === explorerTab ? 'true' : 'false', + ); + } + + userEvent.click(settingsTab); + + for (const tab of activities.getAllByRole('tab')) { + expect(tab).toHaveAttribute( + 'aria-selected', + tab === settingsTab ? 'true' : 'false', + ); + } + }); +}); diff --git a/src/activities/Activities.tsx b/src/activities/Activities.tsx new file mode 100644 index 00000000..eb509b38 --- /dev/null +++ b/src/activities/Activities.tsx @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { Classes, Icon, IconName } from '@blueprintjs/core'; +import { useI18n } from '@shopify/react-i18n'; +import React, { useCallback, useMemo } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; +import { I18nId } 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', +} + +type ActivityTabProps = { + /** The label for the tab. */ + label: string; + /** The icon for the tab. */ + icon: IconName; + /** Controls the selected state of the tab. */ + selected: boolean; + /** Callback called when the tab is clicked. */ + onClick: () => void; +}; + +/** + * React component for tabs in {@link Activities}. + */ +const ActivityTab: React.VoidFunctionComponent = ({ + label, + icon, + selected, + onClick, +}) => { + // not using Button component so we can set role to "tab" + return ( +
c) + .join(' ')} + {...{ onClick }} + > + +
+ ); +}; + +type ActivitiesProps = { + /** The currently selected activity. */ + selectedActivity: Activity; + /** Callback called when a tab is clicked. */ + onAction: (activity: Activity) => void; +}; + +/** + * React component that acts as a tab control to select activities. + */ +const Activities: React.VoidFunctionComponent = ({ + selectedActivity, + onAction, +}) => { + // istanbul ignore next: babel-loader rewrites this line + const [i18n] = useI18n(); + + return ( +
+ onAction(Activity.Explorer)} + /> + onAction(Activity.Settings)} + /> +
+ ); +}; + +/** + * React hook to get selected state and component. + * @returns The current selected activity (state) and the activity component. + */ +export function useActivities(): [ + selectedActivity: Activity, + activitiesComponent: React.ReactElement, +] { + const [selectedActivity, setSelectedActivity] = useLocalStorage( + 'activities.selectedActivity', + Activity.Explorer, + ); + + const handleAction = useCallback( + (newActivity: Activity) => { + // if activity is already selected, select none + if (selectedActivity === newActivity) { + setSelectedActivity(Activity.None); + } else { + // otherwise select the new activity + setSelectedActivity(newActivity); + } + }, + [selectedActivity, setSelectedActivity], + ); + + const activitiesComponent = useMemo( + () => ( + handleAction(a)} + /> + ), + [Activities, selectedActivity, handleAction], + ); + + return [selectedActivity, activitiesComponent]; +} diff --git a/src/toolbar/buttons/settings/i18n.test.ts b/src/activities/i18n.test.ts similarity index 75% rename from src/toolbar/buttons/settings/i18n.test.ts rename to src/activities/i18n.test.ts index b8f901e0..56d5d167 100644 --- a/src/toolbar/buttons/settings/i18n.test.ts +++ b/src/activities/i18n.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2022 The Pybricks Authors -import { lookup } from '../../../../test'; +import { lookup } from '../../test'; import { I18nId } from './i18n'; import en from './translations/en.json'; diff --git a/src/activities/i18n.ts b/src/activities/i18n.ts new file mode 100644 index 00000000..5d4ea7ef --- /dev/null +++ b/src/activities/i18n.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors +// +// Explorer translation keys. + +export enum I18nId { + Title = 'title', + Explorer = 'explorer', + Settings = 'settings', +} diff --git a/src/activities/translations/en.json b/src/activities/translations/en.json new file mode 100644 index 00000000..67d7cafb --- /dev/null +++ b/src/activities/translations/en.json @@ -0,0 +1,5 @@ +{ + "title": "Activities", + "explorer": "File Explorer", + "settings": "Settings & Help" +} diff --git a/src/app/App.tsx b/src/app/App.tsx index 6a5c9004..528ad452 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -5,8 +5,10 @@ import { Classes } from '@blueprintjs/core'; import React, { useEffect, useState } from 'react'; import SplitterLayout from 'react-splitter-layout'; import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts'; +import { Activity, useActivities } from '../activities/Activities'; import Editor from '../editor/Editor'; import Explorer from '../explorer/Explorer'; +import Settings from '../settings/Settings'; import { useSettingIsShowDocsEnabled } from '../settings/hooks'; import StatusBar from '../status-bar/StatusBar'; import Terminal from '../terminal/Terminal'; @@ -155,16 +157,20 @@ const App: React.VFC = () => { return () => removeEventListener('keydown', listener); }, []); + const [selectedActivity, activitiesComponent] = useActivities(); + return (
- -
- +
+ {activitiesComponent} + {selectedActivity !== Activity.None && ( +
+ {selectedActivity === Activity.Explorer && } + {selectedActivity === Activity.Settings && } +
+ )} {/* need a container with position: relative; for SplitterLayout since it uses position: absolute; */} -
+
{ secondaryInitialSize={terminalSplit} onSecondaryPaneSizeChange={setTerminalSplit} > - +
+ + +
diff --git a/src/app/app.scss b/src/app/app.scss index 77b38e7b..ac04ec7d 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -16,9 +16,60 @@ .pb-app-body { // height makes everything fit without scrolling - height: calc( - var(--pb-vh, 100vh) - #{pb.$toolbar-height} - #{pb.$status-bar-height} - ) !important; + height: calc(var(--pb-vh, 100vh) - #{pb.$status-bar-height}) !important; + position: relative; + display: flex; + flex-direction: row; + + $switcher-width: 50px; + + .pb-activity-tablist { + width: $switcher-width; + $padding: bp.$pt-grid-size * 0.5; + padding: $padding; + gap: $padding; + display: flex; + flex-direction: column; + justify-content: flex-start; + @include pb.background-contrast(6%); + } + + .pb-app-activity-view { + width: 250px; + overflow: hidden; + padding: bp.$pt-grid-size; + + // on small screens, make the activity view an overlay instead of inline + @media screen and (max-width: pb.$narrow-screen-limit) { + position: absolute; + top: 0px; + left: $switcher-width; + height: 100%; + z-index: bp.$pt-z-index-overlay; + background-color: bp.$pt-app-background-color; + + .#{bp.$ns}-dark & { + background-color: bp.$pt-dark-app-background-color; + } + } + } + + .pb-app-main { + min-width: 0; + flex: 1 1 auto; + @include pb.background-contrast(4%); + } +} + +.pb-app-editor { + height: 100%; + display: flex; + flex-direction: column; + + & .pb-editor { + min-height: 0; + flex: 1 1 auto; + } } // make layout splitter match app color scheme diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index ffc9a504..9d4ca65c 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -410,7 +410,7 @@ const Editor: React.VFC = () => { ); return ( -
+
editor?.focus()} i18n={i18n} /> editor?.layout()}> { cleanup(); @@ -14,9 +14,7 @@ afterEach(() => { describe('showDocs setting switch', () => { it('should toggle setting', async () => { - const [settings] = testRender( - undefined} />, - ); + const [settings] = testRender(); const showDocs = settings.getByLabelText('Documentation'); expect(showDocs).toBeChecked(); @@ -28,9 +26,7 @@ describe('showDocs setting switch', () => { describe('darkMode setting switch', () => { it('should toggle setting', async () => { - const [settings] = testRender( - undefined} />, - ); + const [settings] = testRender(); const darkMode = settings.getByLabelText('Dark mode'); expect(darkMode).not.toBeChecked(); @@ -45,11 +41,10 @@ describe('darkMode setting switch', () => { expect(localStorage.getItem('usehooks-ts-ternary-dark-mode')).toBe('"light"'); }); }); + describe('flashCurrentProgram setting switch', () => { it('should toggle the setting', () => { - const [settings] = testRender( - undefined} />, - ); + const [settings] = testRender(); expect(localStorage.getItem('setting.flashCurrentProgram')).toBe(null); @@ -66,9 +61,7 @@ describe('hubName setting', () => { // old settings did not use json format, so lack quotes localStorage.setItem('setting.hubName', 'old name'); - const [settings] = testRender( - undefined} />, - ); + const [settings] = testRender(); const textBox = settings.getByLabelText('Hub name'); @@ -76,9 +69,7 @@ describe('hubName setting', () => { }); it('should update the setting', () => { - const [settings] = testRender( - undefined} />, - ); + const [settings] = testRender(); expect(localStorage.getItem('setting.hubName')).toBe(null); @@ -91,9 +82,7 @@ describe('hubName setting', () => { describe('about dialog', () => { it('should open the dialog when the button is clicked', async () => { - const [settings] = testRender( - undefined} />, - ); + const [settings] = testRender(); const appName = process.env.REACT_APP_NAME; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx new file mode 100644 index 00000000..1fd4d206 --- /dev/null +++ b/src/settings/Settings.tsx @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021-2022 The Pybricks Authors + +import { + AnchorButton, + Button, + ButtonGroup, + ControlGroup, + FormGroup, + Icon, + InputGroup, + Intent, + Label, + Switch, +} from '@blueprintjs/core'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import { useI18n } from '@shopify/react-i18n'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useTernaryDarkMode } from 'usehooks-ts'; +import AboutDialog from '../about/AboutDialog'; +import { appCheckForUpdate, appReload, appShowInstallPrompt } from '../app/actions'; +import { + pybricksBugReportsUrl, + pybricksGitterUrl, + pybricksProjectsUrl, + pybricksSupportUrl, + tooltipDelay, +} from '../app/constants'; +import { pseudolocalize } from '../i18n'; +import { useSelector } from '../reducers'; +import ExternalLinkIcon from '../utils/ExternalLinkIcon'; +import { isMacOS } from '../utils/os'; +import { + useSettingFlashCurrentProgram, + useSettingHubName, + useSettingIsShowDocsEnabled, +} from './hooks'; +import { I18nId } from './i18n'; +import './settings.scss'; + +const Settings: React.VoidFunctionComponent = () => { + const { isSettingShowDocsEnabled, setIsSettingShowDocsEnabled } = + useSettingIsShowDocsEnabled(); + const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false); + const { isDarkMode, setTernaryDarkMode } = useTernaryDarkMode(); + + const [isFlashCurrentProgramEnabled, setIsFlashCurrentProgramEnabled] = + useSettingFlashCurrentProgram(); + const isServiceWorkerRegistered = useSelector( + (s) => s.app.isServiceWorkerRegistered, + ); + const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate); + const updateAvailable = useSelector((s) => s.app.updateAvailable); + const hasUnresolvedInstallPrompt = useSelector( + (s) => s.app.hasUnresolvedInstallPrompt, + ); + const promptingInstall = useSelector((s) => s.app.promptingInstall); + const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse); + const { hubName, isHubNameValid, setHubName } = useSettingHubName(); + + const dispatch = useDispatch(); + + // istanbul ignore next: babel-loader rewrites this line + const [i18n] = useI18n(); + + return ( +
+ {isMacOS() ? 'Cmd' : 'Ctrl'}-+, + out: {isMacOS() ? 'Cmd' : 'Ctrl'}--, + })} + > + + + setIsSettingShowDocsEnabled( + (e.target as HTMLInputElement).checked, + ) + } + /> + + + + setTernaryDarkMode( + (e.target as HTMLInputElement).checked + ? 'dark' + : 'light', + ) + } + /> + + + + + + setIsFlashCurrentProgramEnabled( + (e.target as HTMLInputElement).checked, + ) + } + /> + + + + + + setHubName(e.currentTarget.value)} + onMouseOver={(e) => e.preventDefault()} + className="pb-hub-name-input" + intent={isHubNameValid ? Intent.NONE : Intent.DANGER} + placeholder="Pybricks Hub" + rightElement={ + isHubNameValid ? undefined : ( + + + + ) + } + /> + + + + + + {i18n.translate(I18nId.HelpProjectsLabel)} + + + + {i18n.translate(I18nId.HelpSupportLabel)} + + + + {i18n.translate(I18nId.HelpChatLabel)} + + + + {i18n.translate(I18nId.HelpBugsLabel)} + + + setIsAboutDialogOpen(false)} + /> + + + + + {hasUnresolvedInstallPrompt && ( + + )} + {isServiceWorkerRegistered && !updateAvailable && ( + + )} + {isServiceWorkerRegistered && updateAvailable && ( + + )} + + + + {process.env.NODE_ENV === 'development' && ( + + pseudolocalize(!i18n.pseudolocalize)} + label="Pseudolocalize" + /> + + )} +
+ ); +}; + +export default Settings; diff --git a/src/settings/SettingsDrawer.tsx b/src/settings/SettingsDrawer.tsx deleted file mode 100644 index f9054f06..00000000 --- a/src/settings/SettingsDrawer.tsx +++ /dev/null @@ -1,317 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors - -import { - AnchorButton, - Button, - ButtonGroup, - Classes, - ControlGroup, - Drawer, - DrawerSize, - FormGroup, - Icon, - InputGroup, - Intent, - Label, - Switch, -} from '@blueprintjs/core'; -import { Tooltip2 } from '@blueprintjs/popover2'; -import { useI18n } from '@shopify/react-i18n'; -import React, { useCallback, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { useTernaryDarkMode } from 'usehooks-ts'; -import AboutDialog from '../about/AboutDialog'; -import { appCheckForUpdate, appReload, appShowInstallPrompt } from '../app/actions'; -import { - pybricksBugReportsUrl, - pybricksGitterUrl, - pybricksProjectsUrl, - pybricksSupportUrl, - tooltipDelay, -} from '../app/constants'; -import { pseudolocalize } from '../i18n'; -import { useSelector } from '../reducers'; -import ExternalLinkIcon from '../utils/ExternalLinkIcon'; -import { isMacOS } from '../utils/os'; -import { - useSettingFlashCurrentProgram, - useSettingHubName, - useSettingIsShowDocsEnabled, -} from './hooks'; -import { I18nId } from './i18n'; -import './settings.scss'; - -type SettingsProps = { - isOpen: boolean; - onClose(): void; -}; - -const SettingsDrawer: React.VoidFunctionComponent = ({ - isOpen, - onClose, -}) => { - const { isSettingShowDocsEnabled, setIsSettingShowDocsEnabled } = - useSettingIsShowDocsEnabled(); - const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false); - const { isDarkMode, setTernaryDarkMode } = useTernaryDarkMode(); - - const [isFlashCurrentProgramEnabled, setIsFlashCurrentProgramEnabled] = - useSettingFlashCurrentProgram(); - const isServiceWorkerRegistered = useSelector( - (s) => s.app.isServiceWorkerRegistered, - ); - const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate); - const updateAvailable = useSelector((s) => s.app.updateAvailable); - const hasUnresolvedInstallPrompt = useSelector( - (s) => s.app.hasUnresolvedInstallPrompt, - ); - const promptingInstall = useSelector((s) => s.app.promptingInstall); - const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse); - const { hubName, isHubNameValid, setHubName } = useSettingHubName(); - - const dispatch = useDispatch(); - - // istanbul ignore next: babel-loader rewrites this line - const [i18n] = useI18n(); - - // HACK: set additional attributes that are not supported via Drawer props - const handleDrawerOpening = useCallback<(node: HTMLElement) => void>((n) => { - n.setAttribute('role', 'dialog'); - n.setAttribute('aria-modal', 'true'); - n.setAttribute('aria-labelledby', 'settings-drawer-dialog-title'); - }, []); - - return ( - - {i18n.translate(I18nId.Title)} - - } - onOpening={handleDrawerOpening} - onClose={onClose} - // work around https://github.com/palantir/blueprint/issues/5169 - shouldReturnFocusOnClose={false} - > -
-
- {isMacOS() ? 'Cmd' : 'Ctrl'}-+, - out: {isMacOS() ? 'Cmd' : 'Ctrl'}--, - })} - > - - - setIsSettingShowDocsEnabled( - (e.target as HTMLInputElement).checked, - ) - } - /> - - - - setTernaryDarkMode( - (e.target as HTMLInputElement).checked - ? 'dark' - : 'light', - ) - } - /> - - - - - - setIsFlashCurrentProgramEnabled( - (e.target as HTMLInputElement).checked, - ) - } - /> - - - - - - setHubName(e.currentTarget.value)} - onMouseOver={(e) => e.preventDefault()} - className="pb-hub-name-input" - intent={isHubNameValid ? Intent.NONE : Intent.DANGER} - placeholder="Pybricks Hub" - rightElement={ - isHubNameValid ? undefined : ( - - - - ) - } - /> - - - - - - {i18n.translate(I18nId.HelpProjectsLabel)} - - - - {i18n.translate(I18nId.HelpSupportLabel)} - - - - {i18n.translate(I18nId.HelpChatLabel)} - - - - {i18n.translate(I18nId.HelpBugsLabel)} - - - setIsAboutDialogOpen(false)} - /> - - - - - {hasUnresolvedInstallPrompt && ( - - )} - {isServiceWorkerRegistered && !updateAvailable && ( - - )} - {isServiceWorkerRegistered && updateAvailable && ( - - )} - - - - {process.env.NODE_ENV === 'development' && ( - - pseudolocalize(!i18n.pseudolocalize)} - label="Pseudolocalize" - /> - - )} -
-
-
- ); -}; - -export default SettingsDrawer; diff --git a/src/toolbar/Toolbar.test.tsx b/src/toolbar/Toolbar.test.tsx index 43d90571..4b0fc360 100644 --- a/src/toolbar/Toolbar.test.tsx +++ b/src/toolbar/Toolbar.test.tsx @@ -1,33 +1,48 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 The Pybricks Authors -import { getByLabelText, waitFor } from '@testing-library/dom'; import React from 'react'; import { testRender } from '../../test'; import Toolbar from './Toolbar'; -describe('settings button', () => { - it('should open settings drawer', async () => { +describe('toolbar', () => { + it('should have bluetooth button', () => { const [toolbar] = testRender(); - const settingButton = toolbar.getByLabelText('Settings'); + const runButton = toolbar.getByRole('button', { name: 'Bluetooth' }); - expect( - toolbar.queryByRole('dialog', { - name: 'Settings & Help', - }), - ).toBeNull(); + expect(runButton).toBeDefined(); + }); - settingButton.click(); + it('should have flash button', () => { + const [toolbar] = testRender(); - const settingsDrawer = toolbar.getByRole('dialog', { - name: 'Settings & Help', - }); + const runButton = toolbar.getByRole('button', { name: 'Flash' }); - expect(settingsDrawer).toBeVisible(); + expect(runButton).toBeDefined(); + }); - getByLabelText(settingsDrawer, 'Close').click(); + it('should have run button', () => { + const [toolbar] = testRender(); - await waitFor(() => expect(settingsDrawer).not.toBeVisible()); + const runButton = toolbar.getByRole('button', { name: 'Run' }); + + expect(runButton).toBeDefined(); + }); + + it('should have stop button', () => { + const [toolbar] = testRender(); + + const runButton = toolbar.getByRole('button', { name: 'Stop' }); + + expect(runButton).toBeDefined(); + }); + + it('should have repl button', () => { + const [toolbar] = testRender(); + + const runButton = toolbar.getByRole('button', { name: 'REPL' }); + + expect(runButton).toBeDefined(); }); }); diff --git a/src/toolbar/Toolbar.tsx b/src/toolbar/Toolbar.tsx index dc452f92..313453ed 100644 --- a/src/toolbar/Toolbar.tsx +++ b/src/toolbar/Toolbar.tsx @@ -2,21 +2,17 @@ // Copyright (c) 2020-2022 The Pybricks Authors import { ButtonGroup } from '@blueprintjs/core'; -import React, { useState } from 'react'; -import SettingsDrawer from '../settings/SettingsDrawer'; +import React from 'react'; import { preventBrowserNativeContextMenu } from '../utils/react'; import BluetoothButton from './buttons/bluetooth/BluetoothButton'; import FlashButton from './buttons/flash/FlashButton'; import ReplButton from './buttons/repl/ReplButton'; import RunButton from './buttons/run/RunButton'; -import SettingsButton from './buttons/settings/SettingsButton'; import StopButton from './buttons/stop/StopButton'; import './toolbar.scss'; const Toolbar: React.VFC = (_props) => { - const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState(false); - return (
{ - - setIsSettingsDrawerOpen(true)} /> - setIsSettingsDrawerOpen(false)} - /> -
); }; diff --git a/src/toolbar/buttons/settings/SettingsButton.test.tsx b/src/toolbar/buttons/settings/SettingsButton.test.tsx deleted file mode 100644 index f15eebe9..00000000 --- a/src/toolbar/buttons/settings/SettingsButton.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// 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 SettingsButton from './SettingsButton'; - -afterEach(() => { - cleanup(); -}); - -it('should invoke callback when clicked', () => { - const handleAction = jest.fn(); - - const [button] = testRender(); - - button.getByRole('button', { name: 'Settings' }).click(); - - expect(handleAction).toHaveBeenCalled(); -}); diff --git a/src/toolbar/buttons/settings/SettingsButton.tsx b/src/toolbar/buttons/settings/SettingsButton.tsx deleted file mode 100644 index 78ca000d..00000000 --- a/src/toolbar/buttons/settings/SettingsButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2021-2022 The Pybricks Authors - -import { useI18n } from '@shopify/react-i18n'; -import React from 'react'; -import ActionButton, { ActionButtonProps } from '../../ActionButton'; -import { I18nId } from './i18n'; -import icon from './icon.svg'; - -type SettingsButtonProps = Pick; - -const SettingsButton: React.VoidFunctionComponent = ({ - onAction, -}) => { - // istanbul ignore next: babel-loader rewrites this line - const [i18n] = useI18n(); - - return ( - - ); -}; - -export default SettingsButton; diff --git a/src/toolbar/buttons/settings/i18n.ts b/src/toolbar/buttons/settings/i18n.ts deleted file mode 100644 index 8a962ff9..00000000 --- a/src/toolbar/buttons/settings/i18n.ts +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors - -export enum I18nId { - Label = 'label', - Tooltip = 'tooltip', -} diff --git a/src/toolbar/buttons/settings/icon.svg b/src/toolbar/buttons/settings/icon.svg deleted file mode 100644 index 007a45a3..00000000 --- a/src/toolbar/buttons/settings/icon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/toolbar/buttons/settings/translations/en.json b/src/toolbar/buttons/settings/translations/en.json deleted file mode 100644 index 053a328a..00000000 --- a/src/toolbar/buttons/settings/translations/en.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Settings", - "tooltip": "Open settings and help" -} diff --git a/src/toolbar/toolbar.scss b/src/toolbar/toolbar.scss index 45f40a85..357712e3 100644 --- a/src/toolbar/toolbar.scss +++ b/src/toolbar/toolbar.scss @@ -7,11 +7,16 @@ @use '../variables' as pb; .pb-toolbar { - height: pb.$toolbar-height; - padding: 0 bp.$pt-grid-size * 1.5; - position: relative; - width: 100%; + padding: bp.$pt-grid-size bp.$pt-grid-size * 1.5; z-index: bp.$pt-z-index-content; + + // make small buttons more square and slightly smaller (some of this is + // done in react) + @media screen and (max-width: pb.$narrow-screen-limit) { + .#{bp.$ns}-button { + padding: 5px; + } + } } .pb-toolbar * { @@ -20,7 +25,6 @@ .pb-toolbar-group { align-items: center; - height: 100%; &.pb-align-left { float: left; @@ -32,18 +36,3 @@ margin-left: bp.$pt-grid-size * 2; } } - -// make small buttons more square and slightly smaller -@media screen and (max-width: 700px) { - .pb-toolbar .#{bp.$ns}-button { - padding: 5px; - } -} - -// this is the point where the buttons start wrapping to 2 rows so we need to -// adjust the height so that they stay in the toolbar -@media screen and (max-width: 350px) { - .pb-toolbar-group { - height: 50%; - } -} diff --git a/src/variables.scss b/src/variables.scss index 0aa042e6..0365fb3e 100644 --- a/src/variables.scss +++ b/src/variables.scss @@ -1,7 +1,33 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020-2022 The Pybricks Authors -$toolbar-height: 72px; +@use 'sass:color'; +@use '@blueprintjs/core/lib/scss/variables' as bp; + $status-bar-height: 30px; +// point at which we change the UI for narrow screens +$narrow-screen-limit: 700px; + +// Official Pybricks branding color. $pybricks-blue: #0088ce; + +/** + * Adjusts the background contrast by $background-adjust (usually a percent). + * + * The light theme will be made darker by that amount and the dark theme will + * be made lighter. + */ +@mixin background-contrast($background-adjust) { + background-color: color.adjust( + bp.$pt-app-background-color, + $lightness: -$background-adjust + ); + + .#{bp.$ns}-dark & { + background-color: color.adjust( + bp.$pt-dark-app-background-color, + $lightness: $background-adjust + ); + } +}