diff --git a/.babelrc b/.babelrc index 8f910cdf7a6..aacea1f975e 100644 --- a/.babelrc +++ b/.babelrc @@ -1,10 +1,13 @@ { "plugins": [ - "syntax-dynamic-import", - "transform-async-to-generator", - "transform-object-rest-spread", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-transform-async-to-generator", + "@babel/plugin-proposal-object-rest-spread", ["react-intl", { "messagesDir": "./translations/messages/" }]], - "presets": [["env", {"targets": {"browsers": ["last 3 versions", "Safari >= 8", "iOS >= 8"]}}], "react"] + "presets": [ + ["@babel/preset-env", {"targets": {"browsers": ["last 3 versions", "Safari >= 8", "iOS >= 8"]}}], + "@babel/preset-react" + ] } diff --git a/.gitattributes b/.gitattributes index 84e89375a72..e81c38bb515 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,11 @@ # File types which we know are binary +# Treat SVG files as binary so that their contents don't change due to line +# endings. The contents of SVGs must not change from the way they're stored +# on assets.scratch.mit.edu so that MD5 calculations don't change. +*.svg binary + # Prefer LF for most file types *.frag text eol=lf *.htm text eol=lf diff --git a/.gitignore b/.gitignore index 0b8a0db40e6..de7972e5eed 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # NPM /node_modules npm-* +/package-lock.json # Testing /.nyc_output diff --git a/.npmignore b/.npmignore index 5d69158021a..61247064a40 100644 --- a/.npmignore +++ b/.npmignore @@ -8,9 +8,11 @@ npm-* # Testing /.nyc_output /coverage +/test # Build /.opt-in +/build # generated translation files /translations diff --git a/.travis.yml b/.travis.yml index 5805f6b6839..9290080d2f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,21 +29,13 @@ before_deploy: export BEFORE_DEPLOY_RAN=true fi deploy: -- provider: script - on: - all_branches: true - skip_cleanup: true - script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -- provider: script - on: - all_branches: true - script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git - provider: npm on: branch: - master - develop - smoke + condition: $TRAVIS_EVENT_TYPE != cron skip_cleanup: true email: $NPM_EMAIL api_key: $NPM_TOKEN @@ -53,11 +45,29 @@ deploy: branch: - master - $PREVIEW_BRANCH + condition: $TRAVIS_EVENT_TYPE != cron access_key_id: $AWS_ACCESS_KEY_ID secret_access_key: $AWS_SECRET_ACCESS_KEY bucket: $AWS_BUCKET_NAME acl: public_read skip_cleanup: true local_dir: build +- provider: script + on: + all_branches: true + condition: $TRAVIS_EVENT_TYPE != cron + skip_cleanup: true + script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git +- provider: script + on: + all_branches: true + condition: $TRAVIS_EVENT_TYPE != cron + script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git +- provider: script + on: + branch: develop + condition: $TRAVIS_EVENT_TYPE == cron + skip_cleanup: true + script: npm run i18n:src && npm run i18n:push after_deploy: - 'curl -X POST -H "Fastly-Key: $FASTLY_TOKEN" -H "Accept: application/json" https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all' diff --git a/README.md b/README.md index c036a170930..440a3a4b06a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ npm install https://github.com/LLK/scratch-gui.git ``` If you want to edit/play yourself: ```bash -git clone git@github.com:LLK/scratch-gui.git +git clone https://github.com/LLK/scratch-gui.git cd scratch-gui npm install ``` @@ -28,33 +28,100 @@ npm start ``` Then go to [http://localhost:8601/](http://localhost:8601/) - the playground outputs the default GUI component +## Developing alongside other Scratch repositories + +### Linking this code to another project's `node_modules/scratch-gui` + +#### Configuration + +If you wish to develop scratch-gui alongside other scratch repositories that depend on it, you may wish +to have the other repositories use your local scratch-gui build instead of fetching the current production +version of the scratch-gui that is found by default using `npm install`. + +To do this: +1. Make sure you have run `npm install` from this (scratch-gui) repository's top level +2. Make sure you have run `npm install` from the top level of each repository (such as scratch-www) that depends on scratch-gui +3. From this (scratch-gui) repository's top level, build the `dist` directory by running `BUILD_MODE=dist npm run build` +4. From this (scratch-gui) repository's top level, establish a link to this repository by running `npm link` +5. From the top level of each repository that depends on scratch-gui, run `npm link scratch-gui` +6. Build or run the repositories that depend on scratch-gui + +Instead of `BUILD_MODE=dist npm run build` you can also use `BUILD_MODE=dist npm run watch`, however this may be unreliable. + +#### Oh no! It didn't work! +* Follow the recipe above step by step and don't change the order. It is especially important to run npm first because installing after the linking will reset the linking. +* Make sure the repositories are siblings on your machine's file tree. +* If you have multiple Terminal tabs or windows open for the different Scratch repositories, make sure to use the same node version in all of them. +* In the worst case unlink the repositories by running `npm unlink` in both, and start over. + ## Testing -NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64. +### Documentation + +You may want to review the documentation for [Jest](https://facebook.github.io/jest/docs/en/api.html) and [Enzyme](http://airbnb.io/enzyme/docs/api/) as you write your tests. -Run linter, unit tests, build, and integration tests. +See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. + +### Running tests + +*NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64.* + +Before running any test, make sure you have run `npm install` from this (scratch-gui) repository's top level. + +#### Main testing command + +To run linter, unit tests, build, and integration tests, all at once: ```bash npm test ``` -Run unit tests in isolation. +#### Running unit tests + +To run unit tests in isolation: ```bash -npm run unit-test +npm run test:unit ``` -Run unit tests in watch mode (watches for code changes and continuously runs tests). See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. +To run unit tests in watch mode (watches for code changes and continuously runs tests): ```bash -npm run unit-test -- --watch +npm run test:unit -- --watch ``` -Run integration tests in isolation. +You can run a single file of integration tests (in this example, the `button` tests): + ```bash -npm run integration-test +$(npm bin)/jest --runInBand test/unit/components/button.test.jsx ``` -You may want to review the documentation for [Jest](https://facebook.github.io/jest/docs/en/api.html) and [Enzyme](http://airbnb.io/enzyme/docs/api/) as you write your tests. +#### Running integration tests -## Publishing to GitHub Pages +Integration tests use a headless browser to manipulate the actual html and javascript that the repo +produces. You will not see this activity (though you can hear it when sounds are played!). +Note that integration tests require you to first create a build that can be loaded in a browser: + +```bash +npm run build +``` + +Then, you can run all integration tests: + +```bash +npm run test:integration +``` + +Or, you can run a single file of integration tests (in this example, the `backpack` tests): + +```bash +$(npm bin)/jest --runInBand test/integration/backpack.test.js +``` + +If you want to watch the browser as it runs the test, rather than running headless, use: + +```bash +USE_HEADLESS=no $(npm bin)/jest --runInBand test/integration/backpack.test.js +``` + +## Publishing to GitHub Pages You can publish the GUI to github.io so that others on the Internet can view it. [Read the wiki for a step-by-step guide.](https://github.com/LLK/scratch-gui/wiki/Publishing-to-GitHub-Pages) diff --git a/package.json b/package.json index 1b589d22840..fa8bb61f44a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist", "deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"", "prune": "./prune-gh-pages.sh", - "i18n:src": "babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/", + "i18n:push": "tx-push-src scratch-editor interface translations/en.json", + "i18n:src": "rimraf ./translations/messages/src && babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/ && npm run i18n:push", "start": "webpack-dev-server", "test": "npm run test:lint && npm run test:unit && npm run build && npm run test:integration", "test:integration": "jest --runInBand test[\\\\/]integration", @@ -29,31 +30,34 @@ "react-dom": "^16.0.0" }, "devDependencies": { - "arraybuffer-loader": "^1.0.3", - "autoprefixer": "^8.1.0", - "babel-core": "^6.23.1", - "babel-eslint": "^8.0.1", - "babel-loader": "^7.1.0", - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-transform-async-to-generator": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.22.0", - "babel-preset-env": "^1.6.1", - "babel-preset-react": "^6.22.0", + "@babel/cli": "^7.1.2", + "@babel/core": "^7.1.2", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.1.0", + "@babel/preset-env": "^7.1.0", + "@babel/preset-react": "^7.0.0", + "arraybuffer-loader": "^1.0.6", + "autoprefixer": "^9.0.1", + "babel-core": "7.0.0-bridge.0", + "babel-eslint": "^10.0.1", + "babel-loader": "^8.0.4", "base64-loader": "1.0.0", - "bowser": "1.9.3", - "chromedriver": "2.40.0", + "bowser": "1.9.4", + "chromedriver": "2.44.1", "classnames": "2.2.6", + "computed-style-to-inline-style": "3.0.0", "copy-webpack-plugin": "^4.5.1", "core-js": "2.5.7", - "css-loader": "^0.28.11", - "enzyme": "^3.1.0", - "enzyme-adapter-react-16": "1.1.1", + "css-loader": "^1.0.0", + "enzyme": "^3.5.0", + "enzyme-adapter-react-16": "1.3.0", "es6-object-assign": "1.1.0", "eslint": "^5.0.1", "eslint-config-scratch": "^5.0.0", "eslint-plugin-import": "^2.8.0", - "eslint-plugin-react": "^7.5.1", - "file-loader": "1.1.11", + "eslint-plugin-react": "7.11.1", + "file-loader": "2.0.0", "get-float-time-domain-data": "0.1.0", "get-user-media-promise": "1.1.4", "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", @@ -61,6 +65,7 @@ "immutable": "3.8.2", "intl": "1.2.5", "jest": "^21.0.0", + "js-base64": "2.4.9", "keymirror": "0.1.1", "lodash.bindall": "4.4.0", "lodash.debounce": "4.0.8", @@ -68,52 +73,55 @@ "lodash.isequal": "4.5.0", "lodash.omit": "4.5.0", "lodash.pick": "4.4.0", + "lodash.throttle": "4.0.1", "minilog": "3.1.0", "mkdirp": "^0.5.1", - "postcss-import": "^11.0.0", - "postcss-loader": "^2.1.4", - "postcss-simple-vars": "^4.0.0", + "papaparse": "4.6.2", + "postcss-import": "^12.0.0", + "postcss-loader": "^3.0.0", + "postcss-simple-vars": "^5.0.1", "prop-types": "^15.5.10", + "query-string": "^5.1.1", "raf": "^3.4.0", "raw-loader": "^0.5.1", "react": "16.2.0", - "react-contextmenu": "2.9.2", + "react-contextmenu": "2.9.4", "react-dom": "16.2.0", "react-draggable": "3.0.5", "react-ga": "2.5.3", "react-intl": "2.4.0", - "react-modal": "3.4.4", - "react-popover": "0.5.7", + "react-modal": "3.6.1", + "react-popover": "0.5.10", "react-redux": "5.0.7", - "react-responsive": "4.1.0", - "react-style-proptype": "3.2.1", - "react-tabs": "2.2.2", + "react-responsive": "5.0.0", + "react-style-proptype": "3.2.2", + "react-tabs": "2.3.0", "react-test-renderer": "16.2.0", - "react-tooltip": "3.6.1", - "react-virtualized": "9.20.0", + "react-tooltip": "3.8.0", + "react-virtualized": "9.20.1", "redux": "3.7.2", "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", "rimraf": "^2.6.1", - "scratch-audio": "0.1.0-prerelease.20180625202813", - "scratch-blocks": "0.1.0-prerelease.1531482946", - "scratch-l10n": "3.0.20180712200642", - "scratch-paint": "0.2.0-prerelease.20180712195436", - "scratch-render": "0.1.0-prerelease.20180618173030", - "scratch-storage": "0.5.1", - "scratch-svg-renderer": "0.2.0-prerelease.20180712223402", - "scratch-vm": "0.1.0-prerelease.1531486395", + "scratch-audio": "0.1.0-prerelease.20190114210212", + "scratch-blocks": "0.1.0-prerelease.1547735159", + "scratch-l10n": "3.1.20190117191816", + "scratch-paint": "0.2.0-prerelease.20190114205252", + "scratch-render": "0.1.0-prerelease.20190116202853", + "scratch-storage": "1.2.2", + "scratch-svg-renderer": "0.2.0-prerelease.20190110205335", + "scratch-vm": "0.2.0-prerelease.20190116202234", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", - "style-loader": "^0.21.0", + "style-loader": "^0.23.0", "svg-to-image": "1.1.3", - "text-encoding": "0.6.4", + "text-encoding": "0.7.0", "to-style": "1.3.3", "uglifyjs-webpack-plugin": "^1.2.5", "wav-encoder": "1.3.0", "web-audio-test-api": "^0.5.2", "webpack": "^4.6.0", - "webpack-cli": "^2.0.15", + "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.3", "xhr": "2.5.0" }, diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 14b082cdfe7..4b05b2d2747 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -12,7 +12,10 @@ module.exports = { 'import/no-commonjs': 'error', 'import/no-amd': 'error', 'import/no-nodejs-modules': 'error', - 'react/jsx-no-literals': 'error' + 'react/jsx-no-literals': 'error', + 'no-confusing-arrow': ['error', { + 'allowParens': true + }] }, settings: { react: { diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css index 8f39946c128..14dbb5df031 100644 --- a/src/components/action-menu/action-menu.css +++ b/src/components/action-menu/action-menu.css @@ -1,4 +1,6 @@ @import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/z-index.css"; $main-button-size: 2.75rem; $more-button-size: 2.25rem; @@ -44,7 +46,7 @@ button::-moz-focus-inner { width: $main-button-size; height: $main-button-size; box-shadow: 0 0 0 4px $motion-transparent; - z-index: 20; /* TODO reorder layout to prevent z-index need */ + z-index: $z-index-add-button; transition: transform, box-shadow 0.5s; } @@ -58,6 +60,10 @@ button::-moz-focus-inner { height: calc($main-button-size - 1rem); } +[dir="rtl"] .main-icon { + transform: scaleX(-1); +} + .more-buttons-outer { /* Need to use two divs to set different overflow x/y @@ -71,6 +77,7 @@ button::-moz-focus-inner { border-top-right-radius: $more-button-size; width: $more-button-size; margin-left: calc(($main-button-size - $more-button-size) / 2); + margin-right: calc(($main-button-size - $more-button-size) / 2); position: absolute; bottom: calc($main-button-size); @@ -147,10 +154,10 @@ button::-moz-focus-inner { .tooltip { border: 1px solid hsla(0, 0%, 0%, .1) !important; - border-radius: .25rem !important; + border-radius: $form-radius !important; box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; - z-index: 100 !important; + z-index: $z-index-tooltip !important; } $arrow-size: 0.5rem; diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx index 2725c502b42..36cf5d4449f 100644 --- a/src/components/action-menu/action-menu.jsx +++ b/src/components/action-menu/action-menu.jsx @@ -24,6 +24,7 @@ class ActionMenu extends React.Component { isOpen: false, forceHide: false }; + this.mainTooltipId = `tooltip-${Math.random()}`; } componentDidMount () { // Touch start on the main button is caught to trigger open and not click @@ -77,6 +78,9 @@ class ActionMenu extends React.Component { return event => { ReactTooltip.hide(); if (fn) fn(event); + // Blur the button so it does not keep focus after being clicked + // This prevents keyboard events from triggering the button + this.buttonRef.blur(); this.setState({forceHide: true, isOpen: false}, () => { setTimeout(() => this.setState({forceHide: false})); }); @@ -101,11 +105,10 @@ class ActionMenu extends React.Component { img: mainImg, title: mainTitle, moreButtons, + tooltipPlace, onClick } = this.props; - const mainTooltipId = `tooltip-${Math.random()}`; - return (
@@ -142,7 +145,7 @@ class ActionMenu extends React.Component { fileAccept, fileChange, fileInput}, keyId) => { const isComingSoon = !handleClick; const hasFileInput = fileInput; - const tooltipId = title; + const tooltipId = `${this.mainTooltipId}-${title}`; return (
); @@ -198,7 +201,8 @@ ActionMenu.propTypes = { fileInput: PropTypes.func // Optional, only for file upload })), onClick: PropTypes.func.isRequired, - title: PropTypes.node.isRequired + title: PropTypes.node.isRequired, + tooltipPlace: PropTypes.string }; export default ActionMenu; diff --git a/src/components/alerts/alert.css b/src/components/alerts/alert.css new file mode 100644 index 00000000000..8e5d56296e6 --- /dev/null +++ b/src/components/alerts/alert.css @@ -0,0 +1,103 @@ +@import "../../css/units.css"; +@import "../../css/colors.css"; +@import "../../css/z-index.css"; + +.alert { + width: 100%; + display: flex; + flex-direction: row; + overflow: hidden; + justify-content: flex-start; + border-radius: $space; + padding-top: .875rem; + padding-bottom: .875rem; + padding-left: 1rem; + padding-right: 1rem; + margin-bottom: 7px; + min-height: 1.5rem; +} + +.alert.warn { + background: #FFF0DF; + border: 1px solid #FF8C1A; + box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25); +} + +.alert.success { + background: $extensions-light; + border: 1px solid $extensions-tertiary; + box-shadow: 0px 0px 0px 2px $extensions-light; +} + +.alert-spinner { + self-align: center; +} + +.icon-section { + min-width: 1.25rem; + min-height: 1.25rem; + display: flex; + padding-right: 1rem; +} + +.alert-icon { + vertical-align: middle; +} + +.alert-message { + color: #555; + font-weight: bold; + font-size: .8125rem; + line-height: .875rem; + display: flex; + align-items: center; + padding-right: .5rem; +} + +.alert-buttons { + display: flex; + flex-direction: row; +} + +.alert-close-button { + outline-style:none; +} + +.alert-close-button-container { + outline-style: none; + width: 30px; + height: 30px; + align-self: center; +} + +.alert-connection-button { + min-height: 2rem; + width: 6.5rem; + padding: 0.55rem 0.9rem; + border-radius: 0.35rem; + background: #FF8C1A; + color: white; + font-weight: 700; + font-size: 0.77rem; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + align-self: stretch; + outline-style:none; +} + +[dir="ltr"] .alert-connection-button { + margin-right: 13px; +} + +[dir="rtl"] .alert-connection-button { + margin-left: 13px; +} + +/* prevent last button in list from too much margin to edge of alert */ +.alert-buttons > :last-child { + margin-left: 0; + margin-right: 0; +} diff --git a/src/components/alerts/alert.jsx b/src/components/alerts/alert.jsx new file mode 100644 index 00000000000..db86bd4b8d6 --- /dev/null +++ b/src/components/alerts/alert.jsx @@ -0,0 +1,140 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import {FormattedMessage} from 'react-intl'; + +import Box from '../box/box.jsx'; +import CloseButton from '../close-button/close-button.jsx'; +import Spinner from '../spinner/spinner.jsx'; +import {AlertLevels} from '../../lib/alerts/index.jsx'; + +import styles from './alert.css'; + +const closeButtonColors = { + [AlertLevels.SUCCESS]: CloseButton.COLOR_GREEN, + [AlertLevels.WARN]: CloseButton.COLOR_ORANGE +}; + +const AlertComponent = ({ + content, + closeButton, + extensionName, + iconSpinner, + iconURL, + level, + showDownload, + showSaveNow, + onCloseAlert, + onDownload, + onSaveNow, + onReconnect, + showReconnect +}) => ( + + {/* TODO: implement Rtl handling */} + {(iconSpinner || iconURL) && ( +
+ {iconSpinner && ( + + )} + {iconURL && ( + + )} +
+ )} +
+ {extensionName ? ( + + ) : content} +
+
+ {showSaveNow && ( + + )} + {showDownload && ( + + )} + {showReconnect && ( + + )} + {closeButton && ( + + + + )} +
+
+); + +AlertComponent.propTypes = { + closeButton: PropTypes.bool, + content: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), + extensionName: PropTypes.string, + iconSpinner: PropTypes.bool, + iconURL: PropTypes.string, + level: PropTypes.string, + onCloseAlert: PropTypes.func.isRequired, + onDownload: PropTypes.func, + onReconnect: PropTypes.func, + onSaveNow: PropTypes.func, + showDownload: PropTypes.func, + showReconnect: PropTypes.bool, + showSaveNow: PropTypes.bool +}; + +AlertComponent.defaultProps = { + level: AlertLevels.WARN +}; + +export default AlertComponent; diff --git a/src/components/alerts/alerts.css b/src/components/alerts/alerts.css new file mode 100644 index 00000000000..3b7cf5221a8 --- /dev/null +++ b/src/components/alerts/alerts.css @@ -0,0 +1,4 @@ +.alerts-inner-container { + min-width: 200px; + max-width: 548px; +} diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx new file mode 100644 index 00000000000..50acd7ce161 --- /dev/null +++ b/src/components/alerts/alerts.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Box from '../box/box.jsx'; +import Alert from '../../containers/alert.jsx'; + +import styles from './alerts.css'; + +const AlertsComponent = ({ + alertsList, + className, + onCloseAlert +}) => ( + + + {alertsList.map((a, index) => ( + + ))} + + +); + +AlertsComponent.propTypes = { + alertsList: PropTypes.arrayOf(PropTypes.object), + className: PropTypes.string, + onCloseAlert: PropTypes.func +}; + +export default AlertsComponent; diff --git a/src/components/alerts/inline-message.css b/src/components/alerts/inline-message.css new file mode 100644 index 00000000000..b5169b41e68 --- /dev/null +++ b/src/components/alerts/inline-message.css @@ -0,0 +1,27 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.inline-message { + color: $ui-white; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + display: flex; + justify-content: end; + align-items: center; + font-size: .8125rem; +} + +.success { + color: $ui-white-dim; +} + +.info { + color: $ui-white; +} + +.warn { + color: $error-light; +} + +.spinner { + margin-right: $space; +} diff --git a/src/components/alerts/inline-message.jsx b/src/components/alerts/inline-message.jsx new file mode 100644 index 00000000000..981e1984a1c --- /dev/null +++ b/src/components/alerts/inline-message.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import Spinner from '../spinner/spinner.jsx'; +import {AlertLevels} from '../../lib/alerts/index.jsx'; + +import styles from './inline-message.css'; + +const InlineMessageComponent = ({ + content, + iconSpinner, + level +}) => ( +
+ {/* TODO: implement Rtl handling */} + {iconSpinner && ( + + )} + {content} +
+); + +InlineMessageComponent.propTypes = { + content: PropTypes.element, + iconSpinner: PropTypes.bool, + level: PropTypes.string +}; + +InlineMessageComponent.defaultProps = { + level: AlertLevels.INFO +}; + +export default InlineMessageComponent; diff --git a/src/components/asset-panel/asset-panel.css b/src/components/asset-panel/asset-panel.css index 9a999057b57..bfd578909fd 100644 --- a/src/components/asset-panel/asset-panel.css +++ b/src/components/asset-panel/asset-panel.css @@ -5,16 +5,32 @@ display: flex; flex-grow: 1; border: 1px solid $ui-black-transparent; - border-top-right-radius: $space; background: white; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 0.85rem; } +[dir="ltr"] .wrapper { + border-top-right-radius: $space; + border-bottom-right-radius: $space; +} + +[dir="rtl"] .wrapper { + border-top-left-radius: $space; + border-bottom-left-radius: $space; +} + .detail-area { display: flex; flex-grow: 1; flex-shrink: 1; - border-left: 1px solid $ui-black-transparent; overflow-y: auto; } + +[dir="ltr"] .detail-area { + border-left: 1px solid $ui-black-transparent; +} + +[dir="rtl"] .detail-area { + border-right: 1px solid $ui-black-transparent; +} diff --git a/src/components/asset-panel/icon--sound-rtl.svg b/src/components/asset-panel/icon--sound-rtl.svg new file mode 100644 index 00000000000..adf85017394 --- /dev/null +++ b/src/components/asset-panel/icon--sound-rtl.svg @@ -0,0 +1,20 @@ + + + + Artboard + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index 3dc4e803d9e..ad24ed954cc 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -32,6 +32,7 @@ $fade-out-distance: 100px; position: absolute; bottom: 0; left: 0; + right:0; background: linear-gradient(rgba(232,237,241, 0),rgba(232,237,241, 1)); height: $fade-out-distance; width: 100%; @@ -49,11 +50,13 @@ $fade-out-distance: 100px; overflow-y: scroll; display: flex; flex-direction: column; + /* Make sure there is room to scroll beyond the last tile */ + padding-bottom: 70px; } .list-item { width: 5rem; - min-height: 5rem; + height: 5rem; margin: 0.5rem auto; } @@ -70,5 +73,5 @@ $fade-out-distance: 100px; .list-item.placeholder { background: black; - filter: opacity(15%) brightness(20%); + filter: opacity(15%) brightness(0%); } diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index fc509cc71a7..3a8df1475df 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -15,6 +15,7 @@ const Selector = props => { buttons, containerRef, dragType, + isRtl, items, selectedItemIndex, draggingIndex, @@ -41,6 +42,7 @@ const Selector = props => { img={img} moreButtons={moreButtons} title={title} + tooltipPlace={isRtl ? 'left' : 'right'} onClick={onClick} /> @@ -62,7 +64,7 @@ const Selector = props => { onRemoveSortable={onRemoveSortable} > div { + /* Need to set the background to get blend-mode below to work */ + background: $ui-primary; +} + +.backpack-item img { mix-blend-mode: multiply; /* Make white transparent for thumnbnails */ } + +.more { + background: $motion-primary; + color: $ui-white; + border: none; + outline: none; + font-weight: bold; + border-radius: 0.5rem; + font-size: 0.85rem; + padding: 0.5rem; + margin: 0.5rem; + cursor: pointer; +} diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx index 0f7b0b89f7e..f5d1a0ae280 100644 --- a/src/components/backpack/backpack.jsx +++ b/src/components/backpack/backpack.jsx @@ -10,14 +10,28 @@ import styles from './backpack.css'; // TODO make sprite selector item not require onClick const noop = () => {}; -const dragTypeMap = { +const dragTypeMap = { // Keys correspond with the backpack-server item types costume: DragConstants.BACKPACK_COSTUME, sound: DragConstants.BACKPACK_SOUND, - code: DragConstants.BACKPACK_CODE, + script: DragConstants.BACKPACK_CODE, sprite: DragConstants.BACKPACK_SPRITE }; -const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, onToggle, onDelete}) => ( +const Backpack = ({ + blockDragOver, + containerRef, + contents, + dragOver, + error, + expanded, + loading, + showMore, + onToggle, + onDelete, + onMouseEnter, + onMouseLeave, + onMore +}) => (
{expanded ? (
{error ? (
@@ -66,11 +84,7 @@ const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, on
) : ( contents.length > 0 ? ( -
+
{contents.map(item => ( ))} + {showMore && ( + + )}
) : (
@@ -104,6 +130,8 @@ const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, on ); Backpack.propTypes = { + blockDragOver: PropTypes.bool, + containerRef: PropTypes.func, contents: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, thumbnailUrl: PropTypes.string, @@ -111,19 +139,25 @@ Backpack.propTypes = { name: PropTypes.string })), dragOver: PropTypes.bool, - dropAreaRef: PropTypes.func, error: PropTypes.bool, expanded: PropTypes.bool, loading: PropTypes.bool, onDelete: PropTypes.func, - onToggle: PropTypes.func + onMore: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onToggle: PropTypes.func, + showMore: PropTypes.bool }; Backpack.defaultProps = { + blockDragOver: false, contents: [], dragOver: false, expanded: false, loading: false, + showMore: false, + onMore: null, onToggle: null }; diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css index e02486ce0fe..fd66c68bd76 100644 --- a/src/components/blocks/blocks.css +++ b/src/components/blocks/blocks.css @@ -1,6 +1,22 @@ @import "../../css/units.css"; @import "../../css/colors.css"; +.blocks { + height: 100%; +} + +.drag-over:after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.75; + background-color: $drop-highlight; + transition: all 0.25s ease; +} + .blocks :global(.injectionDiv){ position: absolute; top: 0; @@ -12,6 +28,13 @@ border-bottom-right-radius: $space; } +[dir="rtl"] .blocks :global(.injectionDiv) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: $space; + border-bottom-left-radius: $space; +} + .blocks :global(.blocklyMainBackground) { stroke: none; } @@ -31,6 +54,11 @@ -ms-overflow-style: none; } +[dir="rtl"] .blocks :global(.blocklyToolboxDiv) { + border-right: none; + border-left: 1px solid $ui-black-transparent; +} + .blocks :global(.blocklyToolboxDiv::-webkit-scrollbar) { display: none; } @@ -40,6 +68,12 @@ box-sizing: content-box; } +[dir="rtl"] .blocks :global(.blocklyFlyout) { + border-right: none; + border-left: 1px solid $ui-black-transparent; +} + + .blocks :global(.blocklyBlockDragSurface) { /* Fix an issue where the drag surface was preventing hover events for sharing blocks. diff --git a/src/components/blocks/blocks.jsx b/src/components/blocks/blocks.jsx index 683da23c1f0..a812b039fb2 100644 --- a/src/components/blocks/blocks.jsx +++ b/src/components/blocks/blocks.jsx @@ -1,22 +1,27 @@ import PropTypes from 'prop-types'; +import classNames from 'classnames'; import React from 'react'; import Box from '../box/box.jsx'; import styles from './blocks.css'; const BlocksComponent = props => { const { - componentRef, + containerRef, + dragOver, ...componentProps } = props; return ( ); }; BlocksComponent.propTypes = { - componentRef: PropTypes.func + containerRef: PropTypes.func, + dragOver: PropTypes.bool }; export default BlocksComponent; diff --git a/src/components/browser-modal/browser-modal.css b/src/components/browser-modal/browser-modal.css index aa8840c4cc8..2f4145bf665 100644 --- a/src/components/browser-modal/browser-modal.css +++ b/src/components/browser-modal/browser-modal.css @@ -27,11 +27,21 @@ } .illustration { + display: flex; + align-items: center; + justify-content: center; width: 100%; - height: 208px; - background-color: $motion-primary; - background-image: url('./unsupported.png'); - background-size: cover; + height: 100px; + background-color: $control-primary; +} + +[dir="rtl"] .illustration { + transform: scaleX(-1); +} + +.illustration img { + height: 80%; + width: auto; } .body { diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx index bb5c82fcffa..0419e9122d1 100644 --- a/src/components/browser-modal/browser-modal.jsx +++ b/src/components/browser-modal/browser-modal.jsx @@ -5,80 +5,109 @@ import Box from '../box/box.jsx'; import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; import styles from './browser-modal.css'; +import unhappyBrowser from './unsupported-browser.svg'; const messages = defineMessages({ label: { id: 'gui.unsupportedBrowser.label', defaultMessage: 'Browser is not supported', description: '' + }, + error: { + id: 'gui.unsupportedBrowser.errorLabel', + defaultMessage: 'An Error Occurred', + description: 'Heading shown when there is an unhandled exception in an unsupported browser' } }); -const BrowserModal = ({intl, ...props}) => ( - - +const BrowserModal = ({intl, ...props}) => { + const label = props.error ? messages.error : messages.label; + return ( + +
+ + + - -

- -

-

- { /* eslint-disable max-len */ } - - { /* eslint-enable max-len */ } -

+ +

+ +

+

+ { /* eslint-disable max-len */ } + { + props.error ? : + } + { /* eslint-enable max-len */ } +

- - + + - -
- - - - ) - }} - /> + +
+ + + + ) + }} + /> +
+
-
- -); + + ); +}; BrowserModal.propTypes = { + error: PropTypes.bool, intl: intlShape.isRequired, + isRtl: PropTypes.bool, onBack: PropTypes.func.isRequired }; -export default injectIntl(BrowserModal); +BrowserModal.defaultProps = { + error: false +}; + +const WrappedBrowserModal = injectIntl(BrowserModal); + +WrappedBrowserModal.setAppElement = ReactModal.setAppElement; + +export default WrappedBrowserModal; diff --git a/src/components/browser-modal/unsupported-browser.svg b/src/components/browser-modal/unsupported-browser.svg new file mode 100644 index 00000000000..7749f7539b3 --- /dev/null +++ b/src/components/browser-modal/unsupported-browser.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/button/button.css b/src/components/button/button.css index 9ce746c5214..30f000a7a36 100644 --- a/src/components/button/button.css +++ b/src/components/button/button.css @@ -1,6 +1,8 @@ +@import "../../css/units.css"; + .outlined-button { cursor: pointer; - border-radius: .25rem; + border-radius: $form-radius; font-weight: bold; display: flex; flex-direction: row; @@ -11,10 +13,17 @@ } .icon { - margin-right: .5rem; height: 1.5rem; } +[dir="ltr"] .icon { + margin-right: .5rem; +} + +[dir="rtl"] .icon { + margin-left: .5rem; +} + .content { white-space: nowrap; } diff --git a/src/components/camera-modal/camera-modal.css b/src/components/camera-modal/camera-modal.css index 4ea962fbafd..7522fbb2ec2 100644 --- a/src/components/camera-modal/camera-modal.css +++ b/src/components/camera-modal/camera-modal.css @@ -135,6 +135,10 @@ $main-button-size: 2.75rem; color: $ui-white; } +[dir="rtl"] .retake-button img { + transform: scaleX(-1); +} + @keyframes flash { 0% { opacity: 1; } 100% { opacity: 0; } diff --git a/src/components/camera-modal/camera-modal.jsx b/src/components/camera-modal/camera-modal.jsx index f115a6f8a75..8c499c36249 100644 --- a/src/components/camera-modal/camera-modal.jsx +++ b/src/components/camera-modal/camera-modal.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import Box from '../box/box.jsx'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import styles from './camera-modal.css'; import backIcon from './icon--back.svg'; import cameraIcon from '../action-menu/icon--camera.svg'; @@ -79,7 +79,7 @@ const CameraModal = ({intl, ...props}) => ( {props.capture ?