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 && (
- }
- size="lg"
- width="100%"
- disabled={!fs || !connected || typeof progress !== "undefined"}
- onClick={handleFlash}
- >
- {typeof progress === "undefined"
- ? "Flash"
- : `Flashing… (${(progress * 100).toFixed(0)}%)`}
-
+
+ {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} {...props}>
- Download
-
+ }
+ 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 (
+
+
+
+
{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