diff --git a/src/editor/EditorContainer.tsx b/src/editor/EditorContainer.tsx index 6b7569423..f0ad644e3 100644 --- a/src/editor/EditorContainer.tsx +++ b/src/editor/EditorContainer.tsx @@ -5,6 +5,7 @@ import { useSettings } from "../settings"; import ZoomControls from "../workbench/ZoomControls"; import Editor from "./Editor"; import NonMainFileNotice from "./NonMainFileNotice"; +import EditorToolbar from "./EditorToolbar"; interface EditorContainerProps { filename: string; diff --git a/src/editor/EditorToolbar.tsx b/src/editor/EditorToolbar.tsx new file mode 100644 index 000000000..6fd5fd222 --- /dev/null +++ b/src/editor/EditorToolbar.tsx @@ -0,0 +1,31 @@ +import { HStack } from "@chakra-ui/react"; +import React from "react"; +import DownloadButton from "../workbench/DownloadButton"; +import ProjectNameEditable from "../workbench/ProjectNameEditable"; +import ZoomControls from "../workbench/ZoomControls"; + +interface EditorToolbarProps {} + +const EditorToolbar = ({}: EditorToolbarProps) => { + return ( + + + + + + + + + + ); +}; + +export default EditorToolbar; diff --git a/src/workbench/CollapsableButton.tsx b/src/workbench/CollapsableButton.tsx new file mode 100644 index 000000000..529be61ac --- /dev/null +++ b/src/workbench/CollapsableButton.tsx @@ -0,0 +1,42 @@ +import { + Button, + HTMLChakraProps, + IconButton, + ThemingProps, +} from "@chakra-ui/react"; +import React from "react"; + +export interface CollapsableButtonProps + extends HTMLChakraProps<"button">, + ThemingProps<"Button"> { + mode: "icon" | "button"; + text: string; + icon: React.ReactElement; + /** + * Width used only in button mode. + */ + buttonWidth?: number | string; +} + +/** + * Button that can be a regular or icon button. + * + * We'd like to do this at a lower-level so we can animate a transition. + */ +const CollapsableButton = ({ + mode, + text, + icon, + buttonWidth, + ...props +}: CollapsableButtonProps) => { + return mode === "icon" ? ( + + ) : ( + + ); +}; + +export default CollapsableButton; diff --git a/src/workbench/DeviceConnection.tsx b/src/workbench/DeviceConnection.tsx index 6ad391a9c..d8b0542e0 100644 --- a/src/workbench/DeviceConnection.tsx +++ b/src/workbench/DeviceConnection.tsx @@ -1,4 +1,4 @@ -import { Button, HStack, Switch, Text, VStack } from "@chakra-ui/react"; +import { Button, Flex, HStack, Switch, Text, VStack } from "@chakra-ui/react"; import React, { useCallback, useState } from "react"; import { RiFlashlightFill } from "react-icons/ri"; import { useConnectionStatus, useDevice } from "../device/device-hooks"; @@ -10,6 +10,7 @@ import useActionFeedback, { } from "../common/use-action-feedback"; import { BoardId } from "../device/board-id"; import Separate from "../common/Separate"; +import FlashButton from "./FlashButton"; class HexGenerationError extends Error {} @@ -23,7 +24,6 @@ const DeviceConnection = () => { const connectionStatus = useConnectionStatus(); const connected = connectionStatus === ConnectionStatus.CONNECTED; const supported = connectionStatus !== ConnectionStatus.NOT_SUPPORTED; - const [progress, setProgress] = useState(undefined); const actionFeedback = useActionFeedback(); const device = useDevice(); const fs = useFileSystem(); @@ -38,67 +38,30 @@ const DeviceConnection = () => { } } }, [device, connected]); - - const handleFlash = useCallback(async () => { - const dataSource = async (boardId: BoardId) => { - try { - return await fs.toHexForFlash(boardId); - } catch (e) { - throw new HexGenerationError(e.message); - } - }; - - try { - await device.flash(dataSource, { partial: true, progress: setProgress }); - } catch (e) { - if (e instanceof HexGenerationError) { - actionFeedback.expectedError({ - title: "Failed to build the hex file", - description: e.message, - }); - } else { - handleWebUSBError(actionFeedback, e); - } - } - }, [fs, device, actionFeedback]); - + const buttonWidth = "10rem"; return ( - + {supported ? ( - - + + {connected ? "micro:bit connected" : "micro:bit disconnected"} ) : null} - - - {connected && ( - + + {supported && ( + )} - + - + ); }; diff --git a/src/workbench/DownloadButton.tsx b/src/workbench/DownloadButton.tsx index 5db12ac09..692b1529d 100644 --- a/src/workbench/DownloadButton.tsx +++ b/src/workbench/DownloadButton.tsx @@ -1,8 +1,18 @@ -import { Button, ButtonProps } from "@chakra-ui/react"; +import { + BoxProps, + Button, + ButtonProps, + Icon, + IconButton, +} from "@chakra-ui/react"; import React, { useCallback } from "react"; import { RiDownload2Line } from "react-icons/ri"; import useActionFeedback from "../common/use-action-feedback"; import { useFileSystem } from "../fs/fs-hooks"; +import CollapsableButton, { CollapsableButtonProps } from "./CollapsableButton"; + +interface DownloadButtonProps + extends Omit {} /** * Download HEX button. @@ -12,7 +22,7 @@ import { useFileSystem } from "../fs/fs-hooks"; * * Otherwise it's a more minor action. */ -const DownloadButton = (props: ButtonProps) => { +const DownloadButton = (props: DownloadButtonProps) => { const fs = useFileSystem(); const actionFeedback = useActionFeedback(); const handleDownload = useCallback(async () => { @@ -33,9 +43,12 @@ const DownloadButton = (props: ButtonProps) => { }, []); return ( - + } + onClick={handleDownload} + text="Download" + /> ); }; diff --git a/src/workbench/Files.tsx b/src/workbench/Files.tsx index 439722cc1..5e99e1190 100644 --- a/src/workbench/Files.tsx +++ b/src/workbench/Files.tsx @@ -1,9 +1,11 @@ import { Button, + Center, HStack, IconButton, List, ListItem, + Text, VStack, } from "@chakra-ui/react"; import React, { useCallback } from "react"; @@ -13,6 +15,7 @@ import { useFileSystem, useFileSystemState } from "../fs/fs-hooks"; import { saveAs } from "file-saver"; import useActionFeedback from "../common/use-action-feedback"; import ProjectNameEditable from "./ProjectNameEditable"; +import OpenButton from "./OpenButton"; interface FilesProps { onSelectedFileChanged: (name: string) => void; @@ -27,15 +30,20 @@ const Files = ({ onSelectedFileChanged }: FilesProps) => { return null; } return ( - - - - {fs.files.map((f) => ( - - onSelectedFileChanged(f.name)} /> - - ))} - + + + + {fs.files.map((f) => ( + + onSelectedFileChanged(f.name)} + /> + + ))} + + + ); }; diff --git a/src/workbench/FlashButton.tsx b/src/workbench/FlashButton.tsx new file mode 100644 index 000000000..5432547d6 --- /dev/null +++ b/src/workbench/FlashButton.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useState } from "react"; +import { RiFlashlightFill } from "react-icons/ri"; +import Separate from "../common/Separate"; +import useActionFeedback, { + ActionFeedback, +} from "../common/use-action-feedback"; +import { ConnectionStatus, WebUSBError } from "../device"; +import { BoardId } from "../device/board-id"; +import { useConnectionStatus, useDevice } from "../device/device-hooks"; +import { useFileSystem } from "../fs/fs-hooks"; +import CollapsableButton, { CollapsableButtonProps } from "./CollapsableButton"; + +class HexGenerationError extends Error {} + +/** + * Flash button. + */ +const FlashButton = ( + props: Omit +) => { + const fs = useFileSystem(); + const actionFeedback = useActionFeedback(); + const device = useDevice(); + const status = useConnectionStatus(); + const connected = status === ConnectionStatus.CONNECTED; + const [progress, setProgress] = useState(); + + const handleFlash = useCallback(async () => { + const dataSource = async (boardId: BoardId) => { + try { + return await fs.toHexForFlash(boardId); + } catch (e) { + throw new HexGenerationError(e.message); + } + }; + + try { + await device.flash(dataSource, { partial: true, progress: setProgress }); + } catch (e) { + if (e instanceof HexGenerationError) { + actionFeedback.expectedError({ + title: "Failed to build the hex file", + description: e.message, + }); + } else { + handleWebUSBError(actionFeedback, e); + } + } + }, [fs, device, actionFeedback]); + const text = + typeof progress === "undefined" + ? "Flash" + : `Flashing… (${(progress * 100).toFixed(0)}%)`; + return ( + } + onClick={handleFlash} + text={text} + /> + ); +}; + +const handleWebUSBError = (actionFeedback: ActionFeedback, e: any) => { + if (e instanceof WebUSBError) { + actionFeedback.expectedError({ + title: e.title, + description: ( +
}> + {[e.message, e.description].filter(Boolean)} +
+ ), + }); + } else { + actionFeedback.unexpectedError(e); + } +}; + +export default FlashButton; diff --git a/src/workbench/Help.tsx b/src/workbench/Help.tsx new file mode 100644 index 000000000..2712903c0 --- /dev/null +++ b/src/workbench/Help.tsx @@ -0,0 +1,50 @@ +import { + Button, + HStack, + IconButton, + Link, + Text, + VStack, +} from "@chakra-ui/react"; +import React, { useCallback } from "react"; +import { RiExternalLinkLine, RiFileCopy2Line } from "react-icons/ri"; +import Separate from "../common/Separate"; +import useActionFeedback from "../common/use-action-feedback"; +import config from "../config"; +import { copyVersion, versionInfo } from "./HelpMenu"; + +/** + * Help as a tab. + */ +const Help = () => { + const externalLinkIcon = ; + const actionFeedback = useActionFeedback(); + const handleCopyVersion = useCallback(() => { + copyVersion(actionFeedback); + }, [actionFeedback]); + return ( + + + + {externalLinkIcon} Documentation + + {externalLinkIcon} Support + + + +
}> + {versionInfo} +
+
+ } + variant="outline" + aria-label="Copy version information to the clipboard" + /> +
+
+ ); +}; + +export default Help; diff --git a/src/workbench/HelpMenu.tsx b/src/workbench/HelpMenu.tsx index f5362625b..488e4f29d 100644 --- a/src/workbench/HelpMenu.tsx +++ b/src/workbench/HelpMenu.tsx @@ -1,6 +1,4 @@ -import React, { useCallback } from "react"; import { - Button, IconButton, Menu, MenuButton, @@ -12,23 +10,24 @@ import { ThemeTypings, ThemingProps, } from "@chakra-ui/react"; +import React, { useCallback } from "react"; import { - RiArrowDropDownLine, RiExternalLinkLine, RiFileCopy2Line, - RiInformationLine, RiQuestionLine, } from "react-icons/ri"; -import { microPythonVersions } from "../fs/fs"; import Separate from "../common/Separate"; +import useActionFeedback, { + ActionFeedback, +} from "../common/use-action-feedback"; import config from "../config"; -import useActionFeedback from "../common/use-action-feedback"; +import { microPythonVersions } from "../fs/fs"; interface HelpMenuProps extends ThemingProps<"Menu"> { size?: ThemeTypings["components"]["Button"]["sizes"]; } -const versionInfo = [ +export const versionInfo = [ `Editor ${process.env.REACT_APP_VERSION}`, `MicroPython ${microPythonVersions.map((mpy) => mpy.version).join("/")}`, ]; @@ -36,18 +35,22 @@ const versionInfo = [ const openInNewTab = (href: string) => () => window.open(href, "_blank", "noopener"); -const handleDocumentation = openInNewTab(config.documentationLink); -const handleSupport = openInNewTab(config.supportLink); +// Exported for now so we can share them in alt-layouts. +export const handleDocumentation = openInNewTab(config.documentationLink); +export const handleSupport = openInNewTab(config.supportLink); +export const copyVersion = async (actionFeedback: ActionFeedback) => { + try { + await navigator.clipboard.writeText(versionInfo.join("\n")); + } catch (e) { + actionFeedback.unexpectedError(e); + } +}; const HelpMenu = ({ size, ...props }: HelpMenuProps) => { const actionFeedback = useActionFeedback(); const handleCopyVersion = useCallback(async () => { - try { - await navigator.clipboard.writeText(versionInfo.join("\n")); - } catch (e) { - actionFeedback.unexpectedError(e); - } - }, [actionFeedback, versionInfo]); + copyVersion(actionFeedback); + }, [actionFeedback]); // TODO: Can we make these actual links and still use the menu components? return ( diff --git a/src/workbench/LeftPanel.tsx b/src/workbench/LeftPanel.tsx index d4f25e0a6..c410efabb 100644 --- a/src/workbench/LeftPanel.tsx +++ b/src/workbench/LeftPanel.tsx @@ -1,29 +1,30 @@ import { - Box, + Center, Flex, - HStack, Icon, + Image, Tab, TabList, TabPanel, TabPanels, Tabs, - VStack, } from "@chakra-ui/react"; import React, { ReactNode, useMemo } from "react"; import { IconType } from "react-icons"; import { - RiFile3Line, RiLayoutMasonryFill, + RiQuestionLine, + RiSave2Line, RiSettings2Line, } from "react-icons/ri"; -import GradientLine from "../common/GradientLine"; import DeviceConnection from "./DeviceConnection"; import Files from "./Files"; +import Help from "./Help"; import LeftPanelTabContent from "./LeftPanelTabContent"; import Logo from "./Logo"; import Packages from "./Packages"; import Settings from "./Settings"; +import pythonLogo from "./python-icon.png"; interface LeftPanelProps { onSelectedFileChanged: (filename: string) => void; @@ -44,8 +45,8 @@ const LeftPanel = ({ onSelectedFileChanged }: LeftPanelProps) => { }, { id: "files", - title: "Files", - icon: RiFile3Line, + title: "Load and save", + icon: RiSave2Line, contents: , }, { @@ -54,6 +55,12 @@ const LeftPanel = ({ onSelectedFileChanged }: LeftPanelProps) => { icon: RiSettings2Line, contents: , }, + { + id: "help", + title: "Help", + icon: RiQuestionLine, + contents: , + }, ], [onSelectedFileChanged] ); @@ -74,6 +81,16 @@ interface LeftPanelConentsProps { const LeftPanelContents = ({ panes }: LeftPanelConentsProps) => { return ( + + + Python logo + {panes.map((p) => ( @@ -92,7 +109,6 @@ const LeftPanelContents = ({ panes }: LeftPanelConentsProps) => { ))} - ); }; diff --git a/src/workbench/LeftPanelTabContent.tsx b/src/workbench/LeftPanelTabContent.tsx index 9151deafa..53af91aff 100644 --- a/src/workbench/LeftPanelTabContent.tsx +++ b/src/workbench/LeftPanelTabContent.tsx @@ -8,7 +8,7 @@ interface LeftPanelTabContentProps { const LeftPanelTabContent = ({ title, children }: LeftPanelTabContentProps) => { return ( - + {title} diff --git a/src/workbench/OpenButton.tsx b/src/workbench/OpenButton.tsx index fd0be6f57..bc3e5ebcc 100644 --- a/src/workbench/OpenButton.tsx +++ b/src/workbench/OpenButton.tsx @@ -4,10 +4,14 @@ import { RiFolderOpenLine } from "react-icons/ri"; import useActionFeedback from "../common/use-action-feedback"; import { useFileSystem } from "../fs/fs-hooks"; +interface OpenButtonProps extends ButtonProps { + text?: string; +} + /** * Open HEX button, with an associated input field. */ -const OpenButton = (props: ButtonProps) => { +const OpenButton = ({ text = "Open", ...props }: OpenButtonProps) => { const fs = useFileSystem(); const actionFeedback = useActionFeedback(); const ref = useRef(null); @@ -51,7 +55,7 @@ const OpenButton = (props: ButtonProps) => { onClick={handleChooseFile} {...props} > - Open + {text} ); diff --git a/src/workbench/ProjectNameEditable.tsx b/src/workbench/ProjectNameEditable.tsx index ad7b7d6f6..0f247b134 100644 --- a/src/workbench/ProjectNameEditable.tsx +++ b/src/workbench/ProjectNameEditable.tsx @@ -44,7 +44,6 @@ const ProjectNameEditable = () => { defaultValue={"Name your project"} whiteSpace="nowrap" onChange={handleChange} - justifyContent="space-between" > {(props) => ( <> @@ -53,7 +52,11 @@ const ProjectNameEditable = () => { display="flex" alignItems="center" /> - + )} diff --git a/src/workbench/Serial.tsx b/src/workbench/Serial.tsx new file mode 100644 index 000000000..11532feb9 --- /dev/null +++ b/src/workbench/Serial.tsx @@ -0,0 +1,24 @@ +import { Flex, HStack, VStack } from "@chakra-ui/react"; +import React from "react"; +import Placeholder from "../common/Placeholder"; +import DeviceConnection from "./DeviceConnection"; +import ProjectNameEditable from "./ProjectNameEditable"; + +const Serial = () => { + return ( + + + + + + + + ); +}; + +export default Serial; diff --git a/src/workbench/Workbench.tsx b/src/workbench/Workbench.tsx index e8a0e129a..9dbd7422f 100644 --- a/src/workbench/Workbench.tsx +++ b/src/workbench/Workbench.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { + Bottom, BottomResizable, Fill, LeftResizable, @@ -13,6 +14,7 @@ import EditorContainer from "../editor/EditorContainer"; import { MAIN_FILE } from "../fs/fs"; import Header from "./Header"; import LeftPanel from "./LeftPanel"; +import Serial from "./Serial"; import Simulator from "./Simulator"; const Workbench = () => { @@ -20,10 +22,6 @@ const Workbench = () => { return ( // https://github.com/aeagle/react-spaces - -
- - { - + + + + + + - - + + - - - ); diff --git a/src/workbench/python-icon.png b/src/workbench/python-icon.png new file mode 100644 index 000000000..3c69cc497 Binary files /dev/null and b/src/workbench/python-icon.png differ