switch to blueprintjs UI toolkit

This commit is contained in:
David Lechner
2020-06-07 18:59:02 -05:00
committed by David Lechner
parent d0f5a42c85
commit d9211b5b10
14 changed files with 417 additions and 310 deletions
+7 -14
View File
@@ -1,12 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import Button from 'react-bootstrap/Button';
import Image from 'react-bootstrap/Image';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';
import { TooltipId } from './button';
import en from './button.en.json';
@@ -28,16 +25,12 @@ type Props = ActionButtonProps & WithI18nProps;
class ActionButton extends React.Component<Props> {
render(): JSX.Element {
return (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id={`${this.props.id}-tooltip`}>
{this.props.i18n.translate(this.props.tooltip)}.
</Tooltip>
}
<Tooltip
content={this.props.i18n.translate(this.props.tooltip)}
position={Position.BOTTOM}
>
<Button
variant="light"
intent={Intent.PRIMARY}
onClick={(): void => this.props.onAction()}
disabled={this.props.enabled === false}
style={
@@ -46,9 +39,9 @@ class ActionButton extends React.Component<Props> {
: undefined
}
>
<Image src={this.props.icon} alt={this.props.id} />
<img src={this.props.icon} alt={this.props.id} />
</Button>
</OverlayTrigger>
</Tooltip>
);
}
}
+67 -33
View File
@@ -1,50 +1,84 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import React from 'react';
import { Col, Row } from 'react-bootstrap';
import Container from 'react-bootstrap/Container';
import React, { EffectCallback, useEffect, useState } from 'react';
import SplitterLayout from 'react-splitter-layout';
import Editor from './Editor';
import StatusBar from './StatusBar';
import Terminal from './Terminal';
import Toolbar from './Toolbar';
import 'react-splitter-layout/lib/index.css';
function useShowDocs(): boolean {
function getShowDocs(): boolean {
return window.innerWidth >= 1024;
}
const [showDocs, setShowDocs] = useState(getShowDocs);
useEffect((): ReturnType<EffectCallback> => {
function handleResize(): void {
setShowDocs(getShowDocs());
}
window.addEventListener('resize', handleResize);
return (): void => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount and unmount
return showDocs;
}
function App(): JSX.Element {
const showDocs = useShowDocs();
const [dragging, setDragging] = useState(false);
return (
<div>
<Container fluid>
<Row className="vh-100">
<Col md={12} lg={8} className="d-flex flex-column container-col">
<Row>
<Col>
<Toolbar />
</Col>
</Row>
<Row className="row flex-grow-1">
<Col>
<Editor />
</Col>
</Row>
<Row>
<Col className="terminal py-2">
<Terminal />
</Col>
</Row>
<Row className="mt-2">
<Col>
<StatusBar />
</Col>
</Row>
</Col>
<Col lg={4} className="embed-responsive d-lg-block d-md-none">
<div className="app">
<Toolbar />
<SplitterLayout
customClassName="app-main"
onDragStart={(): void => setDragging(true)}
onDragEnd={(): void => setDragging(false)}
percentage={true}
secondaryInitialSize={Number(
localStorage.getItem('app-main-split') || 30,
)}
onSecondaryPaneSizeChange={(value): void =>
localStorage.setItem('app-main-split', String(value))
}
>
<SplitterLayout
vertical={true}
percentage={true}
secondaryInitialSize={Number(
localStorage.getItem('app-docs-split') || 30,
)}
onSecondaryPaneSizeChange={(value): void =>
localStorage.setItem('app-docs-split', String(value))
}
>
<Editor />
<div className="terminal-padding">
<Terminal />
</div>
</SplitterLayout>
{showDocs && (
<div className="docs-iframe">
{dragging && <div className="docs-iframe-overlay" />}
<iframe
src="https://docs.pybricks.com"
allowFullScreen={true}
title="docs"
></iframe>
</Col>
</Row>
</Container>
width="100%"
height="100%"
frameBorder="none"
/>
</div>
)}
</SplitterLayout>
<StatusBar />
</div>
);
}
+31 -10
View File
@@ -1,9 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import React, { ReactElement } from 'react';
import { ResizeSensor } from '@blueprintjs/core';
import { Ace } from 'ace-builds';
import React from 'react';
import AceEditor from 'react-ace';
import { ReactReduxContext } from 'react-redux';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { setEditSession } from '../actions/editor';
import 'ace-builds/src-noconflict/mode-python';
@@ -11,16 +14,30 @@ import 'ace-builds/src-noconflict/theme-xcode';
import 'ace-builds/src-min-noconflict/ext-searchbox';
import 'ace-builds/src-min-noconflict/ext-language_tools';
class Editor extends React.Component {
type DispatchProps = { onSessionChanged: (session?: Ace.EditSession) => void };
type EditorProps = DispatchProps;
class Editor extends React.Component<EditorProps> {
private editorRef: React.RefObject<AceEditor>;
constructor(props: EditorProps) {
super(props);
this.editorRef = React.createRef();
}
render(): JSX.Element {
return (
<ReactReduxContext.Consumer>
{({ store }): ReactElement => (
<ResizeSensor
onResize={(): void => this.editorRef.current?.editor?.resize()}
>
<div className="editor-container">
<AceEditor
ref={this.editorRef}
mode="python"
theme="xcode"
fontSize="16pt"
width="100"
width="100%"
height="100%"
focus={true}
placeholder="Write your program here..."
@@ -31,7 +48,7 @@ class Editor extends React.Component {
enableLiveAutocompletion: true,
}}
onFocus={(_, e): void => {
store.dispatch(setEditSession(e?.session));
this.props.onSessionChanged(e?.session);
}}
onChange={(v): void => localStorage.setItem('program', v)}
commands={[
@@ -47,10 +64,14 @@ class Editor extends React.Component {
},
]}
/>
)}
</ReactReduxContext.Consumer>
</div>
</ResizeSensor>
);
}
}
export default Editor;
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onSessionChanged: (s): Action => dispatch(setEditSession(s)),
});
export default connect(undefined, mapDispatchToProps)(Editor);
+49 -36
View File
@@ -1,12 +1,13 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { IconName, Intent, Toast } from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import Toast from 'react-bootstrap/Toast';
import { connect } from 'react-redux';
import { Dispatch } from '../actions';
import { remove } from '../actions/notification';
import { NotificationLevel, remove } from '../actions/notification';
import { Level } from '../reducers/notification';
import en from './notification.en.json';
interface DispatchProps {
@@ -15,7 +16,7 @@ interface DispatchProps {
interface OwnProps {
id: number;
style: string;
level: NotificationLevel;
message?: string;
messageId?: string;
helpUrl?: string;
@@ -23,51 +24,63 @@ interface OwnProps {
type NotificationProps = DispatchProps & OwnProps & WithI18nProps;
function mapTitle(style: string): string {
switch (style) {
case 'danger':
return 'Error';
case 'warning':
return 'Warning';
function mapIntent(level: NotificationLevel): Intent {
switch (level) {
case Level.Error:
return Intent.DANGER;
case Level.Warning:
return Intent.WARNING;
case Level.Info:
return Intent.PRIMARY;
default:
return 'Info';
return Intent.NONE;
}
}
function mapIcon(level: NotificationLevel): IconName | undefined {
switch (level) {
case Level.Error:
return 'error';
case Level.Warning:
return 'warning-sign';
case Level.Info:
return 'info-sign';
default:
return undefined;
}
}
class Notification extends React.Component<NotificationProps> {
render(): JSX.Element {
const title = mapTitle(this.props.style);
return (
<Toast
onClose={(): void => {
onDismiss={(): void => {
this.props.onClose();
}}
transition={false}
>
<Toast.Header>
<strong className={`mr-auto text-${this.props.style}`}>
{title}
</strong>
</Toast.Header>
<Toast.Body>
<p>
{this.props.messageId
? this.props.i18n.translate(this.props.messageId)
: this.props.message || 'missing message!'}
</p>
<p>
timeout={0}
intent={mapIntent(this.props.level)}
icon={mapIcon(this.props.level)}
message={
<div>
<p>
{this.props.messageId
? this.props.i18n.translate(this.props.messageId)
: this.props.message || 'missing message!'}
</p>
{this.props.helpUrl && (
<a
href={this.props.helpUrl}
target="_blank"
rel="noopener noreferrer"
>
More info
</a>
<p>
<a
href={this.props.helpUrl}
target="_blank"
rel="noopener noreferrer"
>
More info
</a>
</p>
)}
</p>
</Toast.Body>
</Toast>
</div>
}
/>
);
}
}
+14 -39
View File
@@ -1,12 +1,11 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { Toaster } from '@blueprintjs/core';
import React from 'react';
import { Collapse } from 'react-bootstrap';
import { connect } from 'react-redux';
import { TransitionGroup } from 'react-transition-group';
import { RootState } from '../reducers';
import { Level, NotificationList } from '../reducers/notification';
import { NotificationList } from '../reducers/notification';
import Notification from './Notification';
interface StateProps {
@@ -15,45 +14,21 @@ interface StateProps {
type NotificationStackProps = StateProps;
function mapLevelToStyle(level: Level): string {
switch (level) {
case Level.Error:
return 'danger';
case Level.Warning:
return 'warning';
case Level.Info:
return 'info';
}
}
class NotificationStack extends React.Component<NotificationStackProps> {
render(): JSX.Element {
return (
<div aria-live="polite" aria-atomic="true" style={{ position: 'relative' }}>
<div
style={{
position: 'absolute',
top: '10px',
right: '10px',
minWidth: '350px',
zIndex: 999,
}}
>
<TransitionGroup>
{this.props.list.map((n) => (
<Collapse key={n.id} in={true}>
<Notification
id={n.id}
style={mapLevelToStyle(n.level)}
message={n.message}
messageId={n.messageId}
helpUrl={n.helpUrl}
/>
</Collapse>
))}
</TransitionGroup>
</div>
</div>
<Toaster>
{this.props.list.map((n) => (
<Notification
id={n.id}
key={n.id}
level={n.level}
message={n.message}
messageId={n.messageId}
helpUrl={n.helpUrl}
/>
))}
</Toaster>
);
}
}
+7 -14
View File
@@ -1,12 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import Button from 'react-bootstrap/Button';
import Image from 'react-bootstrap/Image';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';
import Dropzone, { FileRejection } from 'react-dropzone';
import { TooltipId } from './button';
import en from './button.en.json';
@@ -81,17 +78,13 @@ class OpenFileButton extends React.Component<Props> {
noKeyboard={this.props.onClick !== undefined}
>
{({ getRootProps, getInputProps }): JSX.Element => (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id={`${this.props.id}-tooltip`}>
{this.props.i18n.translate(this.props.tooltip)}.
</Tooltip>
}
<Tooltip
content={this.props.i18n.translate(this.props.tooltip)}
position={Position.BOTTOM}
>
<Button
{...getRootProps()}
variant="light"
intent={Intent.PRIMARY}
disabled={this.props.enabled === false}
style={
this.props.enabled === false
@@ -106,9 +99,9 @@ class OpenFileButton extends React.Component<Props> {
: {})}
>
<input {...getInputProps()} />
<Image src={this.props.icon} alt={this.props.id} />
<img src={this.props.icon} alt={this.props.id} />
</Button>
</OverlayTrigger>
</Tooltip>
)}
</Dropzone>
);
+7 -3
View File
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { ProgressBar } from '@blueprintjs/core';
import React from 'react';
import ProgressBar from 'react-bootstrap/ProgressBar';
import { connect } from 'react-redux';
import { RootState } from '../reducers';
@@ -13,8 +13,12 @@ type StatusProps = StateProps;
class StatusBar extends React.Component<StatusProps> {
render(): JSX.Element {
return (
<div className="status-bar p-2">
<ProgressBar className="w-25" now={this.props.progress} />
<div className="status-bar">
<ProgressBar
className="status-bar-item"
value={this.props.progress}
animate={false}
/>
</div>
);
}
+4 -8
View File
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { ResizeSensor } from '@blueprintjs/core';
import React from 'react';
import { connect } from 'react-redux';
import ResizeObserver from 'react-resize-observer';
import { Subscription } from 'rxjs';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
@@ -62,13 +62,9 @@ class Terminal extends React.Component<TerminalProps> {
render(): JSX.Element {
return (
<div
id="terminal"
ref={this.terminalRef}
style={{ height: 'inherit', width: 'inherit' }}
>
<ResizeObserver onResize={(): void => this.fitAddon.fit()} />
</div>
<ResizeSensor onResize={(): void => this.fitAddon.fit()}>
<div className="terminal-container" ref={this.terminalRef} />
</ResizeSensor>
);
}
}
+20 -17
View File
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { ButtonGroup, Navbar } from '@blueprintjs/core';
import React from 'react';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import ButtonToolbar from 'react-bootstrap/ButtonToolbar';
import BluetoothButton from './BluetoothButton';
import FlashButton from './FlashButton';
import OpenButton from './OpenButton';
@@ -15,21 +14,25 @@ import StopButton from './StopButton';
class Toolbar extends React.Component {
render(): JSX.Element {
return (
<ButtonToolbar className="m-2">
<ButtonGroup className="mr-2" size="lg">
<OpenButton id="open" />
<SaveAsButton id="saveAs" />
</ButtonGroup>
<ButtonGroup className="mr-2" size="lg">
<BluetoothButton id="bluetooth" />
<RunButton id="run" />
<StopButton id="stop" />
</ButtonGroup>
<ButtonGroup className="mr-2" size="lg">
<ReplButton id="repl" />
<FlashButton id="flash" />
</ButtonGroup>
</ButtonToolbar>
<Navbar fixedToTop={true}>
<Navbar.Group>
<ButtonGroup>
<OpenButton id="open" />
<SaveAsButton id="saveAs" />
</ButtonGroup>
<Navbar.Divider />
<ButtonGroup>
<BluetoothButton id="bluetooth" />
<RunButton id="run" />
<StopButton id="stop" />
</ButtonGroup>
<Navbar.Divider />
<ButtonGroup>
<ReplButton id="repl" />
<FlashButton id="flash" />
</ButtonGroup>
</Navbar.Group>
</Navbar>
);
}
}