diff --git a/package.json b/package.json index a3ff0b80b12f0ff763551c8bd08a572f0b2f286a..136a6bc41fdb41da7f6b59c69479d9cabc2031fc 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", "html-webpack-plugin": "^3.2.0", "immutable": "3.8.2", + "intl": "1.2.5", "jest": "^21.0.0", "keymirror": "0.1.1", "lodash.bindall": "4.4.0", @@ -95,13 +96,13 @@ "redux-throttle": "0.1.1", "rimraf": "^2.6.1", "scratch-audio": "0.1.0-prerelease.20180625202813", - "scratch-blocks": "0.1.0-prerelease.1531144787", + "scratch-blocks": "0.1.0-prerelease.1531409796", "scratch-l10n": "3.0.20180703181510", - "scratch-paint": "0.2.0-prerelease.20180709132225", + "scratch-paint": "0.2.0-prerelease.20180712144339", "scratch-render": "0.1.0-prerelease.20180618173030", "scratch-storage": "0.5.1", "scratch-svg-renderer": "0.2.0-prerelease.20180618172917", - "scratch-vm": "0.1.0-prerelease.1530902855", + "scratch-vm": "0.1.0-prerelease.1531340421", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.21.0", diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css index 6771a6851d50d5aefd856d94700811d6be15bf3d..8f39946c128d445c5dbd98acbade5ac10553f1c2 100644 --- a/src/components/action-menu/action-menu.css +++ b/src/components/action-menu/action-menu.css @@ -28,7 +28,7 @@ button::-moz-focus-inner { } .button:hover { - background: $pen-primary; + background: $extensions-primary; } .button:active { @@ -126,7 +126,7 @@ button::-moz-focus-inner { is not very easy to style. */ .tooltip { - background-color: $pen-primary !important; + background-color: $extensions-primary !important; opacity: 1 !important; border: 1px solid hsla(0, 0%, 0%, .1) !important; box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; @@ -134,7 +134,7 @@ button::-moz-focus-inner { } .tooltip:after { - background-color: $pen-primary; + background-color: $extensions-primary; } .coming-soon-tooltip { diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css index 5f738b196ff18175e11a547f48e6fec3adece552..e02486ce0fe376d091090b4355d2a0febc3c8331 100644 --- a/src/components/blocks/blocks.css +++ b/src/components/blocks/blocks.css @@ -40,6 +40,14 @@ box-sizing: content-box; } +.blocks :global(.blocklyBlockDragSurface) { + /* + Fix an issue where the drag surface was preventing hover events for sharing blocks. + This does not prevent user interaction on the blocks themselves. + */ + pointer-events: none; +} + /* Shrink category font to fit "My Blocks" for now. Probably will need different solutions for language support later, so diff --git a/src/components/camera-modal/camera-modal.css b/src/components/camera-modal/camera-modal.css index 5f631e41bf5ad9b22675065153f683534c896a7b..4ea962fbafd68bc7ecdf3736d96930070a46f0de 100644 --- a/src/components/camera-modal/camera-modal.css +++ b/src/components/camera-modal/camera-modal.css @@ -93,7 +93,7 @@ $main-button-size: 2.75rem; } .main-button:hover { - background: $pen-primary; + background: $extensions-primary; box-shadow: 0 0 0 6px $motion-transparent; } diff --git a/src/components/cards/card.css b/src/components/cards/card.css index 99ab8a66c01f10aa99f68fdbd1153bf1e2a72538..9e14a1fb2a6b1152e0b099479febe77507c68ea9 100644 --- a/src/components/cards/card.css +++ b/src/components/cards/card.css @@ -14,21 +14,21 @@ top: 5%; background: $ui-white; border: 1px solid $ui-tertiary; - width: 10px; + width: .75rem; z-index: 10; opacity: 0.9; overflow: hidden; } .left-card { - left: -10px; + left: -.75rem; border-right: 0; border-top-left-radius: 0.75rem; border-bottom-left-radius: 0.75rem; } .right-card { - right: -10px; + right: -.75rem; border-left: 0; border-top-right-radius: 0.75rem; border-bottom-right-radius: 0.75rem; @@ -39,9 +39,9 @@ position: absolute; top: 0; left: 0; - height: 1.8rem; + height: 2.5rem; width: 100%; - background: $motion-primary; + background: $extensions-primary; } .left-button, .right-button { @@ -51,14 +51,24 @@ z-index: 20; user-select: none; cursor: pointer; - background: $motion-primary; - box-shadow: 0 0 0 4px $motion-transparent; - height: 40px; - width: 40px; + background: $extensions-primary; + box-shadow: 0 0 0 4px $extensions-transparent; + height: 44px; + width: 44px; border-radius: 100%; display: flex; justify-content: center; align-items: center; + transition: all 0.25s ease; +} + +.left-button:hover, .right-button:hover { + box-shadow: 0 0 0 6px $extensions-transparent; + transform: scale(1.125); +} + +.left-button img, .right-button img{ + width: 1.75rem; } .left-button { @@ -88,10 +98,10 @@ flex-direction: row; justify-content: space-between; align-items: center; - background: $motion-primary; - border-bottom: 1px solid $motion-tertiary; - padding: 0.5rem; + background: $extensions-primary; + border-bottom: 1px solid $extensions-tertiary; font-size: 0.625rem; + font-weight: bold; } .remove-button, .all-button { @@ -101,6 +111,11 @@ flex-direction: row; justify-content: center; align-items: center; + padding: 0.75rem; +} + +.remove-button:hover, .all-button:hover { + background-color: $ui-black-transparent; } .step-title { @@ -161,14 +176,14 @@ color: $motion-primary; font-weight: bold; font-size: 0.85rem; - margin: 14px 0px; + margin: .625rem 0px; text-align: center; font-weight: bold; text-align: center; } .help-icon, .close-icon { - height: 0.75rem; + height: 1rem; } .help-icon { @@ -198,9 +213,9 @@ display: flex; align-items: center; color: $ui-white; - font-size: 12px; + font-size: .75rem; font-weight: bold; - line-height: 15px; + line-height: 1rem; text-align: center; } @@ -228,10 +243,10 @@ height: 0.5rem; margin: 0 0.25rem; border-radius: 100%; - background: transparent; - border: 2px solid $ui-white-transparent; + background: $ui-white-transparent; } .active-step-pip { - background: white; + background: $ui-white; + box-shadow: 0px 0px 0px 2px $ui-black-transparent; } diff --git a/src/components/cards/cards.jsx b/src/components/cards/cards.jsx index ab01f8ecdf1f54a193abb692a38c0f00061e80b3..dc081ae0b64548c429a9c3f4da0e6ae4c03e1500 100644 --- a/src/components/cards/cards.jsx +++ b/src/components/cards/cards.jsx @@ -8,7 +8,7 @@ import styles from './card.css'; import nextIcon from './icon--next.svg'; import prevIcon from './icon--prev.svg'; -import helpIcon from './icon--help.svg'; +import helpIcon from '../../lib/assets/icon--tutorials.svg'; import closeIcon from '../close-button/icon--close.svg'; const CardHeader = ({onCloseCards, onShowAll, totalSteps, step}) => ( @@ -22,9 +22,9 @@ const CardHeader = ({onCloseCards, onShowAll, totalSteps, step}) => ( src={helpIcon} /> <FormattedMessage - defaultMessage="All How-Tos" - description="Title for button to return to how-to library" - id="gui.cards.all-how-tos" + defaultMessage="Tutorials" + description="Title for button to return to tutorials library" + id="gui.cards.all-tutorials" /> </div> {totalSteps > 1 ? ( diff --git a/src/components/cards/icon--help.svg b/src/components/cards/icon--help.svg deleted file mode 100644 index 436d052874bd47c11a55909be61d0fb83777c000..0000000000000000000000000000000000000000 Binary files a/src/components/cards/icon--help.svg and /dev/null differ diff --git a/src/components/connection-modal/connected-step.jsx b/src/components/connection-modal/connected-step.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c02f644bc4e55d807124bd8cc41004a98cdca24b --- /dev/null +++ b/src/components/connection-modal/connected-step.jsx @@ -0,0 +1,71 @@ +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import Box from '../box/box.jsx'; +import Dots from './dots.jsx'; +import bluetoothIcon from './icons/bluetooth-white.svg'; +import styles from './connection-modal.css'; +import classNames from 'classnames'; + +const ConnectedStep = props => ( + <Box className={styles.body}> + <Box className={styles.activityArea}> + <Box className={styles.centeredRow}> + <div className={styles.deviceActivity}> + <img + className={styles.deviceActivityIcon} + src={props.deviceImage} + /> + <img + className={styles.bluetoothConnectedIcon} + src={bluetoothIcon} + /> + </div> + </Box> + </Box> + <Box className={styles.bottomArea}> + <Box className={styles.instructions}> + <FormattedMessage + defaultMessage="Connected" + description="Message indicating that a device was connected" + id="gui.connection.connected" + /> + </Box> + <Dots + success + total={3} + /> + <div className={styles.cornerButtons}> + <button + className={classNames(styles.redButton, styles.connectionButton)} + onClick={props.onDisconnect} + > + <FormattedMessage + defaultMessage="Disconnect" + description="Button to disconnect the device" + id="gui.connection.disconnect" + /> + </button> + <button + className={styles.connectionButton} + onClick={props.onCancel} + > + <FormattedMessage + defaultMessage="Go to Editor" + description="Button to return to the editor" + id="gui.connection.go-to-editor" + /> + </button> + </div> + </Box> + </Box> +); + +ConnectedStep.propTypes = { + deviceImage: PropTypes.string.isRequired, + onCancel: PropTypes.func, + onDisconnect: PropTypes.func +}; + +export default ConnectedStep; diff --git a/src/components/connection-modal/connecting-step.jsx b/src/components/connection-modal/connecting-step.jsx new file mode 100644 index 0000000000000000000000000000000000000000..063e67a826c79f4c9f55548e321d499d8c86d6a3 --- /dev/null +++ b/src/components/connection-modal/connecting-step.jsx @@ -0,0 +1,71 @@ +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import Box from '../box/box.jsx'; +import Dots from './dots.jsx'; + +import bluetoothIcon from './icons/bluetooth-white.svg'; +import closeIcon from '../close-button/icon--close.svg'; + +import styles from './connection-modal.css'; + +const ConnectingStep = props => ( + <Box className={styles.body}> + <Box className={styles.activityArea}> + <Box className={styles.centeredRow}> + <div className={styles.deviceActivity}> + <img + className={styles.deviceActivityIcon} + src={props.deviceImage} + /> + <img + className={styles.bluetoothConnectingIcon} + src={bluetoothIcon} + /> + </div> + </Box> + </Box> + <Box className={styles.bottomArea}> + <Box className={styles.instructions}> + <FormattedMessage + defaultMessage="Connecting" + description="" + id="gui.connection.connecting" + /> + </Box> + <Dots + counter={1} + total={3} + /> + <div className={styles.segmentedButton}> + <button + disabled + className={styles.connectionButton} + > + <FormattedMessage + defaultMessage="Connecting..." + description="Label indicating that connection is in progress" + id="gui.connection.connecting-cancelbutton" + /> + </button> + <button + className={styles.connectionButton} + onClick={props.onDisconnect} + > + <img + className={styles.abortConnectingIcon} + src={closeIcon} + /> + </button> + </div> + </Box> + </Box> +); + +ConnectingStep.propTypes = { + deviceImage: PropTypes.string.isRequired, + onDisconnect: PropTypes.func +}; + +export default ConnectingStep; diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css new file mode 100644 index 0000000000000000000000000000000000000000..ee0a0aae1e6ee354f7c3a2decd302f65b000644d --- /dev/null +++ b/src/components/connection-modal/connection-modal.css @@ -0,0 +1,308 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.modal-content { + width: 480px; +} + +.header { + background-color: $pen-primary; +} + +.body { + background: $ui-white; +} + +.label { + font-weight: 500; + margin: 0 0 0.75rem; +} + +.centered-row { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.device-tile-pane { + overflow-y: auto; + width: 100%; + height: 100%; + padding: 0.5rem; +} + +.device-tile { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + background-color: $ui-white; + border-radius: 0.25rem; + padding: 10px; + width: 100%; + height: 55px; + margin-bottom: 0.5rem; +} + +.device-tile-name { + display: flex; + align-items: center; +} + +.device-tile-image { + margin-right: 0.5rem; +} + +.device-tile-name-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; +} + +.device-tile-name-label { + font-weight: bold; + font-size: 0.625rem; +} + +.device-tile-name-text { + font-size: 0.875rem; +} + +.device-tile button { + padding: 0.6rem 0.75rem; + border: none; + border-radius: 0.25rem; + font-weight: 600; + font-size: 0.85rem; + background: $motion-primary; + border: $motion-primary; + color: white; + cursor: pointer; +} + +.signal-strength-meter { + display: flex; + justify-content: space-between; + align-items: flex-end; + width: 22px; + height: 16px; + margin-right: 1rem; +} + +.signal-bar { + width: 4px; + border-radius: 4px; + background-color: #DBDBDB; +} + +.signal-bar:nth-of-type(1) { height: 25%; } +.signal-bar:nth-of-type(2) { height: 50%; } +.signal-bar:nth-of-type(3) { height: 75%; } +.signal-bar:nth-of-type(4) { height: 100%; } + +.green-bar { + background-color: $pen-primary; +} + +.radar { + width: 40px; + height: 40px; + margin-right: 0.5rem; + animation: spin 4s linear infinite; +} + +@keyframes spin { + 100% { + transform: rotate(360deg); + } +} + + +.device-activity { + position: relative; +} + +.device-activity-icon { + /* width: 80px; + height: 80px; */ +} + +.bluetooth-connecting-icon { + position: absolute; + top: -5px; + right: -15px; + padding: 5px 5px; + background-color: $motion-primary; + border-radius: 100%; + box-shadow: 0px 0px 0px 4px $motion-transparent; + /* animation: pulse-blue-ring 1s infinite ease-in-out alternate; */ + animation: wiggle 0.5s infinite ease-in-out alternate; + +} + +@keyframes pulse-blue-ring { + 100% { + box-shadow: 0px 0px 0px 8px $motion-light-transparent; + } +} + + +.bluetooth-connected-icon { + position: absolute; + top: -5px; + right: -15px; + padding: 5px 5px; + background-color: $pen-primary; + border-radius: 100%; + box-shadow: 0px 0px 0px 4px $pen-transparent; +} + + + +@keyframes wiggle { + 0% {transform: rotate(3deg) scale(1.05);} + 25% {transform: rotate(-3deg) scale(1.05);} + 50% {transform: rotate(5deg) scale(1.05);} + 75% {transform: rotate(-2deg) scale(1.05);} + 100% {transform: rotate(0deg) scale(1.05);} +} + +.device-tile-widgets { + display: flex; + align-items: center; +} + +.activityArea { + height: 165px; + background-color: $motion-light-transparent; + display: flex; + justify-content: center; + align-items: center; +} + +.button-row { + font-weight: bolder; + text-align: center; + display: flex; +} + +.abort-connecting-icon { + width: 10px; + transform: rotate(45deg); +} + +.connection-button { + padding: 0.6rem 0.75rem; + border-radius: 0.5rem; + background: $motion-primary; + color: white; + font-weight: 600; + font-size: 0.85rem; + margin: 0.25rem; + border: none; + cursor: pointer; + display: flex; + align-items: center; +} + +.connection-button:disabled { + background: $motion-transparent; +} + +.segmented-button { + display: flex; +} + +.segmented-button .connection-button:first-of-type { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + margin-right: 0; +} + +.segmented-button .connection-button:last-of-type { + margin-left: 1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.button-icon-right { + margin-left: 0.5rem; +} + +.button-icon-left { + margin-right: 0.5rem; +} + +.red-button { + background: $red-primary; +} + +.corner-buttons { + display: flex; + justify-content: space-between; + width: 100%; + padding: 0 1rem; +} + +.bottom-area { + background-color: $ui-white; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + padding-bottom: 12px; +} + +.instructions { + text-align: center; + padding: 1rem; +} + +.dots-row { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding-bottom: 1rem; +} + +.dots-holder { + display: flex; + padding: 0.25rem 0.1rem; + border-radius: 1rem; + background: $motion-light-transparent; +} + +.dots-holder-success { + background: $pen-transparent; +} + +.dots-holder-error { + background: $error-transparent; +} + +.dot { + width: 0.5rem; + height: 0.5rem; + margin: 0 0.3rem; + border-radius: 100%; +} + +.inactive-step-dot { + background: $motion-transparent; +} + +.active-step-dot { + background: $motion-primary; +} + +.success-dot { + background: $pen-primary; +} + +.error-dot { + background: $error-primary; +} diff --git a/src/components/connection-modal/connection-modal.jsx b/src/components/connection-modal/connection-modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5ddcb33e3b891aa5641cbf63d884688cc83c0183 --- /dev/null +++ b/src/components/connection-modal/connection-modal.jsx @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import keyMirror from 'keymirror'; + +import Box from '../box/box.jsx'; +import Modal from '../modal/modal.jsx'; + +import ScanningStep from '../../containers/scanning-step.jsx'; +import ConnectingStep from './connecting-step.jsx'; +import ConnectedStep from './connected-step.jsx'; +import ErrorStep from './error-step.jsx'; + +import styles from './connection-modal.css'; + +const PHASES = keyMirror({ + scanning: null, + connecting: null, + connected: null, + error: null +}); + +const ConnectionModalComponent = props => ( + <Modal + className={styles.modalContent} + contentLabel={props.name} + headerClassName={styles.header} + headerImage={props.smallDeviceImage} + onRequestClose={props.onCancel} + > + <Box className={styles.body}> + {props.phase === PHASES.scanning && <ScanningStep {...props} />} + {props.phase === PHASES.connecting && <ConnectingStep {...props} />} + {props.phase === PHASES.connected && <ConnectedStep {...props} />} + {props.phase === PHASES.error && <ErrorStep {...props} />} + </Box> + </Modal> +); + +ConnectionModalComponent.propTypes = { + name: PropTypes.node, + onCancel: PropTypes.func.isRequired, + phase: PropTypes.oneOf(Object.keys(PHASES)).isRequired, + smallDeviceImage: PropTypes.string, + title: PropTypes.string.isRequired +}; + +export { + ConnectionModalComponent as default, + PHASES +}; diff --git a/src/components/connection-modal/device-tile.jsx b/src/components/connection-modal/device-tile.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c806453aa0ff60828462376bf9c01a3a38ae550f --- /dev/null +++ b/src/components/connection-modal/device-tile.jsx @@ -0,0 +1,87 @@ +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import React from 'react'; +import bindAll from 'lodash.bindall'; +import Box from '../box/box.jsx'; + +import styles from './connection-modal.css'; + +class DeviceTile extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleConnecting' + ]); + } + handleConnecting () { + this.props.onConnecting(this.props.peripheralId); + } + render () { + return ( + <Box className={styles.deviceTile}> + <Box className={styles.deviceTileName}> + <img + className={styles.deviceTileImage} + src={this.props.smallDeviceImage} + /> + <Box className={styles.deviceTileNameWrapper}> + <Box className={styles.deviceTileNameLabel}> + <FormattedMessage + defaultMessage="Device name" + description="Label for field showing the device name" + id="gui.connection.device-name-label" + /> + </Box> + <Box className={styles.deviceTileNameText}> + {this.props.name} + </Box> + </Box> + </Box> + <Box className={styles.deviceTileWidgets}> + <Box className={styles.signalStrengthMeter}> + <div + className={classNames(styles.signalBar, { + [styles.greenBar]: this.props.rssi > -80 + })} + /> + <div + className={classNames(styles.signalBar, { + [styles.greenBar]: this.props.rssi > -60 + })} + /> + <div + className={classNames(styles.signalBar, { + [styles.greenBar]: this.props.rssi > -40 + })} + /> + <div + className={classNames(styles.signalBar, { + [styles.greenBar]: this.props.rssi > -20 + })} + /> + </Box> + <button + onClick={this.handleConnecting} + > + <FormattedMessage + defaultMessage="Connect" + description="Button to start connecting to a specific device" + id="gui.connection.connect" + /> + </button> + </Box> + </Box> + ); + } +} + +DeviceTile.propTypes = { + name: PropTypes.string, + onConnecting: PropTypes.func, + peripheralId: PropTypes.string, + rssi: PropTypes.number, + smallDeviceImage: PropTypes.string +}; + +export default DeviceTile; diff --git a/src/components/connection-modal/dots.jsx b/src/components/connection-modal/dots.jsx new file mode 100644 index 0000000000000000000000000000000000000000..23814eaf75ecffed01d5f964da940e8c22277448 --- /dev/null +++ b/src/components/connection-modal/dots.jsx @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; + +import Box from '../box/box.jsx'; +import styles from './connection-modal.css'; + +const Dots = props => ( + <Box className={styles.dotsRow}> + <div + className={classNames( + styles.dotsHolder, + { + [styles.dotsHolderError]: props.error, + [styles.dotsHolderSuccess]: props.success + } + )} + > + {Array(props.total).fill(0) + .map((_, i) => { + let type = 'inactive'; + if (props.counter === i) type = 'active'; + if (props.success) type = 'success'; + if (props.error) type = 'error'; + return (<Dot + key={`dot-${i}`} + type={type} + />); + })} + </div> + </Box> +); + +Dots.propTypes = { + counter: PropTypes.number, + error: PropTypes.bool, + success: PropTypes.bool, + total: PropTypes.number +}; + +const Dot = props => ( + <div + className={classNames( + styles.dot, + { + [styles.inactiveStepDot]: props.type === 'inactive', + [styles.activeStepDot]: props.type === 'active', + [styles.successDot]: props.type === 'success', + [styles.errorDot]: props.type === 'error' + } + )} + /> +); + +Dot.propTypes = { + type: PropTypes.string +}; + +export default Dots; diff --git a/src/components/connection-modal/error-step.jsx b/src/components/connection-modal/error-step.jsx new file mode 100644 index 0000000000000000000000000000000000000000..681d74b72a2b278b13ae23d39d853becf5b4116d --- /dev/null +++ b/src/components/connection-modal/error-step.jsx @@ -0,0 +1,76 @@ +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import Box from '../box/box.jsx'; +import Dots from './dots.jsx'; +import helpIcon from './icons/help.svg'; +import backIcon from './icons/back.svg'; + +import styles from './connection-modal.css'; + +const ErrorStep = props => ( + <Box className={styles.body}> + <Box className={styles.activityArea}> + <Box className={styles.centeredRow}> + <div className={styles.deviceActivity}> + <img + className={styles.deviceActivityIcon} + src={props.deviceImage} + /> + </div> + </Box> + </Box> + <Box className={styles.bottomArea}> + <div className={styles.instructions}> + <FormattedMessage + defaultMessage="Oops, looks like something went wrong." + description="The device connection process has encountered an error." + id="gui.connection.errorMessage" + /> + </div> + <Dots + error + total={3} + /> + <Box className={styles.buttonRow}> + <button + className={styles.connectionButton} + onClick={props.onScanning} + > + <img + className={styles.buttonIconLeft} + src={backIcon} + /> + <FormattedMessage + defaultMessage="Try again" + description="Button to initiate trying the device connection again after an error" + id="gui.connection.tryagainbutton" + /> + </button> + <button + className={styles.connectionButton} + onClick={props.onHelp} + > + <img + className={styles.buttonIconLeft} + src={helpIcon} + /> + <FormattedMessage + defaultMessage="Help" + description="Button to view help content" + id="gui.connection.helpbutton" + /> + </button> + </Box> + </Box> + </Box> +); + +ErrorStep.propTypes = { + deviceImage: PropTypes.string.isRequired, + onHelp: PropTypes.func, + onScanning: PropTypes.func +}; + +export default ErrorStep; diff --git a/src/components/connection-modal/icons/back.svg b/src/components/connection-modal/icons/back.svg new file mode 100644 index 0000000000000000000000000000000000000000..42f7508f279eaeee36aac930e3f343d2bfc7c518 Binary files /dev/null and b/src/components/connection-modal/icons/back.svg differ diff --git a/src/components/connection-modal/icons/bluetooth-white.svg b/src/components/connection-modal/icons/bluetooth-white.svg new file mode 100644 index 0000000000000000000000000000000000000000..df2ae141ebce92e1af1ad8f3f6e90b57c8248225 Binary files /dev/null and b/src/components/connection-modal/icons/bluetooth-white.svg differ diff --git a/src/components/connection-modal/icons/cancel.svg b/src/components/connection-modal/icons/cancel.svg new file mode 100644 index 0000000000000000000000000000000000000000..b30decdb65a22a832208426f4258a5fa1dea00b7 Binary files /dev/null and b/src/components/connection-modal/icons/cancel.svg differ diff --git a/src/components/connection-modal/icons/close.svg b/src/components/connection-modal/icons/close.svg new file mode 100644 index 0000000000000000000000000000000000000000..a537fc800d01115552a00073103be09cf5ce9fcf Binary files /dev/null and b/src/components/connection-modal/icons/close.svg differ diff --git a/src/components/connection-modal/icons/help.svg b/src/components/connection-modal/icons/help.svg new file mode 100644 index 0000000000000000000000000000000000000000..2938e8340ebb54dd699f80eaf48794bd383a96a4 Binary files /dev/null and b/src/components/connection-modal/icons/help.svg differ diff --git a/src/components/connection-modal/icons/refresh.svg b/src/components/connection-modal/icons/refresh.svg new file mode 100644 index 0000000000000000000000000000000000000000..3d4aebb47071a08f9214c5e4c5eafb035e2b86ea Binary files /dev/null and b/src/components/connection-modal/icons/refresh.svg differ diff --git a/src/components/connection-modal/icons/searching.png b/src/components/connection-modal/icons/searching.png new file mode 100644 index 0000000000000000000000000000000000000000..260f3227f2ef09cc1aa6fbca0fc928e2b5be0e28 Binary files /dev/null and b/src/components/connection-modal/icons/searching.png differ diff --git a/src/components/connection-modal/scanning-step.jsx b/src/components/connection-modal/scanning-step.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1728e1fd5d55df75790f0301ef00386783d95b15 --- /dev/null +++ b/src/components/connection-modal/scanning-step.jsx @@ -0,0 +1,103 @@ +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import Box from '../box/box.jsx'; +import DeviceTile from './device-tile.jsx'; +import Dots from './dots.jsx'; + +import radarIcon from './icons/searching.png'; +import refreshIcon from './icons/refresh.svg'; + +import styles from './connection-modal.css'; + +const ScanningStep = props => ( + <Box className={styles.body}> + <Box className={styles.activityArea}> + {props.scanning ? ( + props.deviceList.length === 0 ? ( + <div className={styles.activityAreaInfo}> + <div className={styles.centeredRow}> + <img + className={styles.radar} + src={radarIcon} + /> + <FormattedMessage + defaultMessage="Looking for devices" + description="Text shown while scanning for devices" + id="gui.connection.scanning.lookingfordevices" + /> + </div> + </div> + ) : ( + <Box className={styles.deviceTilePane}> + {props.deviceList.map(device => + (<DeviceTile + key={device.peripheralId} + name={device.name} + peripheralId={device.peripheralId} + rssi={device.rssi} + smallDeviceImage={props.smallDeviceImage} + onConnecting={props.onConnecting} + />) + )} + </Box> + ) + ) : ( + <Box className={styles.instructions}> + <FormattedMessage + defaultMessage="No devices found" + description="Text shown when no devices could be found" + id="gui.connection.scanning.noDevicesFound" + /> + </Box> + )} + </Box> + <Box className={styles.bottomArea}> + <Box className={styles.instructions}> + <FormattedMessage + defaultMessage="Select your device in the list above." + description="Prompt for choosing a device to connect to" + id="gui.connection.scanning.instructions" + /> + </Box> + <Dots + counter={0} + total={3} + /> + <button + className={styles.connectionButton} + onClick={props.onRefresh} + > + <FormattedMessage + defaultMessage="Refresh" + description="Button in prompt for starting a search" + id="gui.connection.search" + /> + <img + className={styles.buttonIconRight} + src={refreshIcon} + /> + </button> + </Box> + </Box> +); + +ScanningStep.propTypes = { + deviceList: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + rssi: PropTypes.number, + peripheralId: PropTypes.string + })), + onConnecting: PropTypes.func, + onRefresh: PropTypes.func, + scanning: PropTypes.bool.isRequired, + smallDeviceImage: PropTypes.string +}; + +ScanningStep.defaultProps = { + deviceList: [], + scanning: true +}; + +export default ScanningStep; diff --git a/src/components/import-modal/import-modal.css b/src/components/import-modal/import-modal.css index b41c02b1baaa92923fc92af9ecbe3d22667cef1f..b265efae02fc5ed6c7348a4422d9b84e9e474fa0 100644 --- a/src/components/import-modal/import-modal.css +++ b/src/components/import-modal/import-modal.css @@ -45,7 +45,7 @@ $sides: 20rem; box-sizing: border-box; width: 100%; - background-color: $pen-primary; + background-color: $extensions-primary; } .header-item { @@ -129,12 +129,12 @@ $sides: 20rem; font-weight: bold; font-size: .875rem; cursor: pointer; - border: 0px solid $pen-primary; + border: 0px solid $extensions-primary; outline: none; } .input-row button.ok-button { - background: $pen-primary; + background: $extensions-primary; color: white; } diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 8977d3f931d2069b805a8347ef887f69d78953e5..19e88bc6ae5fbca3ce3892f67b812c74d108492b 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -106,7 +106,10 @@ class LibraryItem extends React.PureComponent { } LibraryItem.propTypes = { - description: PropTypes.string, + description: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node + ]), disabled: PropTypes.bool, featured: PropTypes.bool, iconURL: PropTypes.string.isRequired, diff --git a/src/components/menu-bar/icon--help.svg b/src/components/menu-bar/icon--help.svg deleted file mode 100644 index 6974a7f45f8c740074060a3414eadc02cefd0084..0000000000000000000000000000000000000000 Binary files a/src/components/menu-bar/icon--help.svg and /dev/null differ diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index eba5f885eff82eae922578bdb57acaa75efbecd0..45669f8d6ed979207c8d2774b55504df885118c0 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -151,9 +151,7 @@ } .help-icon { - height: 2rem; - width: 3rem; - margin-top: 0.5rem; + margin-right: 0.35rem; } .account-nav-menu, .mystuff-button { diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 164e9c78e7160686b988084d6fddfabf003e0ffc..b843e05b45aafbbd4b817b7c097d7d4586315ee2 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import {connect} from 'react-redux'; import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl'; import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; import React from 'react'; import Box from '../box/box.jsx'; @@ -30,6 +31,7 @@ import { import styles from './menu-bar.css'; +import helpIcon from '../../lib/assets/icon--tutorials.svg'; import mystuffIcon from './icon--mystuff.png'; import feedbackIcon from './icon--feedback.svg'; import profileIcon from './icon--profile.png'; @@ -39,18 +41,16 @@ import languageIcon from '../language-selector/language-icon.svg'; import scratchLogo from './scratch-logo.svg'; -import helpIcon from './icon--help.svg'; - const ariaMessages = defineMessages({ language: { id: 'gui.menuBar.LanguageSelector', defaultMessage: 'language selector', description: 'accessibility text for the language selection menu' }, - howTo: { - id: 'gui.menuBar.howToLibrary', - defaultMessage: 'How-to Library', - description: 'accessibility text for the how-to library button' + tutorials: { + id: 'gui.menuBar.tutorialsLibrary', + defaultMessage: 'Tutorials', + description: 'accessibility text for the tutorials button' } }); @@ -128,297 +128,313 @@ MenuBarMenu.propTypes = { open: PropTypes.bool, place: PropTypes.oneOf(['left', 'right']) }; - -const MenuBar = props => ( - <Box className={styles.menuBar}> - <div className={styles.mainMenu}> - <div className={styles.fileGroup}> - <div className={classNames(styles.menuBarItem)}> - <img - alt="Scratch" - className={styles.scratchLogo} - draggable={false} - src={scratchLogo} - /> - </div> - <div - className={classNames(styles.menuBarItem, styles.hoverable, { - [styles.active]: props.languageMenuOpen - })} - onMouseUp={props.onClickLanguage} - > - <MenuBarItemTooltip - enable={window.location.search.indexOf('enable=language') !== -1} - id="menubar-selector" - place="right" - > - <div - aria-label={props.intl.formatMessage(ariaMessages.language)} - className={classNames(styles.languageMenu)} - > - <img - className={styles.languageIcon} - src={languageIcon} - /> +class MenuBar extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleLanguageMouseUp' + ]); + } + handleLanguageMouseUp (e) { + if (!this.props.languageMenuOpen) { + this.props.onClickLanguage(e); + } + } + render () { + return ( + <Box className={styles.menuBar}> + <div className={styles.mainMenu}> + <div className={styles.fileGroup}> + <div className={classNames(styles.menuBarItem)}> <img - className={styles.dropdownCaret} - src={dropdownCaret} + alt="Scratch" + className={styles.scratchLogo} + draggable={false} + src={scratchLogo} /> </div> - <MenuBarMenu - open={props.languageMenuOpen} - onRequestClose={props.onRequestCloseLanguage} + <div + className={classNames(styles.menuBarItem, styles.hoverable, { + [styles.active]: this.props.languageMenuOpen + })} + onMouseUp={this.handleLanguageMouseUp} > - <LanguageSelector /> - </MenuBarMenu> - - </MenuBarItemTooltip> - </div> - <div - className={classNames(styles.menuBarItem, styles.hoverable, { - [styles.active]: props.fileMenuOpen - })} - onMouseUp={props.onClickFile} - > - <div className={classNames(styles.fileMenu)}> - <FormattedMessage - defaultMessage="File" - description="Text for file dropdown menu" - id="gui.menuBar.file" - /> - </div> - <MenuBarMenu - open={props.fileMenuOpen} - onRequestClose={props.onRequestCloseFile} - > - <MenuItemTooltip id="new"> - <MenuItem> - <FormattedMessage - defaultMessage="New" - description="Menu bar item for creating a new project" - id="gui.menuBar.new" - /> - </MenuItem> - </MenuItemTooltip> - <MenuSection> - <MenuItemTooltip id="save"> - <MenuItem> - <FormattedMessage - defaultMessage="Save now" - description="Menu bar item for saving now" - id="gui.menuBar.saveNow" - /> - </MenuItem> - </MenuItemTooltip> - <MenuItemTooltip id="copy"> - <MenuItem> - <FormattedMessage - defaultMessage="Save as a copy" - description="Menu bar item for saving as a copy" - id="gui.menuBar.saveAsCopy" - /></MenuItem> - </MenuItemTooltip> - </MenuSection> - <MenuSection> - <ProjectLoader>{(renderFileInput, loadProject, loadProps) => ( - <MenuItem - onClick={loadProject} - {...loadProps} + <MenuBarItemTooltip + enable={window.location.search.indexOf('enable=language') !== -1} + id="menubar-selector" + place="right" + > + <div + aria-label={this.props.intl.formatMessage(ariaMessages.language)} + className={classNames(styles.languageMenu)} > - <FormattedMessage - defaultMessage="Load from your computer" - description="Menu bar item for uploading a project from your computer" - id="gui.menuBar.uploadFromComputer" + <img + className={styles.languageIcon} + src={languageIcon} /> - {renderFileInput()} - </MenuItem> - )}</ProjectLoader> - <ProjectSaver>{(saveProject, saveProps) => ( - <MenuItem - onClick={saveProject} - {...saveProps} - > - <FormattedMessage - defaultMessage="Save to your computer" - description="Menu bar item for downloading a project to your computer" - id="gui.menuBar.downloadToComputer" + <img + className={styles.dropdownCaret} + src={dropdownCaret} /> - </MenuItem> - )}</ProjectSaver> - </MenuSection> - </MenuBarMenu> - </div> - <div - className={classNames(styles.menuBarItem, styles.hoverable, { - [styles.active]: props.editMenuOpen - })} - onMouseUp={props.onClickEdit} - > - <div className={classNames(styles.editMenu)}> - <FormattedMessage - defaultMessage="Edit" - description="Text for edit dropdown menu" - id="gui.menuBar.edit" - /> + </div> + <MenuBarMenu + open={this.props.languageMenuOpen} + onRequestClose={this.props.onRequestCloseLanguage} + > + <LanguageSelector /> + </MenuBarMenu> + + </MenuBarItemTooltip> + </div> + <div + className={classNames(styles.menuBarItem, styles.hoverable, { + [styles.active]: this.props.fileMenuOpen + })} + onMouseUp={this.props.onClickFile} + > + <div className={classNames(styles.fileMenu)}> + <FormattedMessage + defaultMessage="File" + description="Text for file dropdown menu" + id="gui.menuBar.file" + /> + </div> + <MenuBarMenu + open={this.props.fileMenuOpen} + onRequestClose={this.props.onRequestCloseFile} + > + <MenuItemTooltip id="new"> + <MenuItem> + <FormattedMessage + defaultMessage="New" + description="Menu bar item for creating a new project" + id="gui.menuBar.new" + /> + </MenuItem> + </MenuItemTooltip> + <MenuSection> + <MenuItemTooltip id="save"> + <MenuItem> + <FormattedMessage + defaultMessage="Save now" + description="Menu bar item for saving now" + id="gui.menuBar.saveNow" + /> + </MenuItem> + </MenuItemTooltip> + <MenuItemTooltip id="copy"> + <MenuItem> + <FormattedMessage + defaultMessage="Save as a copy" + description="Menu bar item for saving as a copy" + id="gui.menuBar.saveAsCopy" + /></MenuItem> + </MenuItemTooltip> + </MenuSection> + <MenuSection> + <ProjectLoader>{(renderFileInput, loadProject, loadProps) => ( + <MenuItem + onClick={loadProject} + {...loadProps} + > + <FormattedMessage + defaultMessage="Load from your computer" + description="Menu bar item for uploading a project from your computer" + id="gui.menuBar.uploadFromComputer" + /> + {renderFileInput()} + </MenuItem> + )}</ProjectLoader> + <ProjectSaver>{(saveProject, saveProps) => ( + <MenuItem + onClick={saveProject} + {...saveProps} + > + <FormattedMessage + defaultMessage="Save to your computer" + description="Menu bar item for downloading a project to your computer" + id="gui.menuBar.downloadToComputer" + /> + </MenuItem> + )}</ProjectSaver> + </MenuSection> + </MenuBarMenu> + </div> + <div + className={classNames(styles.menuBarItem, styles.hoverable, { + [styles.active]: this.props.editMenuOpen + })} + onMouseUp={this.props.onClickEdit} + > + <div className={classNames(styles.editMenu)}> + <FormattedMessage + defaultMessage="Edit" + description="Text for edit dropdown menu" + id="gui.menuBar.edit" + /> + </div> + <MenuBarMenu + open={this.props.editMenuOpen} + onRequestClose={this.props.onRequestCloseEdit} + > + <MenuItemTooltip id="undo"> + <MenuItem> + <FormattedMessage + defaultMessage="Undo" + description="Menu bar item for undoing" + id="gui.menuBar.undo" + /> + </MenuItem> + </MenuItemTooltip> + <MenuItemTooltip id="redo"> + <MenuItem> + <FormattedMessage + defaultMessage="Redo" + description="Menu bar item for redoing" + id="gui.menuBar.redo" + /> + </MenuItem> + </MenuItemTooltip> + <MenuSection> + <MenuItemTooltip id="turbo"> + <MenuItem> + <FormattedMessage + defaultMessage="Turbo mode" + description="Menu bar item for toggling turbo mode" + id="gui.menuBar.turboMode" + /> + </MenuItem> + </MenuItemTooltip> + </MenuSection> + </MenuBarMenu> + </div> </div> - <MenuBarMenu - open={props.editMenuOpen} - onRequestClose={props.onRequestCloseEdit} + <Divider className={classNames(styles.divider)} /> + <div + aria-label={this.props.intl.formatMessage(ariaMessages.tutorials)} + className={classNames(styles.menuBarItem, styles.hoverable)} + onClick={this.props.onOpenTipLibrary} > - <MenuItemTooltip id="undo"> - <MenuItem> + <img + className={styles.helpIcon} + src={helpIcon} + /> + <FormattedMessage {...ariaMessages.tutorials} /> + </div> + <Divider className={classNames(styles.divider)} /> + <div className={classNames(styles.menuBarItem)}> + <MenuBarItemTooltip id="title-field"> + <input + disabled + className={classNames(styles.titleField)} + placeholder="Untitled-1" + /> + </MenuBarItemTooltip> + </div> + <div className={classNames(styles.menuBarItem)}> + <MenuBarItemTooltip id="share-button"> + <Button className={classNames(styles.shareButton)}> <FormattedMessage - defaultMessage="Undo" - description="Menu bar item for undoing" - id="gui.menuBar.undo" + defaultMessage="Share" + description="Label for project share button" + id="gui.menuBar.share" /> - </MenuItem> - </MenuItemTooltip> - <MenuItemTooltip id="redo"> - <MenuItem> + </Button> + </MenuBarItemTooltip> + </div> + <div className={classNames(styles.menuBarItem, styles.communityButtonWrapper)}> + {this.props.enableCommunity ? + <Button + className={classNames(styles.communityButton)} + iconClassName={styles.communityButtonIcon} + iconSrc={communityIcon} + onClick={this.props.onSeeCommunity} + > <FormattedMessage - defaultMessage="Redo" - description="Menu bar item for redoing" - id="gui.menuBar.redo" + defaultMessage="See Community" + description="Label for see community button" + id="gui.menuBar.seeCommunity" /> - </MenuItem> - </MenuItemTooltip> - <MenuSection> - <MenuItemTooltip id="turbo"> - <MenuItem> + </Button> : + <MenuBarItemTooltip id="community-button"> + <Button + className={classNames(styles.communityButton)} + iconClassName={styles.communityButtonIcon} + iconSrc={communityIcon} + > <FormattedMessage - defaultMessage="Turbo mode" - description="Menu bar item for toggling turbo mode" - id="gui.menuBar.turboMode" + defaultMessage="See Community" + description="Label for see community button" + id="gui.menuBar.seeCommunity" /> - </MenuItem> - </MenuItemTooltip> - </MenuSection> - </MenuBarMenu> + </Button> + </MenuBarItemTooltip> + } + </div> </div> - </div> - <Divider className={classNames(styles.divider)} /> - <div className={classNames(styles.menuBarItem)}> - <MenuBarItemTooltip id="title-field"> - <input - disabled - className={classNames(styles.titleField)} - placeholder="Untitled-1" - /> - </MenuBarItemTooltip> - </div> - <div className={classNames(styles.menuBarItem)}> - <MenuBarItemTooltip id="share-button"> - <Button className={classNames(styles.shareButton)}> - <FormattedMessage - defaultMessage="Share" - description="Label for project share button" - id="gui.menuBar.share" - /> - </Button> - </MenuBarItemTooltip> - </div> - <div className={classNames(styles.menuBarItem, styles.communityButtonWrapper)}> - {props.enableCommunity ? - <Button - className={classNames(styles.communityButton)} - iconClassName={styles.communityButtonIcon} - iconSrc={communityIcon} - onClick={props.onSeeCommunity} + <div className={classNames(styles.menuBarItem, styles.feedbackButtonWrapper)}> + <a + className={styles.feedbackLink} + href="https://scratch.mit.edu/discuss/topic/299791/" + rel="noopener noreferrer" + target="_blank" > - <FormattedMessage - defaultMessage="See Community" - description="Label for see community button" - id="gui.menuBar.seeCommunity" - /> - </Button> : - <MenuBarItemTooltip id="community-button"> <Button - className={classNames(styles.communityButton)} - iconClassName={styles.communityButtonIcon} - iconSrc={communityIcon} + className={styles.feedbackButton} + iconSrc={feedbackIcon} > <FormattedMessage - defaultMessage="See Community" - description="Label for see community button" - id="gui.menuBar.seeCommunity" + defaultMessage="Give Feedback" + description="Label for feedback form modal button" + id="gui.menuBar.giveFeedback" /> </Button> - </MenuBarItemTooltip> - } - </div> - </div> - <div className={classNames(styles.menuBarItem, styles.feedbackButtonWrapper)}> - <a - className={styles.feedbackLink} - href="https://scratch.mit.edu/discuss/topic/299791/" - rel="noopener noreferrer" - target="_blank" - > - <Button - className={styles.feedbackButton} - iconSrc={feedbackIcon} - > - <FormattedMessage - defaultMessage="Give Feedback" - description="Label for feedback form modal button" - id="gui.menuBar.giveFeedback" - /> - </Button> - </a> - </div> - <div className={styles.accountInfoWrapper}> - <div - aria-label={props.intl.formatMessage(ariaMessages.howTo)} - className={classNames(styles.menuBarItem, styles.hoverable)} - onClick={props.onOpenTipLibrary} - > - <img - className={styles.helpIcon} - src={helpIcon} - /> - </div> - <MenuBarItemTooltip id="mystuff"> - <div - className={classNames( - styles.menuBarItem, - styles.hoverable, - styles.mystuffButton - )} - > - <img - className={styles.mystuffIcon} - src={mystuffIcon} - /> + </a> </div> - </MenuBarItemTooltip> - <MenuBarItemTooltip - id="account-nav" - place="left" - > - <div - className={classNames( - styles.menuBarItem, - styles.hoverable, - styles.accountNavMenu - )} - > - <img - className={styles.profileIcon} - src={profileIcon} - /> - <span> - {'scratch-cat' /* @todo username */} - </span> - <img - className={styles.dropdownCaretIcon} - src={dropdownCaret} - /> + <div className={styles.accountInfoWrapper}> + <MenuBarItemTooltip id="mystuff"> + <div + className={classNames( + styles.menuBarItem, + styles.hoverable, + styles.mystuffButton + )} + > + <img + className={styles.mystuffIcon} + src={mystuffIcon} + /> + </div> + </MenuBarItemTooltip> + <MenuBarItemTooltip + id="account-nav" + place="left" + > + <div + className={classNames( + styles.menuBarItem, + styles.hoverable, + styles.accountNavMenu + )} + > + <img + className={styles.profileIcon} + src={profileIcon} + /> + <span> + {'scratch-cat' /* @todo username */} + </span> + <img + className={styles.dropdownCaretIcon} + src={dropdownCaret} + /> + </div> + </MenuBarItemTooltip> </div> - </MenuBarItemTooltip> - </div> - </Box> -); + </Box> + ); + } +} MenuBar.propTypes = { editMenuOpen: PropTypes.bool, diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css index 3aa21280beba80b61181349e5ae8a1bc38b52459..9a5a035a7de18edba2d6d5343fdc4fa97f213878 100644 --- a/src/components/modal/modal.css +++ b/src/components/modal/modal.css @@ -83,6 +83,10 @@ $sides: 20rem; user-select: none; } +.header-image { + margin-right: 0.5rem; +} + .header-item-filter { display: flex; flex-basis: $sides; diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx index dff3ce8537b748e0c5c554d25652b8f30615efdc..940af75586f59e05291f9970b5bf972f353ccdb3 100644 --- a/src/components/modal/modal.jsx +++ b/src/components/modal/modal.jsx @@ -26,13 +26,19 @@ const ModalComponent = props => ( direction="column" grow={1} > - <div className={styles.header}> + <div className={classNames(styles.header, props.headerClassName)}> <div className={classNames( styles.headerItem, styles.headerItemTitle )} > + {props.headerImage ? ( + <img + className={styles.headerImage} + src={props.headerImage} + /> + ) : null} {props.contentLabel} </div> <div @@ -74,6 +80,8 @@ ModalComponent.propTypes = { PropTypes.object ]).isRequired, fullScreen: PropTypes.bool, + headerClassName: PropTypes.string, + headerImage: PropTypes.string, onRequestClose: PropTypes.func }; diff --git a/src/components/preview-modal/preview-modal.css b/src/components/preview-modal/preview-modal.css index 2c35acf63e0f78fbca407f74b063e691e105f2ec..d6cf6dc4568fd5178ea0849bbd92551838d14bf9 100644 --- a/src/components/preview-modal/preview-modal.css +++ b/src/components/preview-modal/preview-modal.css @@ -69,8 +69,8 @@ } .button-row button.view-project-button { - background: $pen-primary; - border-color: $pen-primary; + background: $extensions-primary; + border-color: $extensions-primary; color: white; } diff --git a/src/components/stage-selector/stage-selector.css b/src/components/stage-selector/stage-selector.css index e845160faee34e2c5768d08883f121079ad06275..4e35dd4b68fbbbfa2b4794510beaa2cd22b83b50 100644 --- a/src/components/stage-selector/stage-selector.css +++ b/src/components/stage-selector/stage-selector.css @@ -20,10 +20,12 @@ $header-height: calc($stage-menu-height - 2px); border-style: solid; border-bottom: 0; cursor: pointer; - transition: border-color 0.25s ease-out, box-shadow 0.25s ease-out; + transition: all 0.25s ease; } .stage-selector.is-selected { + border-top-left-radius: .625rem; + border-top-right-radius: .625rem; border-color: $motion-primary; box-shadow: 0px 0px 0px 4px $motion-transparent; } @@ -44,6 +46,7 @@ $header-height: calc($stage-menu-height - 2px); border-top-right-radius: $space; border-bottom: 1px solid $ui-black-transparent; width: 100%; + transition: background-color 0.25s ease; } .header-title { @@ -53,6 +56,15 @@ $header-height: calc($stage-menu-height - 2px); /* @todo: make this a mixin for all UI text labels */ user-select: none; + transition: color 0.25s ease; +} + +.stage-selector.is-selected .header { + background-color: $motion-primary; +} + +.stage-selector.is-selected .header-title { + color: $ui-white; } .count { @@ -71,9 +83,11 @@ $header-height: calc($stage-menu-height - 2px); .costume-canvas { display: block; + margin-top: .25rem; width: 100%; user-select: none; - border-bottom: 1px solid $ui-black-transparent; + border: 1px solid $ui-black-transparent; + border-radius: .25rem; box-shadow: inset 0 0 4px $ui-black-transparent; } diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx index 9d40d3c249e9d413a89bbfdc9d8f15e82b6ebf98..50161bf8cbbfbc389ddcbd196201badfea408687 100644 --- a/src/components/stage-selector/stage-selector.jsx +++ b/src/components/stage-selector/stage-selector.jsx @@ -80,9 +80,9 @@ const StageSelector = props => { {url ? ( <CostumeCanvas className={styles.costumeCanvas} - height={54} + height={48} url={url} - width={72} + width={64} /> ) : null} <div className={styles.label}> diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 40ad9234ed774d4bdbe0b3e0cc8ee78a14de89e7..c81c5d21ad6bc843712795eec0c8d37655db91a0 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -9,8 +9,10 @@ import VM from 'scratch-vm'; import analytics from '../lib/analytics'; import Prompt from './prompt.jsx'; +import ConnectionModal from './connection-modal.jsx'; import BlocksComponent from '../components/blocks/blocks.jsx'; import ExtensionLibrary from './extension-library.jsx'; +import extensionData from '../lib/libraries/extensions/index.jsx'; import CustomProcedures from './custom-procedures.jsx'; import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants'; @@ -38,6 +40,9 @@ class Blocks extends React.Component { 'attachVM', 'detachVM', 'handleCategorySelected', + 'handleConnectionModalStart', + 'handleConnectionModalClose', + 'handleStatusButtonUpdate', 'handlePromptStart', 'handlePromptCallback', 'handlePromptClose', @@ -56,9 +61,11 @@ class Blocks extends React.Component { 'setLocale' ]); this.ScratchBlocks.prompt = this.handlePromptStart; + this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart; this.state = { workspaceMetrics: {}, - prompt: null + prompt: null, + connectionModal: null }; this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100); this.toolboxUpdateQueue = []; @@ -94,6 +101,7 @@ class Blocks extends React.Component { shouldComponentUpdate (nextProps, nextState) { return ( this.state.prompt !== nextState.prompt || + this.state.connectionModal !== nextState.connectionModal || this.props.isVisible !== nextProps.isVisible || this.props.toolboxXML !== nextProps.toolboxXML || this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible || @@ -148,11 +156,12 @@ class Blocks extends React.Component { setLocale () { this.workspace.getFlyout().setRecyclingEnabled(false); this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); - this.props.vm.setLocale(this.props.locale, this.props.messages); - - this.workspace.updateToolbox(this.props.toolboxXML); - this.props.vm.refreshWorkspace(); - this.workspace.getFlyout().setRecyclingEnabled(true); + this.props.vm.setLocale(this.props.locale, this.props.messages) + .then(() => { + this.workspace.updateToolbox(this.props.toolboxXML); + this.props.vm.refreshWorkspace(); + this.workspace.getFlyout().setRecyclingEnabled(true); + }); } updateToolbox () { @@ -204,6 +213,8 @@ class Blocks extends React.Component { this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate); this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded); this.props.vm.addListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); + this.props.vm.addListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); + this.props.vm.addListener('PERIPHERAL_ERROR', this.handleStatusButtonUpdate); } detachVM () { this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); @@ -215,6 +226,8 @@ class Blocks extends React.Component { this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate); this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded); this.props.vm.removeListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); + this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); + this.props.vm.removeListener('PERIPHERAL_ERROR', this.handleStatusButtonUpdate); } updateToolboxBlockValue (id, value) { @@ -311,6 +324,11 @@ class Blocks extends React.Component { this.handleExtensionAdded(blocksInfo); } handleCategorySelected (categoryId) { + const extension = extensionData.find(ext => ext.extensionId === categoryId); + if (extension && extension.launchDeviceConnectionFlow) { + this.handleConnectionModalStart(categoryId); + } + this.withToolboxUpdates(() => { this.workspace.toolbox_.setSelectedCategoryById(categoryId); }); @@ -330,6 +348,23 @@ class Blocks extends React.Component { p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE; this.setState(p); } + handleConnectionModalStart (extensionId) { + const extension = extensionData.find(ext => ext.extensionId === extensionId); + if (extension) { + this.setState({connectionModal: { + extensionId: extensionId, + deviceImage: extension.deviceImage, + smallDeviceImage: extension.smallDeviceImage, + name: extension.name + }}); + } + } + handleConnectionModalClose () { + this.setState({connectionModal: null}); + } + handleStatusButtonUpdate () { + this.ScratchBlocks.refreshStatusButtons(this.workspace); + } handlePromptCallback (input, optionSelection) { this.state.prompt.callback(input, optionSelection, (optionSelection === 'local') ? [] : @@ -381,6 +416,17 @@ class Blocks extends React.Component { onOk={this.handlePromptCallback} /> ) : null} + {this.state.connectionModal ? ( + <ConnectionModal + deviceImage={this.state.connectionModal.deviceImage} + extensionId={this.state.connectionModal.extensionId} + name={this.state.connectionModal.name} + smallDeviceImage={this.state.connectionModal.smallDeviceImage} + vm={vm} + onCancel={this.handleConnectionModalClose} + onStatusButtonUpdate={this.handleStatusButtonUpdate} + /> + ) : null} {extensionLibraryVisible ? ( <ExtensionLibrary vm={vm} diff --git a/src/containers/connection-modal.jsx b/src/containers/connection-modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ffb5c6ddf5c3b4ec05d61d1cbca8b7c05ac13d3d --- /dev/null +++ b/src/containers/connection-modal.jsx @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import bindAll from 'lodash.bindall'; +import ConnectionModalComponent, {PHASES} from '../components/connection-modal/connection-modal.jsx'; +import VM from 'scratch-vm'; + +class ConnectionModal extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleScanning', + 'handleConnected', + 'handleConnecting', + 'handleDisconnect', + 'handleError' + ]); + this.state = { + phase: PHASES.scanning + }; + } + componentDidMount () { + this.props.vm.on('PERIPHERAL_CONNECTED', this.handleConnected); + this.props.vm.on('PERIPHERAL_ERROR', this.handleError); + + // Check if we're already connected + if (this.props.vm.getPeripheralIsConnected(this.props.extensionId)) { + this.handleConnected(); + } + + } + componentWillUnmount () { + this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleConnected); + this.props.vm.removeListener('PERIPHERAL_ERROR', this.handleError); + } + handleScanning () { + this.setState({ + phase: PHASES.scanning + }); + } + handleConnecting (peripheralId) { + this.props.vm.connectToPeripheral(this.props.extensionId, peripheralId); + this.setState({ + phase: PHASES.connecting + }); + } + handleDisconnect () { + this.props.onStatusButtonUpdate(this.props.extensionId, 'not ready'); + this.props.vm.disconnectExtensionSession(this.props.extensionId); + this.props.onCancel(); + } + handleCancel () { + // If we're not connected to a device, close the websocket so we stop scanning. + if (!this.props.vm.getPeripheralIsConnected(this.props.extensionId)) { + this.props.vm.disconnectExtensionSession(this.props.extensionId); + } + this.props.onCancel(); + } + handleError () { + this.props.onStatusButtonUpdate(); + this.setState({ + phase: PHASES.error + }); + } + handleConnected () { + this.props.onStatusButtonUpdate(); + this.setState({ + phase: PHASES.connected + }); + } + handleHelp () { + // @todo: implement the help button + } + render () { + return ( + <ConnectionModalComponent + deviceImage={this.props.deviceImage} + extensionId={this.props.extensionId} + name={this.props.name} + phase={this.state.phase} + smallDeviceImage={this.props.smallDeviceImage} + title={this.props.extensionId} + vm={this.props.vm} + onCancel={this.props.onCancel} + onConnected={this.handleConnected} + onConnecting={this.handleConnecting} + onDisconnect={this.handleDisconnect} + onHelp={this.handleHelp} + onScanning={this.handleScanning} + /> + ); + } +} + +ConnectionModal.propTypes = { + deviceImage: PropTypes.string.isRequired, + extensionId: PropTypes.string.isRequired, + name: PropTypes.node.isRequired, + onCancel: PropTypes.func.isRequired, + onStatusButtonUpdate: PropTypes.func.isRequired, + smallDeviceImage: PropTypes.string.isRequired, + vm: PropTypes.instanceOf(VM).isRequired +}; + +export default ConnectionModal; diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx index a1af3bac41fcd71221b6414785ebbe6fcb53a1e1..a23f1e6c8c74dada999b93aae070ebaebfe025e0 100644 --- a/src/containers/extension-library.jsx +++ b/src/containers/extension-library.jsx @@ -46,10 +46,17 @@ class ExtensionLibrary extends React.PureComponent { }); } } + let gaLabel = ''; + if (typeof (item.name) === 'string') { + gaLabel = item.name; + } else { + // Name is localized, get the default message for the gaLabel + gaLabel = item.name.props.defaultMessage; + } analytics.event({ category: 'library', action: 'Select Extension', - label: item.name + label: gaLabel }); } render () { diff --git a/src/containers/scanning-step.jsx b/src/containers/scanning-step.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ecc19318913845154c248f716dfad6d440147c2e --- /dev/null +++ b/src/containers/scanning-step.jsx @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import bindAll from 'lodash.bindall'; +import ScanningStepComponent from '../components/connection-modal/scanning-step.jsx'; +import VM from 'scratch-vm'; + +class ScanningStep extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handlePeripheralListUpdate', + 'handlePeripheralScanTimeout', + 'handleRefresh' + ]); + this.state = { + scanning: true, + deviceList: [] + }; + } + componentDidMount () { + this.props.vm.startDeviceScan(this.props.extensionId); + this.props.vm.on( + 'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate); + this.props.vm.on( + 'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout); + } + componentWillUnmount () { + // @todo: stop the device scan here + this.props.vm.removeListener( + 'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate); + this.props.vm.removeListener( + 'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout); + } + handlePeripheralScanTimeout () { + this.setState({scanning: false}); + } + handlePeripheralListUpdate (newList) { + // TODO: sort peripherals by signal strength? so they don't jump around + const peripheralArray = Object.keys(newList).map(id => + newList[id] + ); + this.setState({deviceList: peripheralArray}); + } + handleRefresh () { + this.props.vm.startDeviceScan(this.props.extensionId); + this.setState({ + scanning: true, + deviceList: [] + }); + } + render () { + return ( + <ScanningStepComponent + deviceList={this.state.deviceList} + phase={this.state.phase} + smallDeviceImage={this.props.smallDeviceImage} + title={this.props.extensionId} + onConnected={this.props.onConnected} + onConnecting={this.props.onConnecting} + onRefresh={this.handleRefresh} + /> + ); + } +} + +ScanningStep.propTypes = { + extensionId: PropTypes.string.isRequired, + onConnected: PropTypes.func.isRequired, + onConnecting: PropTypes.func.isRequired, + smallDeviceImage: PropTypes.string, + vm: PropTypes.instanceOf(VM).isRequired +}; + +export default ScanningStep; diff --git a/src/containers/tips-library.jsx b/src/containers/tips-library.jsx index 4db0d06659874b4f0f9d38246297ac7824125503..ebbee3f3022175f797e021e1e0253a0821f5258c 100644 --- a/src/containers/tips-library.jsx +++ b/src/containers/tips-library.jsx @@ -20,9 +20,9 @@ import { const messages = defineMessages({ tipsLibraryTitle: { - defaultMessage: 'How-Tos', - description: 'Heading for the help/how-tos library', - id: 'gui.tipsLibrary.howtos' + defaultMessage: 'Choose a Tutorial', + description: 'Heading for the help/tutorials library', + id: 'gui.tipsLibrary.tutorials' } }); diff --git a/src/css/colors.css b/src/css/colors.css index 8dbc51ecdbeb9157cb0425cdcba22a941ea5bef6..37ffa62227a6f6ccb42a6170d4652db037f4f769 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -15,6 +15,7 @@ $text-primary-transparent: hsla(225, 15%, 40%, 0.75); $motion-primary: hsla(215, 100%, 65%, 1); /* #4C97FF */ $motion-tertiary: hsla(215, 60%, 50%, 1); /* #3373CC */ $motion-transparent: hsla(215, 100%, 65%, 0.35); /* 35% transparent version of motion-primary */ +$motion-light-transparent: hsla(215, 100%, 65%, 0.1); /* 10% transparent version of motion-primary */ $red-primary: hsla(20, 100%, 55%, 1); /* #FF661A */ $red-tertiary: hsla(20, 100%, 45%, 1); /* #E64D00 */ @@ -27,3 +28,11 @@ $control-primary: hsla(38, 100%, 55%, 1); /* #FFAB19 */ $data-primary: hsla(30, 100%, 55%, 1); /* #FF8C1A */ $pen-primary: hsla(163, 85%, 40%, 1); /* #0FBD8C */ +$pen-transparent: hsla(163, 85%, 40%, 0.25); /* #0FBD8C */ + +$error-primary: hsla(30, 100%, 55%, 1); /* #FF8C1A */ +$error-transparent: hsla(30, 100%, 55%, 0.25); /* #FF8C1A */ + +$extensions-primary: hsla(163, 85%, 40%, 1); /* #0FBD8C */ +$extensions-tertiary: hsla(163, 85%, 30%, 1); /* #0B8E69 */ +$extensions-transparent: hsla(163, 85%, 40%, 0.35); /* 35% transparent version of extensions-primary */ diff --git a/src/lib/assets/icon--tutorials.svg b/src/lib/assets/icon--tutorials.svg new file mode 100644 index 0000000000000000000000000000000000000000..fea09e781b3f88a43750b69f25e6e522846a4466 Binary files /dev/null and b/src/lib/assets/icon--tutorials.svg differ diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index 067dcc6209397782d248a3a04ce8ad11f0f668d9..ab170f9c677924e2be4f4995287e9699e1271ffc 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -34,9 +34,6 @@ class AudioEffects { playbackRate = 1 / pitchRatio; sampleCount = Math.floor(buffer.length / playbackRate); break; - case effectTypes.REVERSE: - buffer.getChannelData(0).reverse(); - break; } if (window.OfflineAudioContext) { this.audioContext = new window.OfflineAudioContext(1, sampleCount, buffer.sampleRate); @@ -46,7 +43,24 @@ class AudioEffects { const sampleScale = 44100 / buffer.sampleRate; this.audioContext = new window.webkitOfflineAudioContext(1, sampleScale * sampleCount, 44100); } - this.buffer = buffer; + + // For the reverse effect we need to manually reverse the data into a new audio buffer + // to prevent overwriting the original, so that the undo stack works correctly. + // Doing buffer.reverse() would mutate the original data. + if (name === effectTypes.REVERSE) { + const originalBufferData = buffer.getChannelData(0); + const newBuffer = this.audioContext.createBuffer(1, buffer.length, buffer.sampleRate); + const newBufferData = newBuffer.getChannelData(0); + const bufferLength = buffer.length; + for (let i = 0; i < bufferLength; i++) { + newBufferData[i] = originalBufferData[bufferLength - i - 1]; + } + this.buffer = newBuffer; + } else { + // All other effects use the original buffer because it is not modified. + this.buffer = buffer; + } + this.source = this.audioContext.createBufferSource(); this.source.buffer = this.buffer; this.source.playbackRate.value = playbackRate; diff --git a/src/lib/blocks.js b/src/lib/blocks.js index c1bde0ff2e69c310977c84308cd73c802655d28f..f6b78b68ee4a325c5d03701e8d484466ba856f0c 100644 --- a/src/lib/blocks.js +++ b/src/lib/blocks.js @@ -208,5 +208,12 @@ export default function (vm) { return monitoredBlock ? monitoredBlock.isMonitored : false; }; + ScratchBlocks.FlyoutExtensionCategoryHeader.getExtensionState = function (extensionId) { + if (vm.getPeripheralIsConnected(extensionId)) { + return ScratchBlocks.StatusButtonState.READY; + } + return ScratchBlocks.StatusButtonState.NOT_READY; + }; + return ScratchBlocks; } diff --git a/src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg b/src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg new file mode 100644 index 0000000000000000000000000000000000000000..7f8c8c395969e202e01d4e18c0225b5ab98c49b0 Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg differ diff --git a/src/lib/libraries/extensions/device-connection/ev3/ev3-small.svg b/src/lib/libraries/extensions/device-connection/ev3/ev3-small.svg new file mode 100644 index 0000000000000000000000000000000000000000..260e2fd26c1197626e40e69c9d60acb691f7f264 Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/ev3/ev3-small.svg differ diff --git a/src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg b/src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg new file mode 100644 index 0000000000000000000000000000000000000000..3ef892bcc456fd9196e72356742391fd74b67a93 Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg differ diff --git a/src/lib/libraries/extensions/device-connection/microbit/microbit-small.svg b/src/lib/libraries/extensions/device-connection/microbit/microbit-small.svg new file mode 100644 index 0000000000000000000000000000000000000000..ead401aba1045eba027ae09168f845a7811c9cec Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/microbit/microbit-small.svg differ diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx index 71404f8fb0e2abd8a47018569487c350ad581074..62da95ea5b04e991841878659f14af4b460e3571 100644 --- a/src/lib/libraries/extensions/index.jsx +++ b/src/lib/libraries/extensions/index.jsx @@ -11,6 +11,12 @@ import ev3Image from './ev3.png'; import boostImage from './boost.png'; import translateImage from './translate.png'; +import ev3DeviceImage from './device-connection/ev3/ev3-hub-illustration.svg'; +import ev3MenuImage from './device-connection/ev3/ev3-small.svg'; + +import microbitDeviceImage from './device-connection/microbit/microbit-illustration.svg'; +import microbitMenuImage from './device-connection/microbit/microbit-small.svg'; + export default [ { name: ( @@ -120,7 +126,10 @@ export default [ /> ), featured: true, - disabled: true + disabled: true, + launchDeviceConnectionFlow: true, + deviceImage: microbitDeviceImage, + smallDeviceImage: microbitMenuImage }, { name: 'LEGO WeDo 2.0', @@ -148,7 +157,10 @@ export default [ /> ), featured: true, - disabled: true + disabled: true, + launchDeviceConnectionFlow: true, + deviceImage: ev3DeviceImage, + smallDeviceImage: ev3MenuImage }, { name: 'LEGO Boost', diff --git a/src/lib/libraries/extensions/translate.png b/src/lib/libraries/extensions/translate.png index 3e25993021b71beb390b2f6b9865b566e88839a6..95961ae52fee6e27c5f7946c41a7e4fd1d22fe7a 100644 Binary files a/src/lib/libraries/extensions/translate.png and b/src/lib/libraries/extensions/translate.png differ diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 1c2a0ae1936d2ef2d373ba87437bc696e80d3467..245c918e51e99de0991e05868741c24263d912e0 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -270,12 +270,12 @@ const looks = function (isStage, targetId) { const sound = function (isStage, targetId) { return ` <category name="%{BKY_CATEGORY_SOUND}" id="sound" colour="#D65CD6" secondaryColour="#BD42BD"> - <block id="${targetId}_sound_play" type="sound_play"> + <block id="${targetId}_sound_playuntildone" type="sound_playuntildone"> <value name="SOUND_MENU"> <shadow type="sound_sounds_menu"/> </value> </block> - <block id="${targetId}_sound_playuntildone" type="sound_playuntildone"> + <block id="${targetId}_sound_play" type="sound_play"> <value name="SOUND_MENU"> <shadow type="sound_sounds_menu"/> </value> diff --git a/src/playground/index.jsx b/src/playground/index.jsx index d35785698dfa2048e32ba5d22f5d2b131abfe994..da2be7a27831b6179acb436d6b441f66e86c9dcf 100644 --- a/src/playground/index.jsx +++ b/src/playground/index.jsx @@ -1,5 +1,8 @@ +// Polyfills import 'es6-object-assign/auto'; import 'core-js/fn/array/includes'; +import 'intl'; // For Safari 9 + import React from 'react'; import ReactDOM from 'react-dom'; diff --git a/test/integration/how-tos.test.js b/test/integration/how-tos.test.js index 7a08385e814b2bfb971a6df3ad36535e9f5c8af8..5b9b76e4b74cb0163066010a3bd91da03a8992b3 100644 --- a/test/integration/how-tos.test.js +++ b/test/integration/how-tos.test.js @@ -27,7 +27,7 @@ describe('Working with the how-to library', () => { await loadUri(uri); await clickXpath('//button[@title="tryit"]'); await clickText('Costumes'); - await clickXpath('//*[@aria-label="How-to Library"]'); + await clickXpath('//*[@aria-label="Tutorials"]'); await clickText('Getting Started'); // Modal should close // Make sure YouTube video on first card appears await findByXpath('//div[contains(@class, "step-video")]');