同步到官方#20191231

This commit is contained in:
chengyu 2019-12-31 17:48:57 +08:00
parent 95172bc71f
commit ed79512477
1027 changed files with 103361 additions and 0 deletions

13
.babelrc Normal file
View File

@ -0,0 +1,13 @@
{
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-async-to-generator",
"@babel/plugin-proposal-object-rest-spread",
["react-intl", {
"messagesDir": "./translations/messages/"
}]],
"presets": [
["@babel/preset-env", {"targets": {"browsers": ["last 3 versions", "Safari >= 8", "iOS >= 8"]}}],
"@babel/preset-react"
]
}

3
.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
last 3 versions
Safari >= 8
iOS >= 8

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 4
trim_trailing_whitespace = true
[*.{js,html}]
indent_style = space

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/*
build/*
dist/*

3
.eslintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: ['scratch', 'scratch/node']
};

39
.gitattributes vendored Normal file
View File

@ -0,0 +1,39 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly specify line endings for as many files as possible.
# People who (for example) rsync between Windows and Linux need this.
# 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
*.html text eol=lf
*.iml text eol=lf
*.js text eol=lf
*.js.map text eol=lf
*.json text eol=lf
*.jsx text eol=lf
*.md text eol=lf
*.vert text eol=lf
*.xml text eol=lf
*.yml text eol=lf
# Prefer LF for these files
.editorconfig text eol=lf
.eslintrc text eol=lf
.gitattributes text eol=lf
.gitignore text eol=lf
.gitmodules text eol=lf
LICENSE text eol=lf
Makefile text eol=lf
README text eol=lf
TRADEMARK text eol=lf
# Use CRLF for Windows-specific file types

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Mac OS
.DS_Store
# NPM
/node_modules
npm-*
# Testing
/.nyc_output
/coverage
# Build
/build
/dist
/.opt-in
# generated translation files
/translations
/locale

21
.npmignore Normal file
View File

@ -0,0 +1,21 @@
# Mac OS
.DS_Store
# NPM
/node_modules
npm-*
# Double copies of all the static assets and tutorial gifs
/src
# Testing
/.nyc_output
/coverage
/test
# Build
/.opt-in
/build
# generated translation files
/translations

66
.travis.yml Normal file
View File

@ -0,0 +1,66 @@
language: node_js
sudo: required
dist: xenial
addons:
chrome: stable
node_js:
- 8
env:
global:
- CHROMEDRIVER_VERSION=LATEST
- NODE_ENV=production
- NODE_OPTIONS=--max-old-space-size=7250
- NPM_TAG=latest
- RELEASE_VERSION="0.1.0-prerelease.$(date +'%Y%m%d%H%M%S')"
cache:
directories:
- node_modules
install:
- npm --production=false install
script:
- npm test
before_deploy:
- >
if [ -z "$BEFORE_DEPLOY_RAN" ]; then
npm --no-git-tag-version version $RELEASE_VERSION
if [ "$TRAVIS_BRANCH" == "master" ]; then export NPM_TAG=stable; fi
if [[ "$TRAVIS_BRANCH" == hotfix/* ]]; then export NPM_TAG=hotfix; fi # double brackets are important for matching the wildcard
git config --global user.email $(git log --pretty=format:"%ae" -n1)
git config --global user.name $(git log --pretty=format:"%an" -n1)
export BEFORE_DEPLOY_RAN=true
fi
deploy:
- provider: npm
on:
branch:
- master
- develop
- hotfix/*
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
email: $NPM_EMAIL
api_key: $NPM_TOKEN
tag: $NPM_TAG
- provider: script
on:
branch:
- master
- develop
- smoke
- hotfix/*
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
script: if npm info scratch-gui | grep -q $RELEASE_VERSION; then git tag $RELEASE_VERSION && git push https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git $RELEASE_VERSION; fi
- provider: script
on:
all_branches: true
condition: $TRAVIS_EVENT_TYPE != cron && ! $TRAVIS_BRANCH =~ ^greenkeeper/
tags: false # Don't push tags to gh-pages
skip_cleanup: true
script: npm run deploy -- -x -e $TRAVIS_BRANCH -r 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

12
LICENSE Normal file
View File

@ -0,0 +1,12 @@
Copyright (c) 2016, Massachusetts Institute of Technology
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

198
README.md Normal file
View File

@ -0,0 +1,198 @@
# scratch-gui
#### Scratch GUI is a set of React components that comprise the interface for creating and running Scratch 3.0 projects
[![Build Status](https://travis-ci.com/LLK/scratch-gui.svg?token=Yfq2ryN1BwaxDME69Lnc&branch=master)](https://travis-ci.com/LLK/scratch-gui)
[![Greenkeeper badge](https://badges.greenkeeper.io/LLK/scratch-gui.svg)](https://greenkeeper.io/)
## Installation
This requires you to have Git and Node.js installed.
In your own node environment/application:
```bash
npm install https://github.com/LLK/scratch-gui.git
```
If you want to edit/play yourself:
```bash
git clone https://github.com/LLK/scratch-gui.git
cd scratch-gui
npm install
```
## Getting started
Running the project requires Node.js to be installed.
## Running
Open a Command Prompt or Terminal in the repository and run:
```bash
npm start
```
Then go to [http://localhost:8601/](http://localhost:8601/) - the playground outputs the default GUI component
## Developing alongside other Scratch repositories
### Getting another repo to point to this code
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`.
Here's how to link your local `scratch-gui` code to another project's `node_modules/scratch-gui`.
#### Configuration
1. In your local `scratch-gui` repository's top level:
1. Make sure you have run `npm install`
2. Build the `dist` directory by running `BUILD_MODE=dist npm run build`
3. Establish a link to this repository by running `npm link`
2. From the top level of each repository (such as `scratch-www`) that depends on `scratch-gui`:
1. Make sure you have run `npm install`
2. Run `npm link scratch-gui`
3. Build or run the repositoriy
#### Using `npm run watch`
Instead of `BUILD_MODE=dist npm run build`, you can use `BUILD_MODE=dist npm run watch` instead. This will watch for changes to your `scratch-gui` code, and automatically rebuild when there are changes. Sometimes this has been unreliable; if you are having problems, try going back to `BUILD_MODE=dist npm run build` until you resolve them.
#### Oh no! It didn't work!
If you can't get linking to work right, try:
* Follow the recipe above step by step and don't change the order. It is especially important to run `npm install` _before_ `npm link`, because installing after the linking will reset the linking.
* Make sure the repositories are siblings on your machine's file tree, like `.../.../MY_SCRATCH_DEV_DIRECTORY/scratch-gui/` and `.../.../MY_SCRATCH_DEV_DIRECTORY/scratch-www/`.
* Consistent node.js version: 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.
* If nothing else works, unlink the repositories by running `npm unlink` in both, and start over.
## Testing
### 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.
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
```
#### Running unit tests
To run unit tests in isolation:
```bash
npm run test:unit
```
To run unit tests in watch mode (watches for code changes and continuously runs tests):
```bash
npm run test:unit -- --watch
```
You can run a single file of integration tests (in this example, the `button` tests):
```bash
$(npm bin)/jest --runInBand test/unit/components/button.test.jsx
```
#### Running integration tests
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
```
## Troubleshooting
### Ignoring optional dependencies
When running `npm install`, you can get warnings about optionsl dependencies:
```
npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.2.7
```
You can suppress them by adding the `no-optional` switch:
```
npm install --no-optional
```
Further reading: [Stack Overflow](https://stackoverflow.com/questions/36725181/not-compatible-with-your-operating-system-or-architecture-fsevents1-0-11)
### Resolving dependencies
When installing for the first time, you can get warnings which need to be resolved:
```
npm WARN eslint-config-scratch@5.0.0 requires a peer of babel-eslint@^8.0.1 but none was installed.
npm WARN eslint-config-scratch@5.0.0 requires a peer of eslint@^4.0 but none was installed.
npm WARN scratch-paint@0.2.0-prerelease.20190318170811 requires a peer of react-intl-redux@^0.7 but none was installed.
npm WARN scratch-paint@0.2.0-prerelease.20190318170811 requires a peer of react-responsive@^4 but none was installed.
```
You can check which versions are available:
```
npm view react-intl-redux@0.* version
```
You will neet do install the required version:
```
npm install --no-optional --save-dev react-intl-redux@^0.7
```
The dependency itself might have more missing dependencies, which will show up like this:
```
user@machine:~/sources/scratch/scratch-gui (491-translatable-library-objects)$ npm install --no-optional --save-dev react-intl-redux@^0.7
scratch-gui@0.1.0 /media/cuideigin/Linux/sources/scratch/scratch-gui
├── react-intl-redux@0.7.0
└── UNMET PEER DEPENDENCY react-responsive@5.0.0
```
You will need to install those as well:
```
npm install --no-optional --save-dev react-responsive@^5.0.0
```
Further reading: [Stack Overflow](https://stackoverflow.com/questions/46602286/npm-requires-a-peer-of-but-all-peers-are-in-package-json-and-node-modules)
## 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)
## Donate
We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, design, community, and resource development efforts. Donations of any size are appreciated. Thank you!

1
TRADEMARK Normal file
View File

@ -0,0 +1 @@
The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT). Marks may not be used to endorse or promote products derived from this software without specific prior written permission.

14909
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

145
package.json Normal file
View File

@ -0,0 +1,145 @@
{
"name": "scratch-gui",
"version": "0.1.0",
"description": "GraphicaL User Interface for creating and running Scratch 3.0 projects",
"main": "./dist/scratch-gui.js",
"scripts": {
"build": "npm run clean && webpack --progress --colors --bail",
"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: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",
"test:lint": "eslint . --ext .js,.jsx",
"test:unit": "jest test[\\\\/]unit",
"test:smoke": "jest --runInBand test[\\\\/]smoke",
"watch": "webpack --progress --colors --watch"
},
"author": "Massachusetts Institute of Technology",
"license": "BSD-3-Clause",
"homepage": "https://github.com/LLK/scratch-gui#readme",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/LLK/scratch-gui.git"
},
"peerDependencies": {
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
"devDependencies": {
"@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.4",
"chromedriver": "78.0.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": "^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-import-resolver-webpack": "^0.11.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jest": "^22.14.1",
"eslint-plugin-react": "^7.12.4",
"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",
"html-webpack-plugin": "^3.2.0",
"immutable": "3.8.2",
"intl": "1.2.5",
"jest": "^21.0.0",
"jest-junit": "^7.0.0",
"js-base64": "2.4.9",
"keymirror": "0.1.1",
"lodash.bindall": "4.4.0",
"lodash.debounce": "4.0.8",
"lodash.defaultsdeep": "4.6.0",
"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",
"omggif": "1.0.9",
"papaparse": "5.1.1",
"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.4",
"react-dom": "16.2.0",
"react-draggable": "3.0.5",
"react-ga": "2.5.3",
"react-intl": "2.9.0",
"react-modal": "3.9.1",
"react-popover": "0.5.10",
"react-redux": "5.0.7",
"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.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.20190925183642",
"scratch-l10n": "3.7.20191219145348",
"scratch-blocks": "0.1.0-prerelease.1576850350",
"scratch-paint": "0.2.0-prerelease.20191217213717",
"scratch-render": "0.1.0-prerelease.20191217212645",
"scratch-storage": "1.3.2",
"scratch-svg-renderer": "0.2.0-prerelease.20191217211338",
"scratch-vm": "0.2.0-prerelease.20191227164934",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
"svg-to-image": "1.1.3",
"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": "^3.1.0",
"webpack-dev-server": "^3.1.3",
"xhr": "2.5.0"
},
"jest": {
"setupFiles": [
"raf/polyfill",
"<rootDir>/test/helpers/enzyme-setup.js"
],
"testPathIgnorePatterns": [
"src/test.js"
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
}
}
}

64
prune-gh-pages.sh Normal file
View File

@ -0,0 +1,64 @@
#!/bin/bash
# gh-pages cleanup script: Switches to gh-pages branch, and removes all
# directories that aren't listed as remote branches
function deslash () {
# Recursively build a string of a directory's parents. E.g.,
# deslashed "feature/test/branch" returns feature/test feature
deslashed=$(dirname $1)
if [[ $deslashed =~ .*/.* ]]
then
echo $deslashed $(deslash $deslashed)
else
echo $deslashed
fi
}
repository=origin
if [[ $1 != "" ]]
then
repository=$1
fi
# Cache current branch
current=$(git rev-parse --abbrev-ref HEAD)
# Checkout most recent gh-pages
git fetch --force $repository gh-pages:gh-pages
git checkout gh-pages
git clean -fdx
# Make an array of directories to not delete, from the list of remote branches
branches=$(git ls-remote --refs --quiet $repository | awk '{print $2}' | sed -e 's/refs\/heads\///')
# Add parent directories of branches to the exclusion list (e.g. greenkeeper/)
for branch in $branches; do
if [[ $branch =~ .*/.* ]]; then
branches+=" $(deslash $branch)"
fi
done
# Dedupe all the greenkeepers (or other duplicate parent directories)
branches=$(echo "${branches[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')
# Remove all directories that don't have corresponding branches
# It would be nice if we could exclude everything in .gitignore, but we're
# not on the branch with the .gitignore anymore... so we can't.
find . -type d \
\( \
-path ./.git -o \
-path ./node_modules \
$(printf " -o -path ./%s" $branches) \
\) -prune \
-o -mindepth 1 -type d \
-exec rm -rfv {} \;
# Push
git add -u
git commit -m "Remove stale directories"
git push $repository gh-pages
# Return to where we were
git checkout -f $current
exit

31
src/.eslintrc.js Normal file
View File

@ -0,0 +1,31 @@
const path = require('path');
module.exports = {
root: true,
extends: ['scratch', 'scratch/es6', 'scratch/react', 'plugin:import/errors'],
env: {
browser: true
},
globals: {
process: true
},
rules: {
'import/no-mutable-exports': 'error',
'import/no-commonjs': 'error',
'import/no-amd': 'error',
'import/no-nodejs-modules': 'error',
'react/jsx-no-literals': 'error',
'no-confusing-arrow': ['error', {
'allowParens': true
}]
},
settings: {
react: {
version: '16.2' // Prevent 16.3 lifecycle method errors
},
'import/resolver': {
webpack: {
config: path.resolve(__dirname, '../webpack.config.js')
}
}
}
};

View File

@ -0,0 +1,200 @@
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/z-index.css";
$main-button-size: 2.75rem;
$more-button-size: 2.25rem;
.menu-container {
display: flex;
flex-direction: column-reverse;
transition: 0.2s;
position: relative;
}
.button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: $motion-primary;
outline: none;
border: none;
transition: background-color 0.2s;
}
button::-moz-focus-inner {
border: 0;
}
.button:hover {
background: $extensions-primary;
}
.button:active {
padding: inherit;
}
.button.coming-soon:hover {
background: $data-primary;
}
.main-button {
border-radius: 100%;
width: $main-button-size;
height: $main-button-size;
box-shadow: 0 0 0 4px $motion-transparent;
z-index: $z-index-add-button;
transition: transform, box-shadow 0.5s;
}
.main-button:hover {
transform: scale(1.1);
box-shadow: 0 0 0 6px $motion-transparent;
}
.main-icon {
width: calc($main-button-size - 1rem);
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
which is needed to get animation to look right while
allowing the tooltips to be visible.
*/
overflow-y: hidden;
background: $motion-tertiary;
border-top-left-radius: $more-button-size;
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);
margin-bottom: calc($main-button-size / -2);
padding-bottom: calc($main-button-size / 2);
}
.more-buttons {
max-height: 0;
transition: max-height 1s;
overflow-x: visible;
display: flex;
flex-direction: column;
z-index: 10; /* @todo justify */
}
.file-input {
display: none;
}
.expanded .more-buttons {
max-height: 1000px; /* Arbitrary, needs to be a value in order for animation to run */
}
.force-hidden .more-buttons {
display: none; /* This property does not animate */
}
.more-buttons:first-child { /* Round off top button */
border-top-right-radius: $more-button-size;
border-top-left-radius: $more-button-size;
}
.more-button {
width: $more-button-size;
height: $more-button-size;
background: $motion-tertiary;
}
.more-icon {
width: calc($more-button-size - 1rem);
height: calc($more-button-size - 1rem);
}
.coming-soon .more-icon {
opacity: 0.5;
}
/*
@todo needs to be refactored with coming soon tooltip overrides.
The "!important"s are for the same reason as with coming soon, the library
is not very easy to style.
*/
.tooltip {
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;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
}
.tooltip:after {
background-color: $extensions-primary;
}
.coming-soon-tooltip {
background-color: $data-primary !important;
}
.coming-soon-tooltip:after {
background-color: $data-primary !important;
}
.tooltip {
border: 1px solid hsla(0, 0%, 0%, .1) !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: $z-index-tooltip !important;
}
$arrow-size: 0.5rem;
$arrow-inset: -0.25rem;
$arrow-rounding: 0.125rem;
.tooltip:after {
content: "";
border-top: 1px solid hsla(0, 0%, 0%, .1) !important;
border-left: 0 !important;
border-bottom: 0 !important;
border-right: 1px solid hsla(0, 0%, 0%, .1) !important;
border-radius: $arrow-rounding;
height: $arrow-size !important;
width: $arrow-size !important;
}
.tooltip:global(.place-left):after {
margin-top: $arrow-inset !important;
right: $arrow-inset !important;
transform: rotate(45deg) !important;
}
.tooltip:global(.place-right):after {
margin-top: $arrow-inset !important;
left: $arrow-inset !important;
transform: rotate(-135deg) !important;
}
.tooltip:global(.place-top):after {
margin-right: $arrow-inset !important;
bottom: $arrow-inset !important;
transform: rotate(135deg) !important;
}
.tooltip:global(.place-bottom):after {
margin-left: $arrow-inset !important;
top: $arrow-inset !important;
transform: rotate(-45deg) !important;
}

View File

@ -0,0 +1,210 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import bindAll from 'lodash.bindall';
import ReactTooltip from 'react-tooltip';
import styles from './action-menu.css';
const CLOSE_DELAY = 300; // ms
class ActionMenu extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'clickDelayer',
'handleClosePopover',
'handleToggleOpenState',
'handleTouchStart',
'handleTouchOutside',
'setButtonRef',
'setContainerRef'
]);
this.state = {
isOpen: false,
forceHide: false
};
this.mainTooltipId = `tooltip-${Math.random()}`;
}
componentDidMount () {
// Touch start on the main button is caught to trigger open and not click
this.buttonRef.addEventListener('touchstart', this.handleTouchStart);
// Touch start on document is used to trigger close if it is outside
document.addEventListener('touchstart', this.handleTouchOutside);
}
shouldComponentUpdate (newProps, newState) {
// This check prevents re-rendering while the project is updating.
// @todo check only the state and the title because it is enough to know
// if anything substantial has changed
// This is needed because of the sloppy way the props are passed as a new object,
// which should be refactored.
return newState.isOpen !== this.state.isOpen ||
newState.forceHide !== this.state.forceHide ||
newProps.title !== this.props.title;
}
componentWillUnmount () {
this.buttonRef.removeEventListener('touchstart', this.handleTouchStart);
document.removeEventListener('touchstart', this.handleTouchOutside);
}
handleClosePopover () {
this.closeTimeoutId = setTimeout(() => {
this.setState({isOpen: false});
this.closeTimeoutId = null;
}, CLOSE_DELAY);
}
handleToggleOpenState () {
// Mouse enter back in after timeout was started prevents it from closing.
if (this.closeTimeoutId) {
clearTimeout(this.closeTimeoutId);
this.closeTimeoutId = null;
} else if (!this.state.isOpen) {
this.setState({
isOpen: true,
forceHide: false
});
}
}
handleTouchOutside (e) {
if (this.state.isOpen && !this.containerRef.contains(e.target)) {
this.setState({isOpen: false});
ReactTooltip.hide();
}
}
clickDelayer (fn) {
// Return a wrapped action that manages the menu closing.
// @todo we may be able to use react-transition for this in the future
// for now all this work is to ensure the menu closes BEFORE the
// (possibly slow) action is started.
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}));
});
};
}
handleTouchStart (e) {
// Prevent this touch from becoming a click if menu is closed
if (!this.state.isOpen) {
e.preventDefault();
this.handleToggleOpenState();
}
}
setButtonRef (ref) {
this.buttonRef = ref;
}
setContainerRef (ref) {
this.containerRef = ref;
}
render () {
const {
className,
img: mainImg,
title: mainTitle,
moreButtons,
tooltipPlace,
onClick
} = this.props;
return (
<div
className={classNames(styles.menuContainer, className, {
[styles.expanded]: this.state.isOpen,
[styles.forceHidden]: this.state.forceHide
})}
ref={this.setContainerRef}
onMouseEnter={this.handleToggleOpenState}
onMouseLeave={this.handleClosePopover}
>
<button
aria-label={mainTitle}
className={classNames(styles.button, styles.mainButton)}
data-for={this.mainTooltipId}
data-tip={mainTitle}
ref={this.setButtonRef}
onClick={this.clickDelayer(onClick)}
>
<img
className={styles.mainIcon}
draggable={false}
src={mainImg}
/>
</button>
<ReactTooltip
className={styles.tooltip}
effect="solid"
id={this.mainTooltipId}
place={tooltipPlace || 'left'}
/>
<div className={styles.moreButtonsOuter}>
<div className={styles.moreButtons}>
{(moreButtons || []).map(({img, title, onClick: handleClick,
fileAccept, fileChange, fileInput, fileMultiple}, keyId) => {
const isComingSoon = !handleClick;
const hasFileInput = fileInput;
const tooltipId = `${this.mainTooltipId}-${title}`;
return (
<div key={`${tooltipId}-${keyId}`}>
<button
aria-label={title}
className={classNames(styles.button, styles.moreButton, {
[styles.comingSoon]: isComingSoon
})}
data-for={tooltipId}
data-tip={title}
onClick={hasFileInput ? handleClick : this.clickDelayer(handleClick)}
>
<img
className={styles.moreIcon}
draggable={false}
src={img}
/>
{hasFileInput ? (
<input
accept={fileAccept}
className={styles.fileInput}
multiple={fileMultiple}
ref={fileInput}
type="file"
onChange={fileChange}
/>) : null}
</button>
<ReactTooltip
className={classNames(styles.tooltip, {
[styles.comingSoonTooltip]: isComingSoon
})}
effect="solid"
id={tooltipId}
place={tooltipPlace || 'left'}
/>
</div>
);
})}
</div>
</div>
</div>
);
}
}
ActionMenu.propTypes = {
className: PropTypes.string,
img: PropTypes.string,
moreButtons: PropTypes.arrayOf(PropTypes.shape({
img: PropTypes.string,
title: PropTypes.node.isRequired,
onClick: PropTypes.func, // Optional, "coming soon" if no callback provided
fileAccept: PropTypes.string, // Optional, only for file upload
fileChange: PropTypes.func, // Optional, only for file upload
fileInput: PropTypes.func, // Optional, only for file upload
fileMultiple: PropTypes.bool // Optional, only for file upload
})),
onClick: PropTypes.func.isRequired,
title: PropTypes.node.isRequired,
tooltipPlace: PropTypes.string
};
export default ActionMenu;

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>backdrop-library</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M16.8970588,3.98529412 L17.5588235,3.98529412 C17.9243061,3.98529412 18.2205882,4.28157627 18.2205882,4.64705882 C18.2205882,5.01254138 17.9243061,5.30882353 17.5588235,5.30882353 L16.8970588,5.30882353 L16.8970588,5.97058824 C16.8970588,6.33607079 16.6007767,6.63235294 16.2352941,6.63235294 C15.8698116,6.63235294 15.5735294,6.33607079 15.5735294,5.97058824 L15.5735294,5.30882353 L14.9117647,5.30882353 C14.5462822,5.30882353 14.25,5.01254138 14.25,4.64705882 C14.25,4.28157627 14.5462822,3.98529412 14.9117647,3.98529412 L15.5735294,3.98529412 L15.5735294,3.32352941 C15.5735294,2.95804686 15.8698116,2.66176471 16.2352941,2.66176471 C16.6007767,2.66176471 16.8970588,2.95804686 16.8970588,3.32352941 L16.8970588,3.98529412 Z" id="path-1"></path>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="backdrop-library">
<g id="backdrop-lib-icon" transform="translate(3.850000, 4.650000)" stroke="#FFFFFF" stroke-linecap="round" stroke-width="1.5">
<path d="M12.2943964,8.13062054 L9.83551715,5.59754978 C9.48424868,5.23568253 8.89880123,5.17537132 8.48898801,5.53723857 L5.44466128,8.00999813 C4.97630332,8.37186538 4.33231112,8.31155417 3.98104265,7.8290645 L3.68831893,7.46719725 C3.33705046,6.98470758 2.63451352,6.92439637 2.22470031,7.28626362 L0,9.15591109 L0,9.15591109 C4.82683843e-16,10.0078757 0.690653747,10.6985294 1.54261832,10.6985294 L10.3529412,10.6985294 C11.4575107,10.6985294 12.3529412,9.80309891 12.3529412,8.69852941 L12.3529412,8.13062054 L12.2943964,8.13062054 Z" id="Shape" fill="#FFFFFF" stroke-linejoin="round"></path>
<path d="M12.3529412,3.52941176 L12.3529412,8.82352941 C12.3529412,9.79814956 11.5628554,10.5882353 10.5882353,10.5882353 L1.76470588,10.5882353 C0.790085736,10.5882353 1.19356544e-16,9.79814956 0,8.82352941 L0,1.76470588 C-1.19356544e-16,0.790085736 0.790085736,4.61992691e-15 1.76470588,4.4408921e-15 L8.82352941,4.4408921e-15" id="Rectangle"></path>
</g>
<g id="Combined-Shape">
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
<path stroke="#FFFFFF" stroke-width="0.1" d="M16.9470588,3.93529412 L17.5588235,3.93529412 C17.9519203,3.93529412 18.2705882,4.25396203 18.2705882,4.64705882 C18.2705882,5.04015562 17.9519203,5.35882353 17.5588235,5.35882353 L16.9470588,5.30882353 L16.9470588,5.97058824 C16.9470588,6.36368503 16.6283909,6.68235294 16.2352941,6.68235294 C15.8421973,6.68235294 15.5235294,6.36368503 15.5235294,5.97058824 L15.5735294,5.35882353 L14.9117647,5.35882353 C14.5186679,5.35882353 14.2,5.04015562 14.2,4.64705882 C14.2,4.25396203 14.5186679,3.93529412 14.9117647,3.93529412 L15.5235294,3.98529412 L15.5235294,3.32352941 C15.5235294,2.93043262 15.8421973,2.61176471 16.2352941,2.61176471 C16.6283909,2.61176471 16.9470588,2.93043262 16.9470588,3.32352941 L16.9470588,3.93529412 Z"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>camera</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="camera" fill="#FFFFFF">
<path d="M10,14.5 C8.3485,14.5 7,13.1515 7,11.5 C7,9.8365 8.3485,8.5 10,8.5 C11.6635,8.5 13,9.8365 13,11.5 C13,13.1515 11.6635,14.5 10,14.5 M16,6.25 L14.95,6.25 C14.6635,6.25 14.425,6.1 14.2885,5.86 L13.2835,4.0315 C13.015,3.55 12.52,3.25 11.965,3.25 L8.05,3.25 C7.495,3.25 7,3.55 6.7285,4.0315 L5.71,5.86 C5.575,6.1 5.335,6.25 5.065,6.25 L4,6.25 C3.175,6.25 2.5,6.925 2.5,7.75 L2.5,15.25 C2.5,16.0765 3.175,16.75 4,16.75 L16,16.75 C16.825,16.75 17.5,16.0765 17.5,15.25 L17.5,7.75 C17.5,6.925 16.825,6.25 16,6.25" id="camera-icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>file-upload</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="file-upload" fill="#FFFFFF">
<path d="M5.91737098,6.88612463 L9.63888997,3.16460565 C9.85700097,2.94513145 10.1977994,2.94513145 10.4159104,3.16460565 L14.1374294,6.88612463 C14.4918598,7.24055501 14.232853,7.82536514 13.7421032,7.82536514 L11.8881597,7.82536514 L10.7839727,12.2012171 C10.6749172,12.6251704 10.2386952,12.882814 9.81610516,12.7737585 C9.51620253,12.706962 9.29809153,12.4752191 9.22993184,12.2012171 L8.12574489,7.82536514 L6.31269718,7.82536514 C5.82194742,7.82536514 5.5629406,7.24055501 5.91737098,6.88612463 Z M17,11.601753 L17,13.8510227 C17,15.5822788 15.5959104,17.0000003 13.8646543,17.0000003 L6.1489776,17.0000003 C4.40408958,17.0000003 3,15.5822788 3,13.8510227 L3,11.601753 C3,11.0292116 3.46348588,10.5793576 4.02239533,10.5793576 C4.58130477,10.5793576 5.04479065,11.0292116 5.04479065,11.601753 L5.04479065,13.8510227 C5.04479065,14.4508279 5.53554041,14.9552096 6.1489776,14.9552096 L13.8646543,14.9552096 C14.4644596,14.9552096 14.9552093,14.4508279 14.9552093,13.8510227 L14.9552093,11.601753 C14.9552093,11.0292116 15.4186952,10.5793576 15.9776047,10.5793576 C16.5501461,10.5793576 17,11.0292116 17,11.601753 Z" id="file-upload-icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>paint</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="paint" fill="#FFFFFF">
<path d="M16.1998226,6.58685277 C15.5417294,7.94213763 14.5662678,9.50539101 13.6457617,10.6638734 C12.8750097,11.639511 12.2567593,12.2117734 11.6659868,12.4909258 C11.5972923,12.5341944 11.5299717,12.5481521 11.4461644,12.5481521 C11.3912089,12.5481521 11.3362533,12.5341944 11.2675588,12.5048834 C11.1439087,12.4644063 11.0339976,12.366703 10.979042,12.2410844 C10.8141752,11.8628329 10.580614,11.5432035 10.2659932,11.2905705 C9.94862474,11.053291 9.59141344,10.871842 9.18062043,10.7741386 C9.05559648,10.7462234 8.93194641,10.6638734 8.86325193,10.5368591 C8.79455745,10.4251981 8.76845354,10.2856219 8.79455745,10.1474415 C8.9594242,9.50539101 9.38532999,8.75167956 10.1272304,7.81651905 C11.5849273,5.94480227 14.4975733,3.16863175 15.9113058,3.01370218 C16.2959949,2.9578717 16.5158172,3.08349028 16.6532062,3.19515123 C17.0117914,3.50221886 17.3690027,4.17358036 16.1998226,6.58685277 Z M10.2654437,13.9990466 C10.3478771,14.6969276 10.1692714,15.3808509 9.74199174,15.9251981 C9.37241543,16.412319 8.85033737,16.7486977 8.25956482,16.8882738 C8.23208703,16.9022315 8.19087034,16.9161891 8.16339255,16.9161891 L8.0246297,16.9301467 C7.76496455,16.9720196 7.51766442,16.9999348 7.2689904,16.9999348 C5.29196321,16.9999348 3.90296079,15.6586076 3.35477883,14.7806733 C3.14732149,14.4317328 2.83270076,13.7896823 3.10747869,13.3849114 C3.17617317,13.287208 3.36714383,13.0778437 3.77931072,13.1615894 C5.08450588,13.4407418 5.55162835,12.8545218 5.63543562,12.7428609 C6.51472499,11.5843784 8.14965365,11.3750141 9.27486926,12.2403866 C9.82579901,12.6730728 10.1816364,13.3011656 10.2654437,13.9990466 Z" id="Fill-4"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>search-sprite-library</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="search-sprite-library" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M9.08920325,5.28249422 C11.1744967,5.28249422 12.8944833,6.9872597 12.8944833,9.0877743 C12.8944833,11.1882889 11.1744967,12.8930544 9.08920325,12.8930544 C6.98868865,12.8930544 5.28392318,11.1882889 5.28392318,9.0877743 C5.28392318,6.9872597 6.98868865,5.28249422 9.08920325,5.28249422 M16.667799,15.0544535 L14.5459749,12.9311072 C14.3009148,12.684525 14.2674284,12.3116076 14.4348607,12.0056631 C15.2872434,10.4272329 15.4957728,8.41347867 14.5794613,6.43777725 C13.8473255,4.85630285 12.3997969,3.64470168 10.7117747,3.20937764 C6.25959699,2.06018306 2.30667205,5.81523343 3.10273664,10.2263141 C3.50457422,12.4485977 5.20325124,14.3192733 7.37530511,14.9342066 C9.05876101,15.4121498 10.6706777,15.1503465 11.9918709,14.4334317 C12.2978154,14.2659994 12.672255,14.2994859 12.9203592,14.5445459 L15.0558824,16.6678922 C15.2689781,16.896209 15.5581794,17.0027569 15.8626018,17.0027569 C16.1518031,17.0027569 16.4410044,16.896209 16.667799,16.6678922 C17.1107337,16.2264797 17.1107337,15.4958659 16.667799,15.0544535" id="search" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>sprite-library</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="sprite-library" fill="#FFFFFF">
<path d="M18.5,2.5 L19.25,2.5 C19.6642136,2.5 20,2.83578644 20,3.25 C20,3.66421356 19.6642136,4 19.25,4 L18.5,4 L18.5,4.75 C18.5,5.16421356 18.1642136,5.5 17.75,5.5 C17.3357864,5.5 17,5.16421356 17,4.75 L17,4 L16.25,4 C15.8357864,4 15.5,3.66421356 15.5,3.25 C15.5,2.83578644 15.8357864,2.5 16.25,2.5 L17,2.5 L17,1.75 C17,1.33578644 17.3357864,1 17.75,1 C18.1642136,1 18.5,1.33578644 18.5,1.75 L18.5,2.5 Z M15.9214311,12.3870691 C15.9214311,15.6404905 13.2729235,17 10.0195022,17 C6.767318,17 4.13241787,15.6404905 4.13241787,12.3870691 C4.13241787,11.5829915 4.26725548,10.9026183 4.52827141,10.3335788 L4.42930802,5.63281784 C4.4169376,5.11326006 4.99834749,4.81636989 5.41894188,5.12563048 L7.93013778,7.01830528 C8.51154766,6.70904469 9.22903221,6.57297003 10.0195022,6.57297003 C10.8124464,6.57297003 11.5423013,6.70904469 12.1237112,7.01830528 L14.6349071,5.12563048 C15.0431311,4.81636989 15.624541,5.11326006 15.624541,5.63281784 L15.5255776,10.3335788 C15.7853565,10.9026183 15.9214311,11.5829915 15.9214311,12.3870691 Z M12.5062047,14.4154474 C12.6806277,14.2311281 12.6546498,13.9330009 12.4579601,13.759815 C12.2724037,13.5989995 11.9742765,13.6237403 11.8023276,13.8229041 C11.6650159,13.9824826 11.4670892,14.0690756 11.256792,14.0690756 C10.8609384,14.0690756 10.526937,13.7474445 10.526937,13.3392206 L10.526937,12.6588473 C11.2444215,12.4609205 11.7887202,11.8560068 11.7887202,11.4589162 C11.7887202,10.9640993 11.0093835,10.9640993 10.0692313,10.9640993 C9.11794581,10.9640993 8.35097957,10.9640993 8.35097957,11.4589162 C8.35097957,11.8560068 8.87053734,12.4609205 9.59915527,12.6464769 L9.59915527,13.3392206 C9.59915527,13.7474445 9.27876131,14.0690756 8.88167073,14.0690756 C8.66024015,14.0690756 8.46107634,13.9824826 8.32500168,13.8229041 C8.16418618,13.6237403 7.86729603,13.5989995 7.66936926,13.759815 C7.47267953,13.9330009 7.45907206,14.2311281 7.62112461,14.4154474 C7.93038519,14.7865601 8.3868538,14.9968573 8.88167073,14.9968573 C9.33937638,14.9968573 9.75997077,14.8001676 10.0692313,14.490907 C10.3673585,14.8001676 10.7867159,14.9968573 11.256792,14.9968573 C11.7404755,14.9968573 12.1969441,14.7865601 12.5062047,14.4154474 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>surprise</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="surprise" fill="#FFFFFF">
<path d="M6.99375039,8.65327167 C7.80357478,8.43603525 8.43603525,7.80357478 8.65327167,6.99375039 L9.14824073,5.15274045 C9.38335104,4.28241985 10.6180239,4.28241985 10.8517593,5.15274045 L11.3481032,6.99375039 C11.5653397,7.80357478 12.1978001,8.43603525 13.0062496,8.65327167 L14.8472595,9.14824073 C15.7175802,9.38335104 15.7175802,10.6180239 14.8472595,10.8531342 L13.0062496,11.3481032 C12.1978001,11.5653397 11.5653397,12.1978001 11.3481032,13.0062496 L10.8517593,14.8472595 C10.6180239,15.7175802 9.38335104,15.7175802 9.14824073,14.8472595 L8.65327167,13.0062496 C8.43603525,12.1978001 7.80357478,11.5653397 6.99375039,11.3481032 L5.15274045,10.8531342 C4.28241985,10.6180239 4.28241985,9.38335104 5.15274045,9.14824073 L6.99375039,8.65327167 Z M3.9061823,14.5100817 C4.20079987,14.430095 4.4314281,14.2007999 4.51008165,13.9061823 L4.69005166,13.2369605 C4.77403766,12.9210132 5.22329612,12.9210132 5.30994834,13.2369605 L5.48858524,13.9061823 C5.5685719,14.2007999 5.79786702,14.430095 6.09248459,14.5100817 L6.76303949,14.6900517 C7.07898684,14.7740377 7.07898684,15.2232961 6.76303949,15.3099483 L6.09248459,15.4885852 C5.79786702,15.5685719 5.5685719,15.797867 5.48858524,16.0924846 L5.30994834,16.7617064 C5.22329612,17.0789868 4.77403766,17.0789868 4.69005166,16.7617064 L4.51008165,16.0924846 C4.4314281,15.797867 4.20079987,15.5685719 3.9061823,15.4885852 L3.23696051,15.3099483 C2.92101316,15.2232961 2.92101316,14.7740377 3.23696051,14.6900517 L3.9061823,14.5100817 Z M13.9064844,4.50925154 C14.2012002,4.42923821 14.4305718,4.2012002 14.5092515,3.90648441 L14.6906151,3.23703951 C14.7746291,2.92098683 15.2240373,2.92098683 15.3093849,3.23703951 L15.4894149,3.90648441 C15.5680947,4.2012002 15.7987998,4.42923821 16.0935156,4.50925154 L16.7629605,4.6906151 C17.0790132,4.7746291 17.0790132,5.22403734 16.7629605,5.3093849 L16.0935156,5.4894149 C15.7987998,5.56809468 15.5680947,5.7987998 15.4894149,6.09218203 L15.3093849,6.76162694 C15.2240373,7.07901317 14.7746291,7.07901317 14.6906151,6.76162694 L14.5092515,6.09218203 C14.4305718,5.7987998 14.2012002,5.56809468 13.9064844,5.4894149 L13.2370395,5.3093849 C12.9209868,5.22403734 12.9209868,4.7746291 13.2370395,4.6906151 L13.9064844,4.50925154 Z M5.75,5.75 C5.75,6.164 5.414,6.5 5,6.5 C4.586,6.5 4.25,6.164 4.25,5.75 C4.25,5.336 4.586,5 5,5 C5.414,5 5.75,5.336 5.75,5.75 Z M16,14.25 C16,14.9416667 15.44,15.5 14.75,15.5 C14.0583333,15.5 13.5,14.9416667 13.5,14.25 C13.5,13.56 14.0583333,13 14.75,13 C15.44,13 16,13.56 16,14.25 Z" id="surprise-icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -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;
}

View File

@ -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
}) => (
<Box
className={classNames(styles.alert, styles[level])}
>
{/* TODO: implement Rtl handling */}
{(iconSpinner || iconURL) && (
<div className={styles.iconSection}>
{iconSpinner && (
<Spinner
className={styles.alertSpinner}
level={level}
/>
)}
{iconURL && (
<img
className={styles.alertIcon}
src={iconURL}
/>
)}
</div>
)}
<div className={styles.alertMessage}>
{extensionName ? (
<FormattedMessage
defaultMessage="Scratch lost connection to {extensionName}."
description="Message indicating that an extension peripheral has been disconnected"
id="gui.alerts.lostPeripheralConnection"
values={{
extensionName: (
`${extensionName}`
)
}}
/>
) : content}
</div>
<div className={styles.alertButtons}>
{showSaveNow && (
<button
className={styles.alertConnectionButton}
onClick={onSaveNow}
>
<FormattedMessage
defaultMessage="Try Again"
description="Button to try saving again"
id="gui.alerts.tryAgain"
/>
</button>
)}
{showDownload && (
<button
className={styles.alertConnectionButton}
onClick={onDownload}
>
<FormattedMessage
defaultMessage="Download"
description="Button to download project locally"
id="gui.alerts.download"
/>
</button>
)}
{showReconnect && (
<button
className={styles.alertConnectionButton}
onClick={onReconnect}
>
<FormattedMessage
defaultMessage="Reconnect"
description="Button to reconnect the device"
id="gui.connection.reconnect"
/>
</button>
)}
{closeButton && (
<Box
className={styles.alertCloseButtonContainer}
>
<CloseButton
className={classNames(styles.alertCloseButton)}
color={closeButtonColors[level]}
size={CloseButton.SIZE_LARGE}
onClick={onCloseAlert}
/>
</Box>
)}
</div>
</Box>
);
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;

View File

@ -0,0 +1,4 @@
.alerts-inner-container {
min-width: 200px;
max-width: 548px;
}

View File

@ -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
}) => (
<Box
bounds="parent"
className={className}
>
<Box className={styles.alertsInnerContainer} >
{alertsList.map((a, index) => (
<Alert
closeButton={a.closeButton}
content={a.content}
extensionId={a.extensionId}
extensionName={a.extensionName}
iconSpinner={a.iconSpinner}
iconURL={a.iconURL}
index={index}
key={index}
level={a.level}
message={a.message}
showDownload={a.showDownload}
showReconnect={a.showReconnect}
showSaveNow={a.showSaveNow}
onCloseAlert={onCloseAlert}
/>
))}
</Box>
</Box>
);
AlertsComponent.propTypes = {
alertsList: PropTypes.arrayOf(PropTypes.object),
className: PropTypes.string,
onCloseAlert: PropTypes.func
};
export default AlertsComponent;

View File

@ -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: flex-end;
align-items: center;
font-size: .8125rem;
}
.success {
color: $ui-white-dim;
}
.info {
color: $ui-white;
}
.warn {
color: $error-light;
}
.spinner {
margin-right: $space;
}

View File

@ -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
}) => (
<div
className={classNames(styles.inlineMessage, styles[level])}
>
{/* TODO: implement Rtl handling */}
{iconSpinner && (
<Spinner
small
className={styles.spinner}
level={'info'}
/>
)}
{content}
</div>
);
InlineMessageComponent.propTypes = {
content: PropTypes.element,
iconSpinner: PropTypes.bool,
level: PropTypes.string
};
InlineMessageComponent.defaultProps = {
level: AlertLevels.INFO
};
export default InlineMessageComponent;

View File

@ -0,0 +1,36 @@
@import "../../css/units.css";
@import "../../css/colors.css";
.wrapper {
display: flex;
flex-grow: 1;
border: 1px solid $ui-black-transparent;
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;
overflow: visible;
}
[dir="ltr"] .detail-area {
border-left: 1px solid $ui-black-transparent;
}
[dir="rtl"] .detail-area {
border-right: 1px solid $ui-black-transparent;
}

View File

@ -0,0 +1,23 @@
import React from 'react';
import Box from '../box/box.jsx';
import Selector from './selector.jsx';
import styles from './asset-panel.css';
const AssetPanel = props => (
<Box className={styles.wrapper}>
<Selector
className={styles.selector}
{...props}
/>
<Box className={styles.detailArea}>
{props.children}
</Box>
</Box>
);
AssetPanel.propTypes = {
...Selector.propTypes
};
export default AssetPanel;

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>backdrop-library</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M16.8970588,3.98529412 L17.5588235,3.98529412 C17.9243061,3.98529412 18.2205882,4.28157627 18.2205882,4.64705882 C18.2205882,5.01254138 17.9243061,5.30882353 17.5588235,5.30882353 L16.8970588,5.30882353 L16.8970588,5.97058824 C16.8970588,6.33607079 16.6007767,6.63235294 16.2352941,6.63235294 C15.8698116,6.63235294 15.5735294,6.33607079 15.5735294,5.97058824 L15.5735294,5.30882353 L14.9117647,5.30882353 C14.5462822,5.30882353 14.25,5.01254138 14.25,4.64705882 C14.25,4.28157627 14.5462822,3.98529412 14.9117647,3.98529412 L15.5735294,3.98529412 L15.5735294,3.32352941 C15.5735294,2.95804686 15.8698116,2.66176471 16.2352941,2.66176471 C16.6007767,2.66176471 16.8970588,2.95804686 16.8970588,3.32352941 L16.8970588,3.98529412 Z" id="path-1"></path>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="backdrop-library">
<g id="backdrop-lib-icon" transform="translate(3.850000, 4.650000)" stroke="#FFFFFF" stroke-linecap="round" stroke-width="1.5">
<path d="M12.2943964,8.13062054 L9.83551715,5.59754978 C9.48424868,5.23568253 8.89880123,5.17537132 8.48898801,5.53723857 L5.44466128,8.00999813 C4.97630332,8.37186538 4.33231112,8.31155417 3.98104265,7.8290645 L3.68831893,7.46719725 C3.33705046,6.98470758 2.63451352,6.92439637 2.22470031,7.28626362 L0,9.15591109 L0,9.15591109 C4.82683843e-16,10.0078757 0.690653747,10.6985294 1.54261832,10.6985294 L10.3529412,10.6985294 C11.4575107,10.6985294 12.3529412,9.80309891 12.3529412,8.69852941 L12.3529412,8.13062054 L12.2943964,8.13062054 Z" id="Shape" fill="#FFFFFF" stroke-linejoin="round"></path>
<path d="M12.3529412,3.52941176 L12.3529412,8.82352941 C12.3529412,9.79814956 11.5628554,10.5882353 10.5882353,10.5882353 L1.76470588,10.5882353 C0.790085736,10.5882353 1.19356544e-16,9.79814956 0,8.82352941 L0,1.76470588 C-1.19356544e-16,0.790085736 0.790085736,4.61992691e-15 1.76470588,4.4408921e-15 L8.82352941,4.4408921e-15" id="Rectangle"></path>
</g>
<g id="Combined-Shape">
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
<path stroke="#FFFFFF" stroke-width="0.1" d="M16.9470588,3.93529412 L17.5588235,3.93529412 C17.9519203,3.93529412 18.2705882,4.25396203 18.2705882,4.64705882 C18.2705882,5.04015562 17.9519203,5.35882353 17.5588235,5.35882353 L16.9470588,5.30882353 L16.9470588,5.97058824 C16.9470588,6.36368503 16.6283909,6.68235294 16.2352941,6.68235294 C15.8421973,6.68235294 15.5235294,6.36368503 15.5235294,5.97058824 L15.5735294,5.35882353 L14.9117647,5.35882353 C14.5186679,5.35882353 14.2,5.04015562 14.2,4.64705882 C14.2,4.25396203 14.5186679,3.93529412 14.9117647,3.93529412 L15.5235294,3.98529412 L15.5235294,3.32352941 C15.5235294,2.93043262 15.8421973,2.61176471 16.2352941,2.61176471 C16.6283909,2.61176471 16.9470588,2.93043262 16.9470588,3.32352941 L16.9470588,3.93529412 Z"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>paint</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="paint" fill="#FFFFFF">
<path d="M16.1998226,6.58685277 C15.5417294,7.94213763 14.5662678,9.50539101 13.6457617,10.6638734 C12.8750097,11.639511 12.2567593,12.2117734 11.6659868,12.4909258 C11.5972923,12.5341944 11.5299717,12.5481521 11.4461644,12.5481521 C11.3912089,12.5481521 11.3362533,12.5341944 11.2675588,12.5048834 C11.1439087,12.4644063 11.0339976,12.366703 10.979042,12.2410844 C10.8141752,11.8628329 10.580614,11.5432035 10.2659932,11.2905705 C9.94862474,11.053291 9.59141344,10.871842 9.18062043,10.7741386 C9.05559648,10.7462234 8.93194641,10.6638734 8.86325193,10.5368591 C8.79455745,10.4251981 8.76845354,10.2856219 8.79455745,10.1474415 C8.9594242,9.50539101 9.38532999,8.75167956 10.1272304,7.81651905 C11.5849273,5.94480227 14.4975733,3.16863175 15.9113058,3.01370218 C16.2959949,2.9578717 16.5158172,3.08349028 16.6532062,3.19515123 C17.0117914,3.50221886 17.3690027,4.17358036 16.1998226,6.58685277 Z M10.2654437,13.9990466 C10.3478771,14.6969276 10.1692714,15.3808509 9.74199174,15.9251981 C9.37241543,16.412319 8.85033737,16.7486977 8.25956482,16.8882738 C8.23208703,16.9022315 8.19087034,16.9161891 8.16339255,16.9161891 L8.0246297,16.9301467 C7.76496455,16.9720196 7.51766442,16.9999348 7.2689904,16.9999348 C5.29196321,16.9999348 3.90296079,15.6586076 3.35477883,14.7806733 C3.14732149,14.4317328 2.83270076,13.7896823 3.10747869,13.3849114 C3.17617317,13.287208 3.36714383,13.0778437 3.77931072,13.1615894 C5.08450588,13.4407418 5.55162835,12.8545218 5.63543562,12.7428609 C6.51472499,11.5843784 8.14965365,11.3750141 9.27486926,12.2403866 C9.82579901,12.6730728 10.1816364,13.3011656 10.2654437,13.9990466 Z" id="Fill-4"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>sprite-library</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="sprite-library" fill="#FFFFFF">
<path d="M18.5,2.5 L19.25,2.5 C19.6642136,2.5 20,2.83578644 20,3.25 C20,3.66421356 19.6642136,4 19.25,4 L18.5,4 L18.5,4.75 C18.5,5.16421356 18.1642136,5.5 17.75,5.5 C17.3357864,5.5 17,5.16421356 17,4.75 L17,4 L16.25,4 C15.8357864,4 15.5,3.66421356 15.5,3.25 C15.5,2.83578644 15.8357864,2.5 16.25,2.5 L17,2.5 L17,1.75 C17,1.33578644 17.3357864,1 17.75,1 C18.1642136,1 18.5,1.33578644 18.5,1.75 L18.5,2.5 Z M15.9214311,12.3870691 C15.9214311,15.6404905 13.2729235,17 10.0195022,17 C6.767318,17 4.13241787,15.6404905 4.13241787,12.3870691 C4.13241787,11.5829915 4.26725548,10.9026183 4.52827141,10.3335788 L4.42930802,5.63281784 C4.4169376,5.11326006 4.99834749,4.81636989 5.41894188,5.12563048 L7.93013778,7.01830528 C8.51154766,6.70904469 9.22903221,6.57297003 10.0195022,6.57297003 C10.8124464,6.57297003 11.5423013,6.70904469 12.1237112,7.01830528 L14.6349071,5.12563048 C15.0431311,4.81636989 15.624541,5.11326006 15.624541,5.63281784 L15.5255776,10.3335788 C15.7853565,10.9026183 15.9214311,11.5829915 15.9214311,12.3870691 Z M12.5062047,14.4154474 C12.6806277,14.2311281 12.6546498,13.9330009 12.4579601,13.759815 C12.2724037,13.5989995 11.9742765,13.6237403 11.8023276,13.8229041 C11.6650159,13.9824826 11.4670892,14.0690756 11.256792,14.0690756 C10.8609384,14.0690756 10.526937,13.7474445 10.526937,13.3392206 L10.526937,12.6588473 C11.2444215,12.4609205 11.7887202,11.8560068 11.7887202,11.4589162 C11.7887202,10.9640993 11.0093835,10.9640993 10.0692313,10.9640993 C9.11794581,10.9640993 8.35097957,10.9640993 8.35097957,11.4589162 C8.35097957,11.8560068 8.87053734,12.4609205 9.59915527,12.6464769 L9.59915527,13.3392206 C9.59915527,13.7474445 9.27876131,14.0690756 8.88167073,14.0690756 C8.66024015,14.0690756 8.46107634,13.9824826 8.32500168,13.8229041 C8.16418618,13.6237403 7.86729603,13.5989995 7.66936926,13.759815 C7.47267953,13.9330009 7.45907206,14.2311281 7.62112461,14.4154474 C7.93038519,14.7865601 8.3868538,14.9968573 8.88167073,14.9968573 C9.33937638,14.9968573 9.75997077,14.8001676 10.0692313,14.490907 C10.3673585,14.8001676 10.7867159,14.9968573 11.256792,14.9968573 C11.7404755,14.9968573 12.1969441,14.7865601 12.5062047,14.4154474 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>sound-library</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="sound-library" fill="#FFFFFF">
<path d="M17.75,3.25 L18.5,3.25 C18.9142136,3.25 19.25,3.58578644 19.25,4 C19.25,4.41421356 18.9142136,4.75 18.5,4.75 L17.75,4.75 L17.75,5.5 C17.75,5.91421356 17.4142136,6.25 17,6.25 C16.5857864,6.25 16.25,5.91421356 16.25,5.5 L16.25,4.75 L15.5,4.75 C15.0857864,4.75 14.75,4.41421356 14.75,4 C14.75,3.58578644 15.0857864,3.25 15.5,3.25 L16.25,3.25 L16.25,2.5 C16.25,2.08578644 16.5857864,1.75 17,1.75 C17.4142136,1.75 17.75,2.08578644 17.75,2.5 L17.75,3.25 Z M12.6307801,13.637142 C12.480284,13.637142 12.3255684,13.5975477 12.1849178,13.5141169 C11.7671856,13.2652385 11.6279416,12.7236452 11.8754866,12.303663 C12.346666,11.5004646 12.346666,10.5049512 11.8754866,9.70316691 C11.6279416,9.28177059 11.7671856,8.74017733 12.1849178,8.49129896 C12.6068695,8.24524876 13.1441547,8.38382876 13.3888867,8.80239692 C14.1863754,10.1599153 14.1863754,11.8455006 13.3888867,13.2030189 C13.225732,13.4830071 12.9317723,13.637142 12.6307801,13.637142 Z M15.2566363,15.0553244 C15.1061402,15.0553244 14.950018,15.0157301 14.810774,14.9322993 C14.3930418,14.683421 14.2537977,14.1418277 14.5013427,13.7218455 C15.4873032,12.0447447 15.4873032,9.96038836 14.5013427,8.28470164 C14.2537977,7.8647194 14.3930418,7.32171206 14.810774,7.07283369 C15.2299126,6.82819757 15.7671978,6.96536349 16.0147428,7.38393165 C17.3284191,9.61535244 17.3284191,12.3897806 16.0147428,14.6212014 C15.8501816,14.9011895 15.5576285,15.0553244 15.2566363,15.0553244 Z M10.3785286,6.67858774 L10.3785286,15.3200405 C10.3785286,16.3438356 9.17174676,16.8797725 8.41926624,16.1897007 L6.79475223,14.6978446 C6.21949142,14.1703921 5.46982391,13.8776773 4.69061978,13.8776773 L4.40650564,13.8776773 C3.63011453,13.8776773 3,13.2455828 3,12.4635956 L3,9.5505875 C3,8.77001445 3.63011453,8.13650589 4.40650564,8.13650589 L4.67514822,8.13650589 C5.45435235,8.13650589 6.20401986,7.84379099 6.77928066,7.31633855 L8.41926624,5.81034163 C9.17174676,5.1202698 10.3785286,5.65620673 10.3785286,6.67858774 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>record</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="record" fill="#FFFFFF">
<path d="M15.9365006,11.148381 C15.0369707,13.3332285 13.0518011,14.8264697 10.771958,15.1093996 L10.771958,17.2140836 C10.771958,17.6557686 10.430757,18 9.99650116,18 C9.56224533,18 9.22104432,17.6557686 9.22104432,17.2140836 L9.22104432,15.1093996 C6.95671033,14.8264697 4.97309171,13.347375 4.05650172,11.1798177 C3.90141035,10.7868595 4.07356177,10.3153097 4.47524842,10.1581264 C4.86297684,9.98522477 5.32825095,10.1581264 5.48334232,10.5668029 C6.25879916,12.3901289 8.02684077,13.5847218 9.99650116,13.5847218 C11.9661616,13.5847218 13.7497123,12.3901289 14.50966,10.5337944 C14.6802605,10.1408362 15.1455346,9.95378812 15.533263,10.1266897 C15.9209915,10.2995913 16.1071011,10.7554228 15.9365006,11.148381 Z M7.28224711,8.64692982 L7.28224711,4.68640351 C7.28224711,3.19736842 8.47003548,2 9.91342388,2 C11.3718476,2 12.5446007,3.19736842 12.5446007,4.68640351 L12.5446007,8.64692982 C12.5446007,10.120614 11.3718476,11.3333333 9.91342388,11.3333333 C8.47003548,11.3333333 7.28224711,10.120614 7.28224711,8.64692982 Z" id="record-icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.2 (57519) - http://www.bohemiancoding.com/sketch -->
<title>Artboard</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M63.1539006,65.6857098 C62.40142,65.6857098 61.6278419,65.4877384 60.9245891,65.0705843 C58.8359282,63.8261925 58.1397079,61.1182262 59.3774329,59.018315 C61.7333299,55.0023232 61.7333299,50.0247559 59.3774329,46.0158346 C58.1397079,43.9088529 58.8359282,41.2008866 60.9245891,39.9564948 C63.0343476,38.7262438 65.7207734,39.4191438 66.9444333,41.5119846 C70.9318768,48.2995764 70.9318768,56.7275028 66.9444333,63.5150946 C66.12866,64.9150354 64.6588616,65.6857098 63.1539006,65.6857098 Z M76.2831813,72.7766221 C75.5307008,72.7766221 74.7500901,72.5786507 74.0538698,72.1614966 C71.965209,70.9171048 71.2689887,68.2091385 72.5067136,66.1092273 C77.4365159,57.7237233 77.4365159,47.3019418 72.5067136,38.9235082 C71.2689887,36.823597 71.965209,34.1085603 74.0538698,32.8641685 C76.1495632,31.6409879 78.835989,32.3268175 80.073714,34.4196583 C86.6420953,45.5767622 86.6420953,59.4489029 80.073714,70.6060068 C79.2509082,72.0059476 77.7881423,72.7766221 76.2831813,72.7766221 Z M51.892643,30.8929387 L51.892643,74.1002025 C51.892643,79.219178 45.8587338,81.8988626 42.0963312,78.4485035 L33.9737611,70.9892229 C31.0974571,68.3519607 27.3491195,66.8883863 23.4530989,66.8883863 L22.0325282,66.8883863 C18.1505726,66.8883863 15,63.7279138 15,59.8179782 L15,45.2529375 C15,41.3500723 18.1505726,38.1825294 22.0325282,38.1825294 L23.3757411,38.1825294 C27.2717617,38.1825294 31.0200993,36.718955 33.8964033,34.0816927 L42.0963312,26.5517081 C45.8587338,23.101349 51.892643,25.7810337 51.892643,30.8929387 Z" id="path-1"></path>
</defs>
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Editor-Tabs/Sounds" transform="translate(50.000000, 50.000000) scale(-1, 1) translate(-50.000000, -50.000000) ">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="sound" fill="#4C97FF" fill-rule="evenodd" xlink:href="#path-1"></use>
<g id="Color/Gray" mask="url(#mask-2)" fill="#575E75" fill-rule="evenodd">
<rect id="Color" x="0" y="0" width="100" height="100"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="100px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>Sound</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Sound" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M12.4785058,12.6666667 C12.3144947,12.6666667 12.1458852,12.6272044 11.9926038,12.5440517 C11.537358,12.2960031 11.3856094,11.7562156 11.6553847,11.3376335 C12.1688774,10.5371131 12.1688774,9.54491867 11.6553847,8.74580756 C11.3856094,8.32581618 11.537358,7.78602861 11.9926038,7.53798001 C12.452448,7.29275014 13.0379829,7.43086811 13.3046926,7.84804076 C14.1737981,9.20103311 14.1737981,10.8809986 13.3046926,12.233991 C13.1268862,12.5130457 12.806528,12.6666667 12.4785058,12.6666667 Z M15.3806784,13.8333333 C15.2408902,13.8333333 15.0958763,13.796281 14.9665396,13.7182064 C14.5785295,13.485306 14.4491928,12.9784829 14.6791247,12.5854634 C15.5949331,11.0160321 15.5949331,9.065491 14.6791247,7.49738299 C14.4491928,7.10436352 14.5785295,6.59621712 14.9665396,6.36331669 C15.3558562,6.13438616 15.8549129,6.26274605 16.0848448,6.65444223 C17.3050517,8.74260632 17.3050517,11.3389168 16.0848448,13.4270809 C15.9319924,13.6890939 15.6602547,13.8333333 15.3806784,13.8333333 Z M10.3043478,5.62501557 L10.3043478,13.873675 C10.3043478,14.850934 9.10969849,15.3625101 8.36478311,14.7038052 L6.7566013,13.2797607 C6.18712394,12.7762834 5.44499329,12.4968737 4.67362297,12.4968737 L4.3923652,12.4968737 C3.62377961,12.4968737 3,11.8935108 3,11.1470686 L3,8.36646989 C3,7.62137743 3.62377961,7.01666471 4.3923652,7.01666471 L4.65830695,7.01666471 C5.42967727,7.01666471 6.17180792,6.73725504 6.74128529,6.23377771 L8.36478311,4.79623519 C9.10969849,4.13753026 10.3043478,4.64910643 10.3043478,5.62501557 Z" id="Combined-Shape" fill="#575E75"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,85 @@
@import "../../css/colors.css";
@import "../../css/units.css";
.wrapper {
width: 150px;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
background: $ui-tertiary;
}
.new-buttons {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
padding: 0.75rem 0;
color: $motion-primary;
text-align: center;
background: none;
}
$fade-out-distance: 100px;
.new-buttons:before {
content: "";
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%;
pointer-events: none;
}
.new-buttons > button + button {
margin-top: 0.75rem;
}
.list-area {
/* Must have some height (recalculated by flex-grow) in order to scroll */
height: 0;
flex-grow: 1;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
.list-area:after {
/* Make sure there is room to scroll beyond the last tile */
content: '';
display: block;
height: 70px;
width: 100%;
flex-shrink: 0;
order: 99999999;
}
.list-item {
width: 5rem;
height: 5rem;
margin: 0.5rem auto;
}
@media only screen and (max-width: $full-size-paint) {
.wrapper {
width: 80px;
}
.list-item {
width: 4rem;
}
}
.list-item.placeholder {
background: white;
filter: opacity(15%) brightness(0%);
}

View File

@ -0,0 +1,118 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx';
import Box from '../box/box.jsx';
import ActionMenu from '../action-menu/action-menu.jsx';
import SortableAsset from './sortable-asset.jsx';
import SortableHOC from '../../lib/sortable-hoc.jsx';
import DragConstants from '../../lib/drag-constants';
import styles from './selector.css';
const Selector = props => {
const {
buttons,
containerRef,
dragType,
isRtl,
items,
selectedItemIndex,
draggingIndex,
draggingType,
ordering,
onAddSortable,
onRemoveSortable,
onDeleteClick,
onDuplicateClick,
onExportClick,
onItemClick
} = props;
const isRelevantDrag = draggingType === dragType;
let newButtonSection = null;
if (buttons.length > 0) {
const {img, title, onClick} = buttons[0];
const moreButtons = buttons.slice(1);
newButtonSection = (
<Box className={styles.newButtons}>
<ActionMenu
img={img}
moreButtons={moreButtons}
title={title}
tooltipPlace={isRtl ? 'left' : 'right'}
onClick={onClick}
/>
</Box>
);
}
return (
<Box
className={styles.wrapper}
componentRef={containerRef}
>
<Box className={styles.listArea}>
{items.map((item, index) => (
<SortableAsset
id={item.name}
index={isRelevantDrag ? ordering.indexOf(index) : index}
key={item.name}
onAddSortable={onAddSortable}
onRemoveSortable={onRemoveSortable}
>
<SpriteSelectorItem
asset={item.asset}
className={classNames(styles.listItem, {
[styles.placeholder]: isRelevantDrag && index === draggingIndex
})}
costumeURL={item.url}
details={item.details}
dragPayload={item.dragPayload}
dragType={dragType}
id={index}
index={index}
name={item.name}
number={index + 1 /* 1-indexed */}
selected={index === selectedItemIndex}
onClick={onItemClick}
onDeleteButtonClick={onDeleteClick}
onDuplicateButtonClick={onDuplicateClick}
onExportButtonClick={onExportClick}
/>
</SortableAsset>
))}
</Box>
{newButtonSection}
</Box>
);
};
Selector.propTypes = {
buttons: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
img: PropTypes.string.isRequired,
onClick: PropTypes.func
})),
containerRef: PropTypes.func,
dragType: PropTypes.oneOf(Object.keys(DragConstants)),
draggingIndex: PropTypes.number,
draggingType: PropTypes.oneOf(Object.keys(DragConstants)),
isRtl: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.shape({
url: PropTypes.string,
name: PropTypes.string.isRequired
})),
onAddSortable: PropTypes.func,
onDeleteClick: PropTypes.func,
onDuplicateClick: PropTypes.func,
onExportClick: PropTypes.func,
onItemClick: PropTypes.func.isRequired,
onRemoveSortable: PropTypes.func,
ordering: PropTypes.arrayOf(PropTypes.number),
selectedItemIndex: PropTypes.number.isRequired
};
export default SortableHOC(Selector);

View File

@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
class SortableAsset extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'setRef'
]);
}
componentDidMount () {
this.props.onAddSortable(this.ref);
}
componentWillUnmount () {
this.props.onRemoveSortable(this.ref);
}
setRef (ref) {
this.ref = ref;
}
render () {
return (
<div
className={this.props.className}
ref={this.setRef}
style={{
order: this.props.index
}}
>
{this.props.children}
</div>
);
}
}
SortableAsset.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
index: PropTypes.number.isRequired,
onAddSortable: PropTypes.func.isRequired,
onRemoveSortable: PropTypes.func.isRequired
};
export default SortableAsset;

View File

@ -0,0 +1,53 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import Box from '../box/box.jsx';
import styles from './audio-trimmer.css';
import SelectionHandle from './selection-handle.jsx';
import Playhead from './playhead.jsx';
const AudioSelector = props => (
<div
className={classNames(styles.absolute, styles.selector)}
ref={props.containerRef}
onMouseDown={props.onNewSelectionMouseDown}
onTouchStart={props.onNewSelectionMouseDown}
>
{props.trimStart === null ? null : (
<Box
className={classNames(styles.absolute)}
style={{
left: `${props.trimStart * 100}%`,
width: `${100 * (props.trimEnd - props.trimStart)}%`
}}
>
<Box className={classNames(styles.absolute, styles.selectionBackground)} />
<SelectionHandle
handleStyle={styles.leftHandle}
onMouseDown={props.onTrimStartMouseDown}
/>
<SelectionHandle
handleStyle={styles.rightHandle}
onMouseDown={props.onTrimEndMouseDown}
/>
</Box>
)}
{props.playhead ? (
<Playhead
playbackPosition={props.playhead}
/>
) : null}
</div>
);
AudioSelector.propTypes = {
containerRef: PropTypes.func,
onNewSelectionMouseDown: PropTypes.func.isRequired,
onTrimEndMouseDown: PropTypes.func.isRequired,
onTrimStartMouseDown: PropTypes.func.isRequired,
playhead: PropTypes.number,
trimEnd: PropTypes.number,
trimStart: PropTypes.number
};
export default AudioSelector;

View File

@ -0,0 +1,148 @@
@import "../../css/colors.css";
$border-radius: 4px;
$trim-handle-width: 30px;
$trim-handle-height: 30px;
$trim-handle-border: 3px;
$stripe-size: 10px;
$hover-scale: 1.25;
.absolute {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* Force the browser to paint separately to avoid composite cost with waveform */
transform: translateZ(0);
}
.selector {
cursor: pointer;
}
.trim-background {
cursor: pointer;
touch-action: none;
}
.trim-background-mask {
border: 1px solid $red-tertiary;
opacity: 0.5;
background: repeating-linear-gradient(
45deg,
$red-primary,
$red-primary $stripe-size,
$red-tertiary $stripe-size,
$red-tertiary calc(2 * $stripe-size)
);
}
.selection-background {
background: $motion-primary;
opacity: 0.5;
}
.start-trim-background .trim-background-mask {
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
.end-trim-background .trim-background-mask {
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
.trim-line {
position: absolute;
top: 0;
width: 0px;
height: 100%;
border: 1px solid $red-tertiary;
}
.selector .trim-line {
border: 1px solid $motion-tertiary;
}
.playhead-container {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
.playhead {
/*
Even though playhead is just a line, it is 100% width (the width of the waveform)
so that we can use transform: translateX() using percentages.
*/
width: 100%;
height: 100%;
border-left: 1px solid $motion-primary;
border-top: none;
border-bottom: none;
border-right: none;
}
.right-handle {
transform: scaleX(-1);
}
.selector .left-handle {
left: -1px
}
.selector .right-handle {
right: -1px
}
.trimmer .left-handle {
right: -1px
}
.trimmer .right-handle {
left: -1px
}
.trim-handle {
position: absolute;
width: $trim-handle-width;
height: $trim-handle-height;
right: 0;
user-select: none;
}
.trimmer .trim-handle {
filter: hue-rotate(150deg);
}
.trim-handle img {
position: absolute;
width: $trim-handle-width;
height: $trim-handle-height;
left: calc($trim-handle-border * 1.5);
/* Make sure image dragging isn't triggered */
user-select: none;
user-drag: none;
-webkit-user-drag: none; /* Autoprefixer doesn't seem to work for this */
transition: 0.2s;
}
.top-trim-handle {
top: calc(-$trim-handle-height + $trim-handle-border);
}
.bottom-trim-handle {
bottom: calc(-$trim-handle-height + $trim-handle-border);
}
.top-trim-handle img {
transform: scaleY(-1);
}

View File

@ -0,0 +1,62 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import Box from '../box/box.jsx';
import styles from './audio-trimmer.css';
import SelectionHandle from './selection-handle.jsx';
import Playhead from './playhead.jsx';
const AudioTrimmer = props => (
<div
className={classNames(styles.absolute, styles.trimmer)}
ref={props.containerRef}
>
{props.trimStart === null ? null : (
<Box
className={classNames(styles.absolute, styles.trimBackground, styles.startTrimBackground)}
style={{
width: `${100 * props.trimStart}%`
}}
onMouseDown={props.onTrimStartMouseDown}
onTouchStart={props.onTrimStartMouseDown}
>
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
<SelectionHandle
handleStyle={styles.leftHandle}
/>
</Box>
)}
{props.playhead ? (
<Playhead
playbackPosition={props.playhead}
/>
) : null}
{props.trimEnd === null ? null : (
<Box
className={classNames(styles.absolute, styles.trimBackground, styles.endTrimBackground)}
style={{
left: `${100 * props.trimEnd}%`,
width: `${100 - (100 * props.trimEnd)}%`
}}
onMouseDown={props.onTrimEndMouseDown}
onTouchStart={props.onTrimEndMouseDown}
>
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
<SelectionHandle
handleStyle={styles.rightHandle}
/>
</Box>
)}
</div>
);
AudioTrimmer.propTypes = {
containerRef: PropTypes.func,
onTrimEndMouseDown: PropTypes.func.isRequired,
onTrimStartMouseDown: PropTypes.func.isRequired,
playhead: PropTypes.number,
trimEnd: PropTypes.number,
trimStart: PropTypes.number
};
export default AudioTrimmer;

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="34px" height="34px" viewBox="1 1 33 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
<title>Bottom Left Handle</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M17,5 C23.627417,5 29,10.372583 29,17 L29,29 L17,29 C10.372583,29 5,23.627417 5,17 C5,10.372583 10.372583,5 17,5 Z" id="path-1"></path>
</defs>
<g id="Bottom-Left-Handle" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Bottom-Left" transform="translate(17.000000, 17.000000) scale(1, -1) translate(-17.000000, -17.000000) ">
<use stroke="#4D97FF33" stroke-width="8" xlink:href="#path-1"></use>
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
<use stroke="#3D73CC" stroke-width="1" fill="#4D97FF" fill-rule="evenodd" xlink:href="#path-1"></use>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,21 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import styles from './audio-trimmer.css';
const Playhead = props => (
<div className={styles.playheadContainer}>
<div
className={classNames(styles.playhead)}
style={{
transform: `translateX(${100 * props.playbackPosition}%)`
}}
/>
</div>
);
Playhead.propTypes = {
playbackPosition: PropTypes.number
};
export default Playhead;

View File

@ -0,0 +1,28 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import Box from '../box/box.jsx';
import styles from './audio-trimmer.css';
import handleIcon from './icon--handle.svg';
const SelectionHandle = props => (
<Box
className={classNames(styles.trimLine, props.handleStyle)}
onMouseDown={props.onMouseDown}
onTouchStart={props.onMouseDown}
>
<Box className={classNames(styles.trimHandle, styles.topTrimHandle)}>
<img src={handleIcon} />
</Box>
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle)}>
<img src={handleIcon} />
</Box>
</Box>
);
SelectionHandle.propTypes = {
handleStyle: PropTypes.string,
onMouseDown: PropTypes.func
};
export default SelectionHandle;

View File

@ -0,0 +1,101 @@
@import "../../css/units.css";
@import "../../css/colors.css";
.backpack-container {
flex-shrink: 1;
position: relative;
}
.backpack-header {
margin-top: 0.5rem;
border: 1px solid $ui-black-transparent;
background: $ui-white;
padding: 0.25rem;
text-align: center;
font-size: 0.85rem;
color: $text-primary;
transition: 0.2s;
cursor: pointer;
user-select: none;
}
[dir="ltr"] .backpack-header {
border-top-right-radius: $space;
}
[dir="rtl"] .backpack-header {
border-top-left-radius: $space;
}
.backpack-list {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
border-right: 1px solid $ui-black-transparent;
min-height: 5.5rem;
}
/* Absolute position the inner list to allow scrolling inside flex sized container */
.backpack-list-inner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
overflow-x: auto;
}
.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;
}
.status-message {
width: 100%;
text-align: center;
font-size: 0.85rem;
color: $text-primary;
}
.backpack-item {
width: 4rem;
height: 4.5rem;
margin: 0 0.25rem;
flex-shrink: 0;
/* Need to hide overflow because of background setting below */
overflow: hidden;
}
.backpack-item > 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;
}

View File

@ -0,0 +1,164 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import DragConstants from '../../lib/drag-constants';
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx';
import styles from './backpack.css';
// TODO make sprite selector item not require onClick
const noop = () => {};
const dragTypeMap = { // Keys correspond with the backpack-server item types
costume: DragConstants.BACKPACK_COSTUME,
sound: DragConstants.BACKPACK_SOUND,
script: DragConstants.BACKPACK_CODE,
sprite: DragConstants.BACKPACK_SPRITE
};
const Backpack = ({
blockDragOver,
containerRef,
contents,
dragOver,
error,
expanded,
loading,
showMore,
onToggle,
onDelete,
onMouseEnter,
onMouseLeave,
onMore
}) => (
<div className={styles.backpackContainer}>
<div
className={styles.backpackHeader}
onClick={onToggle}
>
{onToggle ? (
<FormattedMessage
defaultMessage="Backpack"
description="Button to open the backpack"
id="gui.backpack.header"
/>
) : (
<ComingSoonTooltip
place="top"
tooltipId="backpack-tooltip"
>
<FormattedMessage
defaultMessage="Backpack"
description="Button to open the backpack"
id="gui.backpack.header"
/>
</ComingSoonTooltip>
)}
</div>
{expanded ? (
<div
className={classNames(styles.backpackList, {
[styles.dragOver]: dragOver || blockDragOver
})}
ref={containerRef}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{error ? (
<div className={styles.statusMessage}>
<FormattedMessage
defaultMessage="Error loading backpack"
description="Error backpack message"
id="gui.backpack.errorBackpack"
/>
</div>
) : (
loading ? (
<div className={styles.statusMessage}>
<FormattedMessage
defaultMessage="Loading..."
description="Loading backpack message"
id="gui.backpack.loadingBackpack"
/>
</div>
) : (
contents.length > 0 ? (
<div className={styles.backpackListInner}>
{contents.map(item => (
<SpriteSelectorItem
className={styles.backpackItem}
costumeURL={item.thumbnailUrl}
details={item.name}
dragPayload={item}
dragType={dragTypeMap[item.type]}
id={item.id}
key={item.id}
name={item.type}
selected={false}
onClick={noop}
onDeleteButtonClick={onDelete}
/>
))}
{showMore && (
<button
className={styles.more}
onClick={onMore}
>
<FormattedMessage
defaultMessage="More"
description="Load more from backpack"
id="gui.backpack.more"
/>
</button>
)}
</div>
) : (
<div className={styles.statusMessage}>
<FormattedMessage
defaultMessage="Backpack is empty"
description="Empty backpack message"
id="gui.backpack.emptyBackpack"
/>
</div>
)
)
)}
</div>
) : null}
</div>
);
Backpack.propTypes = {
blockDragOver: PropTypes.bool,
containerRef: PropTypes.func,
contents: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
thumbnailUrl: PropTypes.string,
type: PropTypes.string,
name: PropTypes.string
})),
dragOver: PropTypes.bool,
error: PropTypes.bool,
expanded: PropTypes.bool,
loading: PropTypes.bool,
onDelete: 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
};
export default Backpack;

View File

@ -0,0 +1,99 @@
@import "../../css/units.css";
@import "../../css/colors.css";
@import "../../css/z-index.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;
right: 0;
bottom: 0;
left: 0;
border: 1px solid $ui-black-transparent;
border-top-right-radius: $space;
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;
}
.blocks :global(.blocklyToolboxDiv) {
border-right: 1px solid $ui-black-transparent;
border-bottom: 1px solid $ui-black-transparent;
box-sizing: content-box;
height: calc(100% - 3.25rem) !important;
/*
For now, the layout cannot support scrollbars in the category menu.
The line below works for Edge, the `::-webkit-scrollbar` line
below that is for webkit browsers. It isn't possible to do the
same for Firefox, so a different solution may be needed for them.
*/
-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;
}
.blocks :global(.blocklyFlyout) {
border-right: 1px solid $ui-black-transparent;
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.
This does not prevent user interaction on the blocks themselves.
*/
pointer-events: none;
z-index: $z-index-drag-layer; /* make blocks match gui drag layer */
}
/*
Shrink category font to fit "My Blocks" for now.
Probably will need different solutions for language support later, so
make the change here instead of in scratch-blocks.
*/
.blocks :global(.scratchCategoryMenuItemLabel) {
font-size: 0.65rem;
}
.blocks :global(.blocklyMinimalBody) {
min-width: auto;
min-height: auto;
}

View File

@ -0,0 +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 {
containerRef,
dragOver,
...componentProps
} = props;
return (
<Box
className={classNames(styles.blocks, {
[styles.dragOver]: dragOver
})}
{...componentProps}
componentRef={containerRef}
/>
);
};
BlocksComponent.propTypes = {
containerRef: PropTypes.func,
dragOver: PropTypes.bool
};
export default BlocksComponent;

View File

@ -0,0 +1,2 @@
.box {
}

139
src/components/box/box.jsx Normal file
View File

@ -0,0 +1,139 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import stylePropType from 'react-style-proptype';
import styles from './box.css';
const getRandomColor = (function () {
// In "DEBUG" mode this is used to output a random background color for each
// box. The function gives the same "random" set for each seed, allowing re-
// renders of the same content to give the same random display.
const random = (function (seed) {
let mW = seed;
let mZ = 987654321;
const mask = 0xffffffff;
return function () {
mZ = ((36969 * (mZ & 65535)) + (mZ >> 16)) & mask;
mW = ((18000 * (mW & 65535)) + (mW >> 16)) & mask;
let result = ((mZ << 16) + mW) & mask;
result /= 4294967296;
return result + 1;
};
}(601));
return function () {
const r = Math.max(parseInt(random() * 100, 10) % 256, 1);
const g = Math.max(parseInt(random() * 100, 10) % 256, 1);
const b = Math.max(parseInt(random() * 100, 10) % 256, 1);
return `rgb(${r},${g},${b})`;
};
}());
const Box = props => {
const {
alignContent,
alignItems,
alignSelf,
basis,
children,
className,
componentRef,
direction,
element,
grow,
height,
justifyContent,
width,
wrap,
shrink,
style,
...componentProps
} = props;
return React.createElement(element, {
className: classNames(className, styles.box),
ref: componentRef,
style: Object.assign(
{
alignContent: alignContent,
alignItems: alignItems,
alignSelf: alignSelf,
flexBasis: basis,
flexDirection: direction,
flexGrow: grow,
flexShrink: shrink,
flexWrap: wrap,
justifyContent: justifyContent,
width: width,
height: height
},
process.env.DEBUG ? {
backgroundColor: getRandomColor(),
outline: `1px solid black`
} : {},
style
),
...componentProps
}, children);
};
Box.propTypes = {
/** Defines how the browser distributes space between and around content items vertically within this box. */
alignContent: PropTypes.oneOf([
'flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'stretch'
]),
/** Defines how the browser distributes space between and around flex items horizontally within this box. */
alignItems: PropTypes.oneOf([
'flex-start', 'flex-end', 'center', 'baseline', 'stretch'
]),
/** Specifies how this box should be aligned inside of its container (requires the container to be flexable). */
alignSelf: PropTypes.oneOf([
'auto', 'flex-start', 'flex-end', 'center', 'baseline', 'stretch'
]),
/** Specifies the initial length of this box */
basis: PropTypes.oneOfType([
PropTypes.number,
PropTypes.oneOf(['auto'])
]),
/** Specifies the the HTML nodes which will be child elements of this box. */
children: PropTypes.node,
/** Specifies the class name that will be set on this box */
className: PropTypes.string,
/**
* A callback function whose first parameter is the underlying dom elements.
* This call back will be executed immediately after the component is mounted or unmounted
*/
componentRef: PropTypes.func,
/** https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction */
direction: PropTypes.oneOf([
'row', 'row-reverse', 'column', 'column-reverse'
]),
/** Specifies the type of HTML element of this box. Defaults to div. */
element: PropTypes.string,
/** Specifies the flex grow factor of a flex item. */
grow: PropTypes.number,
/** The height in pixels (if specified as a number) or a string if different units are required. */
height: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
/** https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content */
justifyContent: PropTypes.oneOf([
'flex-start', 'flex-end', 'center', 'space-between', 'space-around'
]),
/** Specifies the flex shrink factor of a flex item. */
shrink: PropTypes.number,
/** An object whose keys are css property names and whose values correspond the the css property. */
style: stylePropType,
/** The width in pixels (if specified as a number) or a string if different units are required. */
width: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
/** How whitespace should wrap within this block. */
wrap: PropTypes.oneOf([
'nowrap', 'wrap', 'wrap-reverse'
])
};
Box.defaultProps = {
element: 'div',
style: {}
};
export default Box;

View File

@ -0,0 +1,82 @@
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/typography.css";
@import "../../css/z-index.css";
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: $z-index-modal;
background-color: $ui-modal-overlay;
}
.modal-content {
margin: 100px auto;
outline: none;
border: .25rem solid $ui-white-transparent;
padding: 0;
border-radius: $space;
user-select: none;
width: 500px;
color: $text-primary;
overflow: hidden;
}
.illustration {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100px;
background-color: $control-primary;
}
[dir="rtl"] .illustration {
transform: scaleX(-1);
}
.illustration img {
height: 80%;
width: auto;
}
.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
text-align: center;
}
/* Confirmation buttons at the bottom of the modal */
.button-row {
margin: 1.5rem 0;
font-weight: bolder;
text-align: right;
display: flex;
justify-content: center;
}
.button-row button {
border: 1px solid $motion-primary;
border-radius: 0.25rem;
padding: 0.5rem 2rem;
background: $motion-primary;
color: white;
font-weight: bold;
font-size: 0.875rem;
cursor: pointer;
}
.faq-link-text {
margin: 2rem 0 .5rem 0;
font-size: .875rem;
color: $text-primary;
}
.faq-link {
color: $motion-primary;
text-decoration: none;
}

View File

@ -0,0 +1,113 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactModal from 'react-modal';
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 label = props.error ? messages.error : messages.label;
return (
<ReactModal
isOpen
className={styles.modalContent}
contentLabel={intl.formatMessage({...messages.label})}
overlayClassName={styles.modalOverlay}
onRequestClose={props.onBack}
>
<div dir={props.isRtl ? 'rtl' : 'ltr'} >
<Box className={styles.illustration}>
<img src={unhappyBrowser} />
</Box>
<Box className={styles.body}>
<h2>
<FormattedMessage {...label} />
</h2>
<p>
{ /* eslint-disable max-len */ }
{
props.error ? <FormattedMessage
defaultMessage="We are very sorry, but it looks like you are using a browser version that Scratch does not support. We recommend updating to the latest version of a supported browser such as Google Chrome, Mozilla Firefox, Microsoft Edge, or Apple Safari. "
description="Error message when the browser does not meet our minimum requirements"
id="gui.unsupportedBrowser.notRecommended"
/> : <FormattedMessage
defaultMessage="We are very sorry, but Scratch does not support this browser. We recommend updating to the latest version of a supported browser such as Google Chrome, Mozilla Firefox, Microsoft Edge, or Apple Safari."
description="Error message when the browser does not work at all (IE)"
id="gui.unsupportedBrowser.description"
/>
}
{ /* eslint-enable max-len */ }
</p>
<Box className={styles.buttonRow}>
<button
className={styles.backButton}
onClick={props.onBack}
>
<FormattedMessage
defaultMessage="Back"
description="Button to go back in unsupported browser modal"
id="gui.unsupportedBrowser.back"
/>
</button>
</Box>
<div className={styles.faqLinkText}>
<FormattedMessage
defaultMessage="To learn more, go to the {previewFaqLink}."
description="Invitation to try 3.0 preview"
id="gui.unsupportedBrowser.previewfaq"
values={{
previewFaqLink: (
<a
className={styles.faqLink}
href="//scratch.mit.edu/3faq"
>
<FormattedMessage
defaultMessage="FAQ"
description="link to Scratch 3.0 FAQ page"
id="gui.unsupportedBrowser.previewfaqlinktext"
/>
</a>
)
}}
/>
</div>
</Box>
</div>
</ReactModal>
);
};
BrowserModal.propTypes = {
error: PropTypes.bool,
intl: intlShape.isRequired,
isRtl: PropTypes.bool,
onBack: PropTypes.func.isRequired
};
BrowserModal.defaultProps = {
error: false
};
const WrappedBrowserModal = injectIntl(BrowserModal);
WrappedBrowserModal.setAppElement = ReactModal.setAppElement;
export default WrappedBrowserModal;

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="200px"
height="150px" viewBox="0 0 200 150" style="enable-background:new 0 0 200 150;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.1;fill:#231F20;stroke:#231F20;stroke-width:12;stroke-miterlimit:10;}
.st1{fill:#FFFFFF;stroke:#7F8CA5;stroke-width:2;stroke-miterlimit:10;}
.st2{fill:#BFC6D4;stroke:#7F8CA5;stroke-width:2;stroke-miterlimit:10;}
.st3{fill:#FFFFFF;}
.st4{opacity:0.25;}
.st5{fill:#231F20;}
.st6{opacity:0.15;}
.st7{fill:none;stroke:#231F20;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st8{fill:#7F9BD4;}
</style>
<g id="Layer_1">
</g>
<g id="Unsupported_Mask">
<g>
<g>
<path class="st0" d="M186,140H14c-2.21,0-4-1.79-4-4V14c0-2.21,1.79-4,4-4h172c2.21,0,4,1.79,4,4v122
C190,138.21,188.21,140,186,140z"/>
<path class="st1" d="M186,140H14c-2.21,0-4-1.79-4-4V14c0-2.21,1.79-4,4-4h172c2.21,0,4,1.79,4,4v122
C190,138.21,188.21,140,186,140z"/>
<path class="st2" d="M190,30H10V14c0-2.21,1.79-4,4-4h172c2.21,0,4,1.79,4,4V30z"/>
<path class="st3" d="M179.5,24h-128c-2.21,0-4-1.79-4-4v0c0-2.21,1.79-4,4-4h128c2.21,0,4,1.79,4,4v0
C183.5,22.21,181.71,24,179.5,24z"/>
<g class="st4">
<path class="st5" d="M24.09,20.22c-0.08,0.38-0.38,0.65-0.72,0.72l-2.87,0.66v1.77c0,0.44-0.55,0.66-0.87,0.36l-3.36-3.38
c-0.21-0.19-0.21-0.51,0-0.7l3.36-3.38c0.32-0.32,0.87-0.09,0.87,0.36v1.8l2.87,0.65C23.88,19.2,24.2,19.71,24.09,20.22z"/>
</g>
<g class="st4">
<path class="st5" d="M30.62,19.78c0.08-0.38,0.38-0.65,0.72-0.72l2.87-0.66v-1.77c0-0.44,0.55-0.66,0.87-0.36l3.36,3.38
c0.21,0.19,0.21,0.51,0,0.7l-3.36,3.38c-0.32,0.32-0.87,0.09-0.87-0.36v-1.8l-2.87-0.65C30.83,20.8,30.5,20.29,30.62,19.78z"/>
</g>
<g class="st6">
<line class="st7" x1="69.89" y1="20" x2="51.43" y2="20"/>
<line class="st7" x1="113.74" y1="20" x2="98.4" y2="20"/>
<line class="st7" x1="93.9" y1="20" x2="74.02" y2="20"/>
</g>
</g>
<g>
<circle class="st8" cx="89.61" cy="73.46" r="3.85"/>
<circle class="st8" cx="110.39" cy="73.46" r="3.85"/>
<g>
<path class="st8" d="M83.06,94.84c1.02-3.39,3.54-6.3,6.6-8.19c3.07-1.94,6.72-2.9,10.34-2.91c3.61,0.01,7.27,0.97,10.33,2.91
c3.06,1.89,5.58,4.8,6.6,8.19c0.16,0.53-0.14,1.1-0.68,1.26c-0.4,0.12-0.83-0.02-1.08-0.33l-0.02-0.03
c-3.85-4.75-9.5-7-15.17-7.01c-5.67,0.01-11.32,2.25-15.16,7.01l-0.02,0.02c-0.35,0.43-0.99,0.5-1.42,0.15
C83.06,95.66,82.95,95.23,83.06,94.84z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,29 @@
@import "../../css/units.css";
.outlined-button {
cursor: pointer;
border-radius: $form-radius;
font-weight: bold;
display: flex;
flex-direction: row;
align-items: center;
padding-left: .75rem;
padding-right: .75rem;
user-select: none;
}
.icon {
height: 1.5rem;
}
[dir="ltr"] .icon {
margin-right: .5rem;
}
[dir="rtl"] .icon {
margin-left: .5rem;
}
.content {
white-space: nowrap;
}

View File

@ -0,0 +1,54 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import styles from './button.css';
const ButtonComponent = ({
className,
disabled,
iconClassName,
iconSrc,
onClick,
children,
...props
}) => {
if (disabled) {
onClick = function () {};
}
const icon = iconSrc && (
<img
className={classNames(iconClassName, styles.icon)}
draggable={false}
src={iconSrc}
/>
);
return (
<span
className={classNames(
styles.outlinedButton,
className
)}
role="button"
onClick={onClick}
{...props}
>
{icon}
<div className={styles.content}>{children}</div>
</span>
);
};
ButtonComponent.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
disabled: PropTypes.bool,
iconClassName: PropTypes.string,
iconSrc: PropTypes.string,
onClick: PropTypes.func
};
export default ButtonComponent;

View File

@ -0,0 +1,157 @@
@import "../../css/colors.css";
@import "../../css/units.css";
$main-button-size: 2.75rem;
.modal-content {
width: 552px;
}
.body {
display: flex;
flex-direction: column;
align-items: center;
background: $ui-white;
padding: 1.5rem 2.25rem;
}
.camera-feed-container {
display: flex;
justify-content: space-around;
align-items: center;
background: $ui-primary;
border: 1px solid $ui-black-transparent;
border-radius: 4px;
padding: 3px;
width: 480px;
height: 360px;
position: relative;
overflow: hidden;
}
.canvas {
position: absolute;
width: 480px;
height: 360px;
}
.loading-text {
position: absolute;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: $text-primary-transparent;
font-size: 0.95rem;
font-weight: 500;
text-align: center;
}
.help-text {
margin: 10px auto 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: $text-primary-transparent;
font-size: 0.95rem;
font-weight: 500;
text-align: center;
}
.capture-text {
color: $motion-primary;
}
.disabled-text {
color: $text-primary;
opacity: 0.25;
}
.main-button-row {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
margin-top: 15px;
width: 100%;
}
/* Action Menu */
.main-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: $motion-primary;
outline: none;
border: none;
transition: background-color 0.2s;
border-radius: 100%;
width: $main-button-size;
height: $main-button-size;
box-shadow: 0 0 0 4px $motion-transparent;
}
.main-button:hover {
background: $extensions-primary;
box-shadow: 0 0 0 6px $motion-transparent;
}
.main-button:disabled {
background: $text-primary;
border-color: $ui-black-transparent;
box-shadow: none;
opacity: 0.25;
}
.main-icon {
width: calc($main-button-size - 1rem);
height: calc($main-button-size - 1rem);
}
.button-row {
font-weight: bolder;
text-align: right;
display: flex;
justify-content: space-between;
margin-top: 20px;
width: 480px;
}
.button-row button {
padding: 0.75rem 1rem;
border-radius: 0.25rem;
background: $ui-white;
border: 1px solid $ui-black-transparent;
font-weight: 600;
font-size: 0.85rem;
color: $motion-primary;
cursor: pointer;
}
.button-row button.ok-button {
background: $motion-primary;
border: $motion-primary;
color: $ui-white;
}
[dir="rtl"] .retake-button img {
transform: scaleX(-1);
}
@keyframes flash {
0% { opacity: 1; }
100% { opacity: 0; }
}
.flash-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $ui-white;
animation-name: flash;
animation-duration: 0.5s;
animation-fill-mode: forwards; /* Leave at 0 opacity after animation */
}

View File

@ -0,0 +1,141 @@
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 '../../containers/modal.jsx';
import styles from './camera-modal.css';
import backIcon from './icon--back.svg';
import cameraIcon from '../action-menu/icon--camera.svg';
const messages = defineMessages({
cameraModalTitle: {
defaultMessage: 'Take a Photo',
description: 'Title for prompt to take a picture (to add as a new costume).',
id: 'gui.cameraModal.cameraModalTitle'
},
loadingCameraMessage: {
defaultMessage: 'Loading Camera...',
description: 'Notification to the user that the camera is loading',
id: 'gui.cameraModal.loadingCameraMessage'
},
permissionRequest: {
defaultMessage: 'We need your permission to use your camera',
description: 'Notification to the user that the app needs camera access',
id: 'gui.cameraModal.permissionRequest'
},
retakePhoto: {
defaultMessage: 'Retake Photo',
description: 'A button that allows the user to take the picture again, replacing the old one',
id: 'gui.cameraModal.retakePhoto'
},
save: {
defaultMessage: 'Save',
description: 'A button that allows the user to save the photo they took as a costume',
id: 'gui.cameraModal.save'
},
takePhotoButton: {
defaultMessage: 'Take Photo',
description: 'A button to take a photo',
id: 'gui.cameraModal.takePhoto'
},
loadingCaption: {
defaultMessage: 'Loading...',
description: 'A caption for a disabled button while the video from the camera is still loading',
id: 'gui.cameraModal.loadingCaption'
},
enableCameraCaption: {
defaultMessage: 'Enable Camera',
description: 'A caption for a disabled button prompting the user to enable camera access',
id: 'gui.cameraModal.enableCameraCaption'
}
});
const CameraModal = ({intl, ...props}) => (
<Modal
className={styles.modalContent}
contentLabel={intl.formatMessage(messages.cameraModalTitle)}
onRequestClose={props.onCancel}
>
<Box className={styles.body}>
<Box className={styles.cameraFeedContainer}>
<div className={styles.loadingText}>
{props.access ? intl.formatMessage(messages.loadingCameraMessage) :
`↖️ \u00A0${intl.formatMessage(messages.permissionRequest)}`}
</div>
<canvas
className={styles.canvas}
// height and (below) width of the actual image
// double stage dimensions to avoid the need for
// resizing the captured image when importing costume
// to accommodate double resolution bitmaps
height="720"
ref={props.canvasRef}
width="960"
/>
{props.capture ? (
<div className={styles.flashOverlay} />
) : null}
</Box>
{props.capture ?
<Box className={styles.buttonRow}>
<button
className={styles.retakeButton}
key="retake-button"
onClick={props.onBack}
>
<img
draggable={false}
src={backIcon}
/> {intl.formatMessage(messages.retakePhoto)}
</button>
<button
className={styles.okButton}
onClick={props.onSubmit}
> {intl.formatMessage(messages.save)}
</button>
</Box> :
<Box className={styles.mainButtonRow}>
<button
className={styles.mainButton}
disabled={!props.loaded}
key="capture-button"
onClick={props.onCapture}
>
<img
className={styles.mainIcon}
draggable={false}
src={cameraIcon}
/>
</button>
<div className={styles.helpText}>
{props.access ?
<span className={props.loaded ? styles.captureText : styles.disabledText}>
{props.loaded ?
intl.formatMessage(messages.takePhotoButton) :
intl.formatMessage(messages.loadingCaption)}
</span> :
<span className={styles.disabledText}>
{intl.formatMessage(messages.enableCameraCaption)}
</span>
}
</div>
</Box>
}
</Box>
</Modal>
);
CameraModal.propTypes = {
access: PropTypes.bool,
canvasRef: PropTypes.func.isRequired,
capture: PropTypes.string,
intl: intlShape.isRequired,
loaded: PropTypes.bool,
onBack: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onCapture: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired
};
export default injectIntl(CameraModal);

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18px" height="8px" viewBox="0 0 18 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>re-record</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Desktop---1280x720" transform="translate(-446.000000, -454.000000)" fill="#4C97FF">
<g id="Step-4---Playing-Trim" transform="translate(0.000000, 42.000000)">
<g id="Re-record" transform="translate(434.000000, 400.000000)">
<path d="M23.6951773,18.0886272 L20.5085204,21.2806841 C20.2691162,21.5146883 19.9451104,21.6466907 19.6085044,21.6466907 C19.2730984,21.6466907 18.9490926,21.5146883 18.7084883,21.2806841 L15.5230315,18.0886272 C15.157025,17.7226207 15.049023,17.1766109 15.2470266,16.7026025 C15.4450301,16.228594 15.9010382,15.9225886 16.4170474,15.9225886 L17.5630679,15.9225886 C17.5330673,15.6465836 17.455066,15.3345781 17.3230636,15.0045722 C17.2816629,14.9085705 17.2390621,14.8125688 17.1910612,14.716567 C17.1250601,14.6085651 17.1316602,14.5785646 17.0410586,14.4465622 C16.897056,14.2305584 16.7650536,14.0685555 16.6084509,13.8825522 C16.2910452,13.5405461 15.9070383,13.2465408 15.4990311,13.030537 C15.0850237,12.8145331 14.653016,12.6825308 14.2690091,12.6105295 C13.8910024,12.5445283 13.5429962,12.5385282 13.3389925,12.5385282 C13.2369907,12.5325281 13.1049884,12.5565285 13.0389872,12.5625286 C12.9669859,12.5685287 12.9249852,12.5745288 12.9249852,12.5745288 C12.4989776,12.6165296 12.1149707,12.304524 12.07297,11.8785164 C12.0369693,11.51851 12.2469731,11.1945042 12.5649787,11.0745021 C12.5649787,11.0745021 12.6069795,11.0565018 12.6729807,11.0325013 C12.750982,11.0085009 12.8229833,10.9665001 12.9909863,10.9184993 C13.3269923,10.8164975 13.7529999,10.7084955 14.3110099,10.6544946 C14.8630197,10.6064937 15.5290316,10.6184939 16.2316441,10.7624965 C16.9330566,10.9124992 17.6710698,11.1945042 18.355082,11.6025115 C18.6790878,11.8125152 19.027094,12.0585196 19.2970988,12.2985239 C19.417101,12.3885255 19.6211046,12.5925292 19.7411067,12.7185314 C19.8791092,12.862534 20.0051114,13.0065365 20.1317137,13.1565392 C20.6171224,13.7565499 20.9771288,14.4045615 21.211133,14.992572 C21.3491354,15.328578 21.4451371,15.6465836 21.5171384,15.9225886 L22.8011613,15.9225886 C23.3171705,15.9225886 23.7731787,16.228594 23.9711822,16.7026025 C24.1691857,17.1766109 24.0611838,17.7226207 23.6951773,18.0886272" id="re-record" transform="translate(18.068963, 16.137926) scale(-1, 1) rotate(-45.000000) translate(-18.068963, -16.137926) "></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,306 @@
@import "../../css/units.css";
@import "../../css/colors.css";
@import "../../css/z-index.css";
.card-container-overlay {
position: fixed;
pointer-events: none;
z-index: $z-index-card;
}
.card-container {
position:absolute;
pointer-events: auto;
z-index: $z-index-card;
margin: 0.5rem 2rem;
min-width: 468px;
}
.left-card, .right-card {
height: 90%;
position: absolute;
top: 5%;
background: $ui-white;
border: 1px solid $ui-tertiary;
width: .75rem;
z-index: 10;
opacity: 0.9;
overflow: hidden;
}
.left-card {
left: -.75rem;
border-right: 0;
border-top-left-radius: 0.75rem;
border-bottom-left-radius: 0.75rem;
}
.right-card {
right: -.75rem;
border-left: 0;
border-top-right-radius: 0.75rem;
border-bottom-right-radius: 0.75rem;
}
.left-card::after, .right-card::after {
content: "";
position: absolute;
top: 0;
left: 0;
height: 2.5rem;
width: 100%;
background: $extensions-primary;
}
.left-button, .right-button {
position: absolute;
top: 50%;
margin-top: -15px;
z-index: 20;
user-select: none;
cursor: pointer;
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 {
left: -27px;
}
.right-button {
right: -27px;
}
.card {
border: 1px solid $ui-tertiary;
border-radius: 0.75rem;
display: flex;
flex-direction: column;
cursor: move;
z-index: 20;
overflow: hidden;
box-shadow: 0px 5px 25px 5px $ui-black-transparent;
align-items: center;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.header-buttons {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background: $extensions-primary;
border-bottom: 1px solid $extensions-tertiary;
font-size: 0.625rem;
font-weight: bold;
}
.header-buttons-hidden {
border-bottom: 0px;
}
.header-buttons-right {
display: flex;
flex-direction: row;
}
.header-buttons img {
margin-bottom: 2px;
}
.shrink-expand-button {
cursor: pointer;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0.75rem;
}
.shrink-expand-button:hover, .all-button:hover {
background-color: $ui-black-transparent;
}
.remove-button, .all-button {
cursor: pointer;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0.75rem;
}
.remove-button:hover, .all-button:hover {
background-color: $ui-black-transparent;
}
.step-title {
font-size: 0.9rem;
margin: 0.9rem;
text-align: center;
font-weight: bold;
color: $text-primary;
}
.step-body {
width: 100%;
background: $ui-white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
text-align: center;
/* Min height prevents layout changing when images change */
min-height: 256px;
}
.step-video {
height: 256px;
}
.step-image {
max-width: 450px;
max-height: 200px;
object-fit: contain;
background: #F9F9F9;
border: 1px solid #ddd;
border-radius: 0.5rem;
overflow: hidden;
margin: 0 0.5rem 0.5rem;
}
.decks {
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 0 1rem 0.5rem;
}
.deck {
display: flex;
flex-direction: column;
margin: 0 8px 8px;
cursor: pointer;
border: 1px solid $ui-black-transparent;
border-radius: 0.25rem;
overflow: hidden;
}
.deck-image {
width: 200px;
height: 100px;
object-fit: cover;
}
.deck-name {
color: $motion-primary;
font-weight: bold;
font-size: 0.85rem;
margin: .625rem 0px;
text-align: center;
font-weight: bold;
text-align: center;
max-width: 200px;
}
.help-icon {
height: 1.25rem;
}
.close-icon {
height: 1.25rem;
margin: .125rem 0; /* To offset the .25rem difference in icon size */
}
[dir="ltr"] .help-icon {
margin-right: 0.25rem;
}
[dir="rtl"] .help-icon {
margin-left: 0.25rem;
}
[dir="ltr"] .close-icon {
margin-left: 0.25rem;
}
[dir="rtl"] .close-icon {
margin-right: 0.25rem;
}
.see-all {
display: flex;
flex-direction: row;
justify-content: center;
width: 100%;
padding: 0.5rem;
}
.see-all-button {
cursor: pointer;
padding: 0.5rem 1rem;
background-color: $motion-primary;
color: white;
font-weight: bold;
border-radius: 0.25rem;
display: flex;
align-items: center;
color: $ui-white;
font-size: .75rem;
font-weight: bold;
line-height: 1rem;
text-align: center;
}
[dir="ltr"] .see-all-button img {
margin-left: 0.5rem;
}
[dir="rtl"] .see-all-button img {
margin-right: 0.5rem;
}
.steps-list {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.active-step-pip, .inactiveStepPip {
width: 0.5rem;
height: 0.5rem;
margin: 0 0.25rem;
border-radius: 100%;
background: $ui-white-transparent;
}
.active-step-pip {
background: $ui-white;
box-shadow: 0px 0px 0px 2px $ui-black-transparent;
}
.hidden {
display: none;
}

View File

@ -0,0 +1,440 @@
import PropTypes from 'prop-types';
import React, {Fragment} from 'react';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import Draggable from 'react-draggable';
import styles from './card.css';
import shrinkIcon from './icon--shrink.svg';
import expandIcon from './icon--expand.svg';
import rightArrow from './icon--next.svg';
import leftArrow from './icon--prev.svg';
import helpIcon from '../../lib/assets/icon--tutorials.svg';
import closeIcon from './icon--close.svg';
import {translateVideo} from '../../lib/libraries/decks/translate-video.js';
import {translateImage} from '../../lib/libraries/decks/translate-image.js';
const CardHeader = ({onCloseCards, onShrinkExpandCards, onShowAll, totalSteps, step, expanded}) => (
<div className={expanded ? styles.headerButtons : classNames(styles.headerButtons, styles.headerButtonsHidden)}>
<div
className={styles.allButton}
onClick={onShowAll}
>
<img
className={styles.helpIcon}
src={helpIcon}
/>
<FormattedMessage
defaultMessage="Tutorials"
description="Title for button to return to tutorials library"
id="gui.cards.all-tutorials"
/>
</div>
{totalSteps > 1 ? (
<div className={styles.stepsList}>
{Array(totalSteps).fill(0)
.map((_, i) => (
<div
className={i === step ? styles.activeStepPip : styles.inactiveStepPip}
key={`pip-step-${i}`}
/>
))}
</div>
) : null}
<div className={styles.headerButtonsRight}>
<div
className={styles.shrinkExpandButton}
onClick={onShrinkExpandCards}
>
<img
draggable={false}
src={expanded ? shrinkIcon : expandIcon}
/>
{expanded ?
<FormattedMessage
defaultMessage="Shrink"
description="Title for button to shrink how-to card"
id="gui.cards.shrink"
/> :
<FormattedMessage
defaultMessage="Expand"
description="Title for button to expand how-to card"
id="gui.cards.expand"
/>
}
</div>
<div
className={styles.removeButton}
onClick={onCloseCards}
>
<img
className={styles.closeIcon}
src={closeIcon}
/>
<FormattedMessage
defaultMessage="Close"
description="Title for button to close how-to card"
id="gui.cards.close"
/>
</div>
</div>
</div>
);
class VideoStep extends React.Component {
componentDidMount () {
const script = document.createElement('script');
script.src = `https://fast.wistia.com/embed/medias/${this.props.video}.jsonp`;
script.async = true;
script.setAttribute('id', 'wistia-video-content');
document.body.appendChild(script);
const script2 = document.createElement('script');
script2.src = 'https://fast.wistia.com/assets/external/E-v1.js';
script2.async = true;
script2.setAttribute('id', 'wistia-video-api');
document.body.appendChild(script2);
}
// We use the Wistia API here to update or pause the video dynamically:
// https://wistia.com/support/developers/player-api
componentDidUpdate (prevProps) {
// Ensure the wistia API is loaded and available
if (!(window.Wistia && window.Wistia.api)) return;
// Get a handle on the currently loaded video
const video = window.Wistia.api(prevProps.video);
// Reset the video source if a new video has been chosen from the library
if (prevProps.video !== this.props.video) {
video.replaceWith(this.props.video);
}
// Pause the video if the modal is being shrunken
if (!this.props.expanded) {
video.pause();
}
}
componentWillUnmount () {
const script = document.getElementById('wistia-video-content');
script.parentNode.removeChild(script);
const script2 = document.getElementById('wistia-video-api');
script2.parentNode.removeChild(script2);
}
render () {
return (
<div className={styles.stepVideo}>
<div
className={`wistia_embed wistia_async_${this.props.video}`}
id="video-div"
style={{height: `257px`, width: `466px`}}
>
&nbsp;
</div>
</div>
);
}
}
VideoStep.propTypes = {
expanded: PropTypes.bool.isRequired,
video: PropTypes.string.isRequired
};
const ImageStep = ({title, image}) => (
<Fragment>
<div className={styles.stepTitle}>
{title}
</div>
<div className={styles.stepImageContainer}>
<img
className={styles.stepImage}
draggable={false}
key={image} /* Use src as key to prevent hanging around on slow connections */
src={image}
/>
</div>
</Fragment>
);
ImageStep.propTypes = {
image: PropTypes.string.isRequired,
title: PropTypes.node.isRequired
};
const NextPrevButtons = ({isRtl, onNextStep, onPrevStep, expanded}) => (
<Fragment>
{onNextStep ? (
<div>
<div className={expanded ? (isRtl ? styles.leftCard : styles.rightCard) : styles.hidden} />
<div
className={expanded ? (isRtl ? styles.leftButton : styles.rightButton) : styles.hidden}
onClick={onNextStep}
>
<img
draggable={false}
src={isRtl ? leftArrow : rightArrow}
/>
</div>
</div>
) : null}
{onPrevStep ? (
<div>
<div className={expanded ? (isRtl ? styles.rightCard : styles.leftCard) : styles.hidden} />
<div
className={expanded ? (isRtl ? styles.rightButton : styles.leftButton) : styles.hidden}
onClick={onPrevStep}
>
<img
draggable={false}
src={isRtl ? rightArrow : leftArrow}
/>
</div>
</div>
) : null}
</Fragment>
);
NextPrevButtons.propTypes = {
expanded: PropTypes.bool.isRequired,
isRtl: PropTypes.bool,
onNextStep: PropTypes.func,
onPrevStep: PropTypes.func
};
CardHeader.propTypes = {
expanded: PropTypes.bool.isRequired,
onCloseCards: PropTypes.func.isRequired,
onShowAll: PropTypes.func.isRequired,
onShrinkExpandCards: PropTypes.func.isRequired,
step: PropTypes.number,
totalSteps: PropTypes.number
};
const PreviewsStep = ({deckIds, content, onActivateDeckFactory, onShowAll}) => (
<Fragment>
<div className={styles.stepTitle}>
<FormattedMessage
defaultMessage="More things to try!"
description="Title card with more things to try"
id="gui.cards.more-things-to-try"
/>
</div>
<div className={styles.decks}>
{deckIds.slice(0, 2).map(id => (
<div
className={styles.deck}
key={`deck-preview-${id}`}
onClick={onActivateDeckFactory(id)}
>
<img
className={styles.deckImage}
draggable={false}
src={content[id].img}
/>
<div className={styles.deckName}>{content[id].name}</div>
</div>
))}
</div>
<div className={styles.seeAll}>
<div
className={styles.seeAllButton}
onClick={onShowAll}
>
<FormattedMessage
defaultMessage="See more"
description="Title for button to see more in how-to library"
id="gui.cards.see-more"
/>
</div>
</div>
</Fragment>
);
PreviewsStep.propTypes = {
content: PropTypes.shape({
id: PropTypes.shape({
name: PropTypes.node.isRequired,
img: PropTypes.string.isRequired,
steps: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.node,
image: PropTypes.string,
video: PropTypes.string,
deckIds: PropTypes.arrayOf(PropTypes.string)
}))
})
}).isRequired,
deckIds: PropTypes.arrayOf(PropTypes.string).isRequired,
onActivateDeckFactory: PropTypes.func.isRequired,
onShowAll: PropTypes.func.isRequired
};
const Cards = props => {
const {
activeDeckId,
content,
dragging,
isRtl,
locale,
onActivateDeckFactory,
onCloseCards,
onShrinkExpandCards,
onDrag,
onStartDrag,
onEndDrag,
onShowAll,
onNextStep,
onPrevStep,
showVideos,
step,
expanded,
...posProps
} = props;
let {x, y} = posProps;
if (activeDeckId === null) return;
// Tutorial cards need to calculate their own dragging bounds
// to allow for dragging the cards off the left, right and bottom
// edges of the workspace.
const cardHorizontalDragOffset = 400; // ~80% of card width
const cardVerticalDragOffset = expanded ? 257 : 0; // ~80% of card height, if expanded
const menuBarHeight = 48; // TODO: get pre-calculated from elsewhere?
const wideCardWidth = 500;
if (x === 0 && y === 0) {
// initialize positions
x = isRtl ? (-190 - wideCardWidth - cardHorizontalDragOffset) : 292;
x += cardHorizontalDragOffset;
// The tallest cards are about 320px high, and the default position is pinned
// to near the bottom of the blocks palette to allow room to work above.
const tallCardHeight = 320;
const bottomMargin = 60; // To avoid overlapping the backpack region
y = window.innerHeight - tallCardHeight - bottomMargin - menuBarHeight;
}
const steps = content[activeDeckId].steps;
return (
// Custom overlay to act as the bounding parent for the draggable, using values from above
<div
className={styles.cardContainerOverlay}
style={{
width: `${window.innerWidth + (2 * cardHorizontalDragOffset)}px`,
height: `${window.innerHeight - menuBarHeight + cardVerticalDragOffset}px`,
top: `${menuBarHeight}px`,
left: `${-cardHorizontalDragOffset}px`
}}
>
<Draggable
bounds="parent"
cancel="#video-div" // disable dragging on video div
position={{x: x, y: y}}
onDrag={onDrag}
onStart={onStartDrag}
onStop={onEndDrag}
>
<div className={styles.cardContainer}>
<div className={styles.card}>
<CardHeader
expanded={expanded}
step={step}
totalSteps={steps.length}
onCloseCards={onCloseCards}
onShowAll={onShowAll}
onShrinkExpandCards={onShrinkExpandCards}
/>
<div className={expanded ? styles.stepBody : styles.hidden}>
{steps[step].deckIds ? (
<PreviewsStep
content={content}
deckIds={steps[step].deckIds}
onActivateDeckFactory={onActivateDeckFactory}
onShowAll={onShowAll}
/>
) : (
steps[step].video ? (
showVideos ? (
<VideoStep
dragging={dragging}
expanded={expanded}
video={translateVideo(steps[step].video, locale)}
/>
) : ( // Else show the deck image and title
<ImageStep
image={content[activeDeckId].img}
title={content[activeDeckId].name}
/>
)
) : (
<ImageStep
image={translateImage(steps[step].image, locale)}
title={steps[step].title}
/>
)
)}
{steps[step].trackingPixel && steps[step].trackingPixel}
</div>
<NextPrevButtons
expanded={expanded}
isRtl={isRtl}
onNextStep={step < steps.length - 1 ? onNextStep : null}
onPrevStep={step > 0 ? onPrevStep : null}
/>
</div>
</div>
</Draggable>
</div>
);
};
Cards.propTypes = {
activeDeckId: PropTypes.string.isRequired,
content: PropTypes.shape({
id: PropTypes.shape({
name: PropTypes.node.isRequired,
img: PropTypes.string.isRequired,
steps: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.node,
image: PropTypes.string,
video: PropTypes.string,
deckIds: PropTypes.arrayOf(PropTypes.string)
}))
})
}),
dragging: PropTypes.bool.isRequired,
expanded: PropTypes.bool.isRequired,
isRtl: PropTypes.bool.isRequired,
locale: PropTypes.string.isRequired,
onActivateDeckFactory: PropTypes.func.isRequired,
onCloseCards: PropTypes.func.isRequired,
onDrag: PropTypes.func,
onEndDrag: PropTypes.func,
onNextStep: PropTypes.func.isRequired,
onPrevStep: PropTypes.func.isRequired,
onShowAll: PropTypes.func,
onShrinkExpandCards: PropTypes.func.isRequired,
onStartDrag: PropTypes.func,
showVideos: PropTypes.bool,
step: PropTypes.number.isRequired,
x: PropTypes.number,
y: PropTypes.number
};
Cards.defaultProps = {
showVideos: true
};
export {
Cards as default,
// Others exported for testability
ImageStep,
VideoStep
};

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
<title>Extensions/Connection/Close</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M15.464935,15.467297 C14.7644059,16.1777705 13.6185877,16.1777705 12.9081142,15.467297 L9.99881899,12.5580018 L7.08841885,15.467297 C6.38236506,16.1733508 5.23765187,16.1733508 4.53159807,15.467297 C4.17912364,15.1148226 4.00012409,14.6485398 4.00012409,14.1888866 C4.00012409,13.7281285 4.17912364,13.2629506 4.53159807,12.9104762 L7.44089328,10.001181 L4.52717833,7.08636112 C4.17359897,6.73278176 3.99459941,6.26760391 4.00012409,5.80242606 C4.00012409,5.34166795 4.17359897,4.88201477 4.52717833,4.52954034 C5.23212719,3.82348655 6.37684038,3.82348655 7.08399911,4.52954034 L9.99881899,7.44325529 L12.9125339,4.52954034 C13.6185877,3.82348655 14.7644059,3.82348655 15.4704597,4.52954034 C16.1765134,5.2344892 16.1765134,6.38030733 15.4704597,7.08636112 L12.5545348,10.001181 L15.4704597,12.914896 C16.1765134,13.6209497 16.1765134,14.7557185 15.464935,15.467297" id="path-1"></path>
</defs>
<g id="Extensions/Connection/Close" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="close" fill="#FFFFFF" xlink:href="#path-1"></use>
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
<title>Tutorials/Expand</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M4.5,2 L15.5,2 C16.8807119,2 18,3.11928813 18,4.5 L18,5.5 C18,5.77614237 17.7761424,6 17.5,6 L2.5,6 C2.22385763,6 2,5.77614237 2,5.5 L2,4.5 C2,3.11928813 3.11928813,2 4.5,2 Z M2.5,7 L17.5,7 C17.7761424,7 18,7.22385763 18,7.5 L18,15.5 C18,16.8807119 16.8807119,18 15.5,18 L4.5,18 C3.11928813,18 2,16.8807119 2,15.5 L2,7.5 C2,7.22385763 2.22385763,7 2.5,7 Z M9.65509466,15.1636146 C9.745416,15.2533569 9.86700241,15.3031495 9.99727357,15.3031495 C10.1275447,15.3031495 10.2491311,15.2533569 10.3394525,15.1636146 L12.1574588,13.3456082 C12.3004676,13.2025994 12.3415754,12.9993764 12.2657286,12.8158388 C12.1898819,12.6334592 12.0109762,12.5153467 11.8100691,12.5153467 L11.1506075,12.5153467 L10.7899012,9.98808625 C10.7464775,9.64474938 10.4703026,9.36567961 10.1124912,9.30951827 C10.0783312,9.30546539 10.0435922,9.30314946 10.0094322,9.30314946 C9.61282891,9.30314946 9.27586085,9.59785176 9.22548933,9.98519134 L8.8740467,12.5153467 L8.18447804,12.5153467 C7.9858869,12.5153467 7.81450796,12.6265114 7.73692425,12.8065751 C7.65876155,12.9866388 7.69639544,13.1962306 7.83708829,13.3456082 L9.65509466,15.1636146 Z" id="path-1"></path>
</defs>
<g id="Tutorials/Expand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Combined-Shape" fill-rule="nonzero"></g>
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.2 (51160) - http://www.bohemiancoding.com/sketch -->
<title>Next</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Next" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M10.1575922,16.994103 C9.84650021,16.994103 9.55614768,16.8751967 9.34045723,16.6608889 L4.99899559,12.3194272 C4.66301623,11.9627084 4.57314521,11.4621959 4.75980041,11.0321977 C4.94507298,10.6021994 5.35433178,10.3367342 5.82857425,10.3367342 L7.47528788,10.3367342 L8.31454496,4.29463633 C8.43483386,3.36965613 9.23952516,2.6658969 10.1866275,2.6658969 C10.2682027,2.6658969 10.3511606,2.67142742 10.4327358,2.68110584 C11.2872018,2.81522106 11.9467168,3.48164924 12.0504142,4.30154948 L12.9117934,10.3367342 L14.4866102,10.3367342 C14.9663832,10.3367342 15.3936162,10.618791 15.5747408,11.0543198 C15.7558655,11.4926138 15.6576987,11.9779173 15.3161888,12.3194272 L10.9747272,16.6608889 C10.7590367,16.8751967 10.4686842,16.994103 10.1575922,16.994103" id="Fill-1" fill="#FFFFFF" transform="translate(10.164103, 9.830000) rotate(-90.000000) translate(-10.164103, -9.830000) "></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.2 (51160) - http://www.bohemiancoding.com/sketch -->
<title>Previous</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Previous" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M10.1575922,16.994103 C9.84650021,16.994103 9.55614768,16.8751967 9.34045723,16.6608889 L4.99899559,12.3194272 C4.66301623,11.9627084 4.57314521,11.4621959 4.75980041,11.0321977 C4.94507298,10.6021994 5.35433178,10.3367342 5.82857425,10.3367342 L7.47528788,10.3367342 L8.31454496,4.29463633 C8.43483386,3.36965613 9.23952516,2.6658969 10.1866275,2.6658969 C10.2682027,2.6658969 10.3511606,2.67142742 10.4327358,2.68110584 C11.2872018,2.81522106 11.9467168,3.48164924 12.0504142,4.30154948 L12.9117934,10.3367342 L14.4866102,10.3367342 C14.9663832,10.3367342 15.3936162,10.618791 15.5747408,11.0543198 C15.7558655,11.4926138 15.6576987,11.9779173 15.3161888,12.3194272 L10.9747272,16.6608889 C10.7590367,16.8751967 10.4686842,16.994103 10.1575922,16.994103" id="Fill-1" fill="#FFFFFF" transform="translate(10.164103, 9.830000) rotate(-270.000000) translate(-10.164103, -9.830000) "></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
<title>Tutorials/Shink</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M2.5,8 C2.22385763,8 2,7.77614237 2,7.5 C2,7.22385763 2.22385763,7 2.5,7 L3.5,7 C3.77614237,7 4,7.22385763 4,7.5 C4,7.77614237 3.77614237,8 3.5,8 L2.5,8 Z M5.5,8 C5.22385763,8 5,7.77614237 5,7.5 C5,7.22385763 5.22385763,7 5.5,7 L6.5,7 C6.77614237,7 7,7.22385763 7,7.5 C7,7.77614237 6.77614237,8 6.5,8 L5.5,8 Z M8.5,8 C8.22385763,8 8,7.77614237 8,7.5 C8,7.22385763 8.22385763,7 8.5,7 L9.5,7 C9.77614237,7 10,7.22385763 10,7.5 C10,7.77614237 9.77614237,8 9.5,8 L8.5,8 Z M11.5,8 C11.2238576,8 11,7.77614237 11,7.5 C11,7.22385763 11.2238576,7 11.5,7 L12.5,7 C12.7761424,7 13,7.22385763 13,7.5 C13,7.77614237 12.7761424,8 12.5,8 L11.5,8 Z M14.5,8 C14.2238576,8 14,7.77614237 14,7.5 C14,7.22385763 14.2238576,7 14.5,7 L15.5,7 C15.7761424,7 16,7.22385763 16,7.5 C16,7.77614237 15.7761424,8 15.5,8 L14.5,8 Z M17,7.5 C17,7.22385763 17.2238576,7 17.5,7 C17.7761424,7 18,7.22385763 18,7.5 L18,8.5 C18,8.77614237 17.7761424,9 17.5,9 C17.2238576,9 17,8.77614237 17,8.5 L17,7.5 Z M17,10.5 C17,10.2238576 17.2238576,10 17.5,10 C17.7761424,10 18,10.2238576 18,10.5 L18,11.5 C18,11.7761424 17.7761424,12 17.5,12 C17.2238576,12 17,11.7761424 17,11.5 L17,10.5 Z M17,13.5 C17,13.2238576 17.2238576,13 17.5,13 C17.7761424,13 18,13.2238576 18,13.5 L18,14.5 C18,14.7761424 17.7761424,15 17.5,15 C17.2238576,15 17,14.7761424 17,14.5 L17,13.5 Z M16.8130884,16.2258789 C16.9469778,15.9843665 17.2513011,15.8971208 17.4928135,16.0310102 C17.7343259,16.1648996 17.8215716,16.4692229 17.6876822,16.7107353 C17.4827416,17.0804113 17.1867239,17.3929429 16.8289716,17.6178178 C16.59518,17.7647739 16.286523,17.6943798 16.1395669,17.4605882 C15.9926108,17.2267966 16.0630049,16.9181396 16.2967965,16.7711835 C16.5118233,16.6360225 16.6900031,16.4479027 16.8130884,16.2258789 Z M14.5614675,17 C14.8376098,17 15.0614675,17.2238576 15.0614675,17.5 C15.0614675,17.7761424 14.8376098,18 14.5614675,18 L13.5614675,18 C13.2853251,18 13.0614675,17.7761424 13.0614675,17.5 C13.0614675,17.2238576 13.2853251,17 13.5614675,17 L14.5614675,17 Z M11.5614675,17 C11.8376098,17 12.0614675,17.2238576 12.0614675,17.5 C12.0614675,17.7761424 11.8376098,18 11.5614675,18 L10.5614675,18 C10.2853251,18 10.0614675,17.7761424 10.0614675,17.5 C10.0614675,17.2238576 10.2853251,17 10.5614675,17 L11.5614675,17 Z M8.56146746,17 C8.83760983,17 9.06146746,17.2238576 9.06146746,17.5 C9.06146746,17.7761424 8.83760983,18 8.56146746,18 L7.56146746,18 C7.28532508,18 7.06146746,17.7761424 7.06146746,17.5 C7.06146746,17.2238576 7.28532508,17 7.56146746,17 L8.56146746,17 Z M5.56146746,17 C5.83760983,17 6.06146746,17.2238576 6.06146746,17.5 C6.06146746,17.7761424 5.83760983,18 5.56146746,18 L4.56146746,18 C4.28532508,18 4.06146746,17.7761424 4.06146746,17.5 C4.06146746,17.2238576 4.28532508,17 4.56146746,17 L5.56146746,17 Z M3.22779886,16.2951753 C3.37445671,16.5291541 3.30366909,16.8377211 3.06969027,16.9843789 C2.83571145,17.1310368 2.52714447,17.0602491 2.38048662,16.8262703 C2.15682122,16.4694332 2.02609629,16.0609535 2.00352115,15.6335963 C1.98895424,15.3578384 2.20069137,15.1224836 2.47644927,15.1079167 C2.75220716,15.0933498 2.98756192,15.3050869 3.00212882,15.5808448 C3.01567642,15.8373067 3.0937811,16.0813625 3.22779886,16.2951753 Z M3,13.6229349 C3,13.8990773 2.77614237,14.1229349 2.5,14.1229349 C2.22385763,14.1229349 2,13.8990773 2,13.6229349 L2,12.6229349 C2,12.3467925 2.22385763,12.1229349 2.5,12.1229349 C2.77614237,12.1229349 3,12.3467925 3,12.6229349 L3,13.6229349 Z M3,10.6229349 C3,10.8990773 2.77614237,11.1229349 2.5,11.1229349 C2.22385763,11.1229349 2,10.8990773 2,10.6229349 L2,9.62293492 C2,9.34679254 2.22385763,9.12293492 2.5,9.12293492 C2.77614237,9.12293492 3,9.34679254 3,9.62293492 L3,10.6229349 Z M3,7.62293492 C3,7.89907729 2.77614237,8.12293492 2.5,8.12293492 C2.22385763,8.12293492 2,7.89907729 2,7.62293492 L2,7.5 C2,7.22385763 2.22385763,7 2.5,7 C2.77614237,7 3,7.22385763 3,7.5 L3,7.62293492 Z M4.5,2 L15.5,2 C16.8807119,2 18,3.11928813 18,4.5 L18,5.5 C18,5.77614237 17.7761424,6 17.5,6 L2.5,6 C2.22385763,6 2,5.77614237 2,5.5 L2,4.5 C2,3.11928813 3.11928813,2 4.5,2 Z M10.3449053,9.44268434 L12.1629117,11.2606907 C12.3036046,11.4100683 12.3412384,11.6196601 12.2630758,11.7997238 C12.185492,11.9797875 12.0141131,12.0909522 11.815522,12.0909522 L11.1259533,12.0909522 L10.7745107,14.6211076 C10.7241392,15.0084472 10.3871711,15.3031495 9.99056779,15.3031495 C9.9564078,15.3031495 9.92166882,15.3008335 9.88750883,15.2967806 C9.52969739,15.2406193 9.25352253,14.9615495 9.21009882,14.6182127 L8.84939246,12.0909522 L8.18993091,12.0909522 C7.98902384,12.0909522 7.81011812,11.9728397 7.73427135,11.7904601 C7.65842459,11.6069225 7.69953238,11.4036995 7.84254116,11.2606907 L9.66054753,9.44268434 C9.75086886,9.35294199 9.87245528,9.30314946 10.0027264,9.30314946 C10.1329976,9.30314946 10.254584,9.35294199 10.3449053,9.44268434 Z" id="path-1"></path>
</defs>
<g id="Tutorials/Shink" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Rectangle" fill-rule="nonzero"></g>
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1,81 @@
@import "../../css/colors.css";
@import "../../css/units.css";
.close-button {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden; /* Mask the icon animation */
background-color: $ui-black-transparent;
border-radius: 50%;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
user-select: none;
cursor: pointer;
transition: all 0.15s ease-out;
}
.close-button.large:hover {
transform: scale(1.1, 1.1);
box-shadow: 0 0 0 4px $ui-black-transparent;
}
.close-button.large.orange:hover {
transform: scale(1.1, 1.1);
box-shadow: 0px 0px 0px 4px hsla(29, 100%, 54%, 0.2);
}
.small {
width: 0.825rem;
height: 0.825rem;
background-color: $motion-primary;
color: $ui-white;
}
.large {
width: 1.75rem;
height: 1.75rem;
box-shadow: 0 0 0 2px $ui-black-transparent;
}
.large.orange {
background-color: hsla(29, 100%, 54%, 0.2);
box-shadow: 0px 0px 0px 2px hsla(29, 100%, 54%, 0.2);
}
.close-icon {
position: relative;
margin: 0.25rem;
user-select: none;
transform-origin: 50%;
transform: rotate(45deg);
}
.close-icon.orange {
transform: rotate(45deg);
transform: scale(1.4);
}
.small .close-icon {
width: 50%;
}
.large .close-icon {
width: 0.75rem;
height: 0.75rem;
}
.back-icon {
position: relative;
margin: 0.25rem;
user-select: none;
}
.small .back-icon {
width: 50%;
}
.large .back-icon {
width: 2rem;
height: 2rem;
}

View File

@ -0,0 +1,76 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import styles from './close-button.css';
import closeIcon from './icon--close.svg';
import closeIconOrange from './icon--close-orange.svg';
import backIcon from '../../lib/assets/icon--back.svg';
let closeIcons = {};
const CloseButton = props => (
<div
aria-label="Close"
className={classNames(
styles.closeButton,
props.className,
{
[styles.small]: props.size === CloseButton.SIZE_SMALL,
[styles.large]: props.size === CloseButton.SIZE_LARGE,
[styles.orange]: props.color === CloseButton.COLOR_ORANGE
}
)}
role="button"
tabIndex="0"
onClick={props.onClick}
>
{props.buttonType === 'back' ?
<img
className={styles.backIcon}
src={backIcon}
/> :
<img
className={classNames(
styles.closeIcon,
{
[styles[props.color]]: (props.color !== CloseButton.COLOR_NEUTRAL)
}
)}
src={(props.color && closeIcons[props.color]) ?
closeIcons[props.color] :
closeIcon
}
/>
}
</div>
);
CloseButton.SIZE_SMALL = 'small';
CloseButton.SIZE_LARGE = 'large';
CloseButton.COLOR_NEUTRAL = 'neutral';
CloseButton.COLOR_GREEN = 'green';
CloseButton.COLOR_ORANGE = 'orange';
closeIcons = {
[CloseButton.COLOR_NEUTRAL]: closeIcon,
[CloseButton.COLOR_GREEN]: closeIcon, // TODO: temporary, need green icon
[CloseButton.COLOR_ORANGE]: closeIconOrange
};
CloseButton.propTypes = {
buttonType: PropTypes.oneOf(['back', 'close']),
className: PropTypes.string,
color: PropTypes.string,
onClick: PropTypes.func.isRequired,
size: PropTypes.oneOf([CloseButton.SIZE_SMALL, CloseButton.SIZE_LARGE])
};
CloseButton.defaultProps = {
color: CloseButton.COLOR_NEUTRAL,
size: CloseButton.SIZE_LARGE,
buttonType: 'close'
};
export default CloseButton;

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Icon</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M15.464935,15.467297 C14.7644059,16.1777705 13.6185877,16.1777705 12.9081142,15.467297 L9.99881899,12.5580018 L7.08841885,15.467297 C6.38236506,16.1733508 5.23765187,16.1733508 4.53159807,15.467297 C4.17912364,15.1148226 4.00012409,14.6485398 4.00012409,14.1888866 C4.00012409,13.7281285 4.17912364,13.2629506 4.53159807,12.9104762 L7.44089328,10.001181 L4.52717833,7.08636112 C4.17359897,6.73278176 3.99459941,6.26760391 4.00012409,5.80242606 C4.00012409,5.34166795 4.17359897,4.88201477 4.52717833,4.52954034 C5.23212719,3.82348655 6.37684038,3.82348655 7.08399911,4.52954034 L9.99881899,7.44325529 L12.9125339,4.52954034 C13.6185877,3.82348655 14.7644059,3.82348655 15.4704597,4.52954034 C16.1765134,5.2344892 16.1765134,6.38030733 15.4704597,7.08636112 L12.5545348,10.001181 L15.4704597,12.914896 C16.1765134,13.6209497 16.1765134,14.7557185 15.464935,15.467297" id="path-1"></path>
</defs>
<g id="Icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Extensions/Connection/Close">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="close" fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
<g id="Color/Tangerine/1_Tangerine" mask="url(#mask-2)" fill="#FF8C1A" fill-rule="evenodd">
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7.48 7.48"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><title>icon--add</title><line class="cls-1" x1="3.74" y1="6.48" x2="3.74" y2="1"/><line class="cls-1" x1="1" y1="3.74" x2="6.48" y2="3.74"/></svg>

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,77 @@
/*
* NOTE: the copious use of `important` is needed to overwrite
* the default tooltip styling, and is required by the 3rd party
* library being used, `react-tooltip`
*/
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/z-index.css";
.coming-soon {
background-color: $data-primary !important;
border: 1px solid $ui-black-transparent !important;
border-radius: $form-radius !important;
box-shadow: 0 0 .5rem $ui-black-transparent !important;
padding: .75rem 1rem !important;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
font-size: 1rem !important;
line-height: 1.25rem !important;
z-index: $z-index-tooltip !important;
}
.coming-soon:after {
content: "";
border-top: 1px solid $ui-black-transparent !important;
border-left: 0 !important;
border-bottom: 0 !important;
border-right: 1px solid $ui-black-transparent !important;
border-radius: $form-radius;
background-color: $data-primary !important;
height: 1rem !important;
width: 1rem !important;
}
.show,
.show:before,
.show:after {
opacity: 1 !important;
}
.left:after {
margin-top: -.5rem !important;
right: -.5rem !important;
transform: rotate(45deg) !important;
}
.right:after {
margin-top: -.5rem !important;
left: -.5rem !important;
transform: rotate(-135deg) !important;
}
.top:after {
margin-right: -.5rem !important;
bottom: -.5rem !important;
transform: rotate(135deg) !important;
}
.bottom:after {
margin-left: -.5rem !important;
top: -.5rem !important;
transform: rotate(-45deg) !important;
}
.coming-soon-image {
width: 1.25rem;
height: 1.25rem;
vertical-align: middle;
}
[dir="ltr"] .coming-soon-image {
margin-left: .125rem;
}
[dir="rtl"] .coming-soon-image {
margin-right: .125rem;
}

View File

@ -0,0 +1,143 @@
import bindAll from 'lodash.bindall';
import classNames from 'classnames';
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import ReactTooltip from 'react-tooltip';
import styles from './coming-soon.css';
import awwCatIcon from './aww-cat.png';
import coolCatIcon from './cool-cat.png';
const messages = defineMessages({
message1: {
defaultMessage: 'Don\'t worry, we\'re on it {emoji}',
description: 'One of the "coming soon" random messages for yet-to-be-done features',
id: 'gui.comingSoon.message1'
},
message2: {
defaultMessage: 'Coming Soon...',
description: 'One of the "coming soon" random messages for yet-to-be-done features',
id: 'gui.comingSoon.message2'
},
message3: {
defaultMessage: 'We\'re working on it {emoji}',
description: 'One of the "coming soon" random messages for yet-to-be-done features',
id: 'gui.comingSoon.message3'
}
});
class ComingSoonContent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'setHide',
'setShow',
'getRandomMessage'
]);
this.state = {
isShowing: false
};
}
setShow () {
// needed to set the opacity to 1, since the default is .9 on show
this.setState({isShowing: true});
}
setHide () {
this.setState({isShowing: false});
}
getRandomMessage () {
// randomly chooses a messages from `messages` to display in the tooltip.
const images = [awwCatIcon, coolCatIcon];
const messageNumber = Math.floor(Math.random() * Object.keys(messages).length) + 1;
const imageNumber = Math.floor(Math.random() * Object.keys(images).length);
return (
<FormattedMessage
{...messages[`message${messageNumber}`]}
values={{
emoji: (
<img
className={styles.comingSoonImage}
src={images[imageNumber]}
/>
)
}}
/>
);
}
render () {
return (
<ReactTooltip
afterHide={this.setHide}
afterShow={this.setShow}
className={classNames(
styles.comingSoon,
this.props.className,
{
[styles.show]: (this.state.isShowing),
[styles.left]: (this.props.place === 'left'),
[styles.right]: (this.props.place === 'right'),
[styles.top]: (this.props.place === 'top'),
[styles.bottom]: (this.props.place === 'bottom')
}
)}
getContent={this.getRandomMessage}
id={this.props.tooltipId}
/>
);
}
}
ComingSoonContent.propTypes = {
className: PropTypes.string,
intl: intlShape,
place: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
tooltipId: PropTypes.string.isRequired
};
ComingSoonContent.defaultProps = {
place: 'bottom'
};
const ComingSoon = injectIntl(ComingSoonContent);
const ComingSoonTooltip = props => (
<div className={props.className}>
<div
data-delay-hide={props.delayHide}
data-delay-show={props.delayShow}
data-effect="solid"
data-for={props.tooltipId}
data-place={props.place}
data-tip="tooltip"
>
{props.children}
</div>
<ComingSoon
className={props.tooltipClassName}
place={props.place}
tooltipId={props.tooltipId}
/>
</div>
);
ComingSoonTooltip.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
delayHide: PropTypes.number,
delayShow: PropTypes.number,
place: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
tooltipClassName: PropTypes.string,
tooltipId: PropTypes.string.isRequired
};
ComingSoonTooltip.defaultProps = {
delayHide: 0,
delayShow: 0
};
export {
ComingSoon as ComingSoonComponent,
ComingSoonTooltip
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,158 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import keyMirror from 'keymirror';
import classNames from 'classnames';
import Box from '../box/box.jsx';
import Dots from './dots.jsx';
import closeIcon from '../close-button/icon--close.svg';
import radarIcon from './icons/searching.png';
import bluetoothIcon from './icons/bluetooth-white.svg';
import backIcon from './icons/back.svg';
import styles from './connection-modal.css';
const PHASES = keyMirror({
prescan: null,
pressbutton: null,
notfound: null
});
const AutoScanningStep = props => (
<Box className={styles.body}>
<Box className={styles.activityArea}>
<div className={styles.activityAreaInfo}>
<div className={styles.centeredRow}>
{props.phase === PHASES.prescan && (
<React.Fragment>
<img
className={styles.radarBig}
src={radarIcon}
/>
<img
className={styles.bluetoothCenteredIcon}
src={bluetoothIcon}
/>
</React.Fragment>
)}
{props.phase === PHASES.pressbutton && (
<React.Fragment>
<img
className={classNames(styles.radarBig, styles.radarSpin)}
src={radarIcon}
/>
<img
className={styles.connectionTipIcon}
src={props.connectionTipIconURL}
/>
</React.Fragment>
)}
{props.phase === PHASES.notfound && (
<Box className={styles.instructions}>
<FormattedMessage
defaultMessage="No devices found"
description="Text shown when no devices could be found"
id="gui.connection.auto-scanning.noPeripheralsFound"
/>
</Box>
)}
</div>
</div>
</Box>
<Box className={styles.bottomArea}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
{props.phase === PHASES.prescan && (
<FormattedMessage
defaultMessage="Have your device nearby, then begin searching."
description="Prompt for beginning the search"
id="gui.connection.auto-scanning.prescan"
/>
)}
{props.phase === PHASES.pressbutton && (
<FormattedMessage
defaultMessage="Press the button on your device."
description="Prompt for pushing the button on the device"
id="gui.connection.auto-scanning.pressbutton"
/>
)}
</Box>
<Dots
className={styles.bottomAreaItem}
counter={0}
total={3}
/>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
{props.phase === PHASES.prescan && (
<button
className={styles.connectionButton}
onClick={props.onStartScan}
>
<FormattedMessage
defaultMessage="Start Searching"
description="Button in prompt for starting a search"
id="gui.connection.auto-scanning.start-search"
/>
</button>
)}
{props.phase === PHASES.pressbutton && (
<div className={styles.segmentedButton}>
<button
disabled
className={styles.connectionButton}
>
<FormattedMessage
defaultMessage="Searching..."
description="Label indicating that search is in progress"
id="gui.connection.connecting-searchbutton"
/>
</button>
<button
className={styles.connectionButton}
onClick={props.onRefresh}
>
<img
className={styles.abortConnectingIcon}
src={closeIcon}
/>
</button>
</div>
)}
{props.phase === PHASES.notfound && (
<button
className={styles.connectionButton}
onClick={props.onRefresh}
>
<img
className={styles.buttonIconLeft}
src={backIcon}
/>
<FormattedMessage
defaultMessage="Try again"
description="Button in prompt for trying a device search again"
id="gui.connection.auto-scanning.try-again"
/>
</button>
)}
</Box>
</Box>
</Box>
);
AutoScanningStep.propTypes = {
connectionTipIconURL: PropTypes.string,
onRefresh: PropTypes.func,
onStartScan: PropTypes.func,
phase: PropTypes.oneOf(Object.keys(PHASES))
};
AutoScanningStep.defaultProps = {
phase: PHASES.prescan
};
export {
AutoScanningStep as default,
PHASES
};

View File

@ -0,0 +1,72 @@
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.peripheralActivity}>
<img
className={styles.peripheralActivityIcon}
src={props.connectionIconURL}
/>
<img
className={styles.bluetoothConnectedIcon}
src={bluetoothIcon}
/>
</div>
</Box>
</Box>
<Box className={styles.bottomArea}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
<FormattedMessage
defaultMessage="Connected"
description="Message indicating that a device was connected"
id="gui.connection.connected"
/>
</Box>
<Dots
success
className={styles.bottomAreaItem}
total={3}
/>
<div className={classNames(styles.bottomAreaItem, 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 = {
connectionIconURL: PropTypes.string.isRequired,
onCancel: PropTypes.func,
onDisconnect: PropTypes.func
};
export default ConnectedStep;

View File

@ -0,0 +1,70 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
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.peripheralActivity}>
<img
className={styles.peripheralActivityIcon}
src={props.connectionIconURL}
/>
<img
className={styles.bluetoothConnectingIcon}
src={bluetoothIcon}
/>
</div>
</Box>
</Box>
<Box className={styles.bottomArea}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
{props.connectingMessage}
</Box>
<Dots
className={styles.bottomAreaItem}
counter={1}
total={3}
/>
<div className={classNames(styles.bottomAreaItem, 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 = {
connectingMessage: PropTypes.node.isRequired,
connectionIconURL: PropTypes.string.isRequired,
onDisconnect: PropTypes.func
};
export default ConnectingStep;

View File

@ -0,0 +1,425 @@
@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;
}
.peripheral-tile-pane {
overflow-y: auto;
width: 100%;
height: 100%;
}
.peripheral-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;
}
.peripheral-tile-name {
display: flex;
align-items: center;
}
[dir="ltr"] .peripheral-tile-image {
margin-right: 0.5rem;
}
[dir="rtl"] .peripheral-tile-image {
margin-left: 0.5rem;
}
.peripheral-tile-name-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.peripheral-tile-name-label {
font-weight: bold;
font-size: 0.625rem;
}
.peripheral-tile-name-text {
font-size: 0.875rem;
}
.peripheral-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;
}
[dir="ltr"] .signal-strength-meter {
margin-right: 1rem;
}
[dir="rtl"] .signal-strength-meter {
margin-left: 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-small {
width: 40px;
height: 40px;
}
[dir="ltr"] .radar-small {
margin-right: 0.5rem;
}
[dir="rtl"] .radar-small {
margin-left: 0.5rem;
}
.radar-big {
width: 120px;
height: 120px;
}
.radar-spin {
animation: spin 4s linear infinite;
}
[dir="ltr"] .radar {
margin-right: .5rem;
}
[dir="rtl"] .radar {
margin-left: .5rem;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
.peripheral-activity {
position: relative;
}
.peripheral-activity-icon {
/* width: 80px;
height: 80px; */
}
.connection-tip-icon {
position: absolute;
}
.bluetooth-connecting-icon {
position: absolute;
top: -5px;
right: -15px;
left: -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;
left: -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);}
}
.bluetooth-centered-icon {
position: absolute;
padding: 5px 5px;
background-color: $motion-primary;
border-radius: 100%;
box-shadow: 0px 0px 0px 2px $motion-transparent;
}
.peripheral-tile-widgets {
display: flex;
align-items: center;
}
.activityArea {
height: 165px;
background-color: $motion-light-transparent;
display: flex;
justify-content: center;
align-items: center;
padding: .5rem;
}
.scratch-link-help {
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
padding-top: .5rem;
padding-bottom: .5rem;
}
.scratch-link-help-step {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
[dir="ltr"] .scratch-link-help-step {
margin-left: 2.5rem;
}
[dir="rtl"] .scratch-link-help-step {
margin-right: 2.5rem;
}
.scratch-link-icon {
max-width: 50px;
}
[dir="ltr"] .help-step-image {
margin-right: 0.5rem;
}
[dir="rtl"] .help-step-image {
margin-left: 0.5rem;
}
.help-step-number {
background: $pen-primary;
border-radius: 100%;
display: flex;
justify-content: center;
align-items: center;
color: $ui-white;
font-weight: bold;
min-width: 2rem;
height: 2rem;
}
[dir="ltr"] .help-step-number {
margin-right: 0.5rem;
}
[dir="rtl"] .help-step-number {
margin-left: 0.5rem;
}
.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;
}
[dir="ltr"] .button-icon-right {
margin-left: 0.5rem;
}
[dir="rtl"] .button-icon-right {
margin-right: 0.5rem;
}
[dir="ltr"] .button-icon-left {
margin-right: 0.5rem;
}
[dir="rtl"] .button-icon-left {
margin-left: 0.5rem;
}
/* reverse back arrow icon for RTL, don't reverse other connection icons */
[dir="rtl"] .button-icon-back {
transform: scaleX(-1);
}
.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-top: 1rem;
padding-bottom: .75rem;
padding-left: .75rem;
padding-right: .75rem;
}
.bottom-area .bottom-area-item+.bottom-area-item {
margin-top: 1rem;
}
.instructions {
text-align: center;
}
.dots-row {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.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;
}

View File

@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
import React from 'react';
import keyMirror from 'keymirror';
import Box from '../box/box.jsx';
import Modal from '../../containers/modal.jsx';
import ScanningStep from '../../containers/scanning-step.jsx';
import AutoScanningStep from '../../containers/auto-scanning-step.jsx';
import ConnectingStep from './connecting-step.jsx';
import ConnectedStep from './connected-step.jsx';
import ErrorStep from './error-step.jsx';
import UnavailableStep from './unavailable-step.jsx';
import styles from './connection-modal.css';
const PHASES = keyMirror({
scanning: null,
connecting: null,
connected: null,
error: null,
unavailable: null
});
const ConnectionModalComponent = props => (
<Modal
className={styles.modalContent}
contentLabel={props.name}
headerClassName={styles.header}
headerImage={props.connectionSmallIconURL}
id="connectionModal"
onHelp={props.onHelp}
onRequestClose={props.onCancel}
>
<Box className={styles.body}>
{props.phase === PHASES.scanning && !props.useAutoScan && <ScanningStep {...props} />}
{props.phase === PHASES.scanning && props.useAutoScan && <AutoScanningStep {...props} />}
{props.phase === PHASES.connecting && <ConnectingStep {...props} />}
{props.phase === PHASES.connected && <ConnectedStep {...props} />}
{props.phase === PHASES.error && <ErrorStep {...props} />}
{props.phase === PHASES.unavailable && <UnavailableStep {...props} />}
</Box>
</Modal>
);
ConnectionModalComponent.propTypes = {
connectingMessage: PropTypes.node.isRequired,
connectionSmallIconURL: PropTypes.string,
connectionTipIconURL: PropTypes.string,
name: PropTypes.node,
onCancel: PropTypes.func.isRequired,
onHelp: PropTypes.func.isRequired,
phase: PropTypes.oneOf(Object.keys(PHASES)).isRequired,
title: PropTypes.string.isRequired,
useAutoScan: PropTypes.bool.isRequired
};
ConnectionModalComponent.defaultProps = {
connectingMessage: 'Connecting'
};
export {
ConnectionModalComponent as default,
PHASES
};

View File

@ -0,0 +1,65 @@
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={classNames(
props.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 = {
className: PropTypes.string,
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;

View File

@ -0,0 +1,78 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import classNames from 'classnames';
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.peripheralActivity}>
<img
className={styles.peripheralActivityIcon}
src={props.connectionIconURL}
/>
</div>
</Box>
</Box>
<Box className={styles.bottomArea}>
<div className={classNames(styles.bottomAreaItem, styles.instructions)}>
<FormattedMessage
defaultMessage="Oops, looks like something went wrong."
description="The device connection process has encountered an error."
id="gui.connection.error.errorMessage"
/>
</div>
<Dots
error
className={styles.bottomAreaItem}
total={3}
/>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
<button
className={styles.connectionButton}
onClick={props.onScanning}
>
<img
className={classNames(styles.buttonIconLeft, styles.buttonIconBack)}
src={backIcon}
/>
<FormattedMessage
defaultMessage="Try again"
description="Button to initiate trying the device connection again after an error"
id="gui.connection.error.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.error.helpbutton"
/>
</button>
</Box>
</Box>
</Box>
);
ErrorStep.propTypes = {
connectionIconURL: PropTypes.string.isRequired,
onHelp: PropTypes.func,
onScanning: PropTypes.func
};
export default ErrorStep;

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>back</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="back" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M10.1575922,16.994103 C9.84650021,16.994103 9.55614768,16.8751967 9.34045723,16.6608889 L4.99899559,12.3194272 C4.66301623,11.9627084 4.57314521,11.4621959 4.75980041,11.0321977 C4.94507298,10.6021994 5.35433178,10.3367342 5.82857425,10.3367342 L7.47528788,10.3367342 L8.31454496,4.29463633 C8.43483386,3.36965613 9.23952516,2.6658969 10.1866275,2.6658969 C10.2682027,2.6658969 10.3511606,2.67142742 10.4327358,2.68110584 C11.2872018,2.81522106 11.9467168,3.48164924 12.0504142,4.30154948 L12.9117934,10.3367342 L14.4866102,10.3367342 C14.9663832,10.3367342 15.3936162,10.618791 15.5747408,11.0543198 C15.7558655,11.4926138 15.6576987,11.9779173 15.3161888,12.3194272 L10.9747272,16.6608889 C10.7590367,16.8751967 10.4686842,16.994103 10.1575922,16.994103" id="Fill-1" fill="#FFFFFF" transform="translate(10.164103, 9.830000) rotate(-270.000000) translate(-10.164103, -9.830000) "></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>bluetooth-white</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="bluetooth-white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M10.0067929,12.1067505 L12.3957387,14.0016608 L10.0067929,15.9178959 L10.0067929,12.1067505 Z M10.0067929,4.08928413 L12.3854344,5.99452362 L10.0067929,7.87910469 L10.0067929,4.08928413 Z M8.88494896,1.00684787 C8.38336008,1.07015593 8.00841492,1.49998436 8.01240369,2.00678206 L8.01240369,7.96240477 L6.64126113,6.8795037 C6.43185026,6.70790553 6.16194292,6.62927025 5.89336518,6.66092428 C5.34557295,6.71856794 4.9480247,7.21003843 5.00552959,7.75915257 C5.03411584,8.03404284 5.17505267,8.28494269 5.39476788,8.45187606 L7.3476073,9.99292759 L5.39476788,11.5443083 C4.96165303,11.8895039 4.88952262,12.5212517 5.23388715,12.9550785 C5.57791929,13.3895718 6.20814627,13.461543 6.64126113,13.1166807 L8.01240369,12.0234504 L8.01240369,18.0000647 C8.01240369,18.5521777 8.45848208,18.9999989 9.00926589,18.9999989 C9.23596146,19.0003321 9.45600907,18.9230297 9.63284491,18.7810863 L14.6188179,14.7826823 C15.0519327,14.4418184 15.1270547,13.8130694 14.7870114,13.3789093 C14.7378164,13.3162677 14.6813087,13.2596236 14.6188179,13.21031 L10.5675486,10.0032568 L14.6188179,6.78587441 C15.0489412,6.44134526 15.1190772,5.81192984 14.7753774,5.38076861 C14.7291741,5.32279175 14.6766552,5.2701461 14.6188179,5.22383126 L9.63284491,1.22542729 C9.42343405,1.05416232 9.15352671,0.975193838 8.88494896,1.00684787 Z" id="Fill-1" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="52px" height="52px" viewBox="0 0 52 52" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.1 (57501) - http://www.bohemiancoding.com/sketch -->
<title>Bluetooth</title>
<desc>Created with Sketch.</desc>
<defs>
<circle id="path-1" cx="22" cy="22" r="22"></circle>
<filter x="-13.6%" y="-13.6%" width="127.3%" height="127.3%" filterUnits="objectBoundingBox" id="filter-2">
<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
<feOffset dx="0" dy="0" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
<feColorMatrix values="0 0 0 0 0.298039216 0 0 0 0 0.592156863 0 0 0 0 1 0 0 0 0.25 0" type="matrix" in="shadowOffsetOuter1"></feColorMatrix>
</filter>
</defs>
<g id="R1_Extension" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="No-Comms-with-Scratch-Link-or-Bluetooth" transform="translate(-506.000000, -345.000000)">
<g id="No-connection-v2" transform="translate(-2.000000, 0.000000)">
<g id="Modal" transform="translate(407.000000, 187.000000)">
<g id="Bluetooth" transform="translate(52.000000, 162.000000)">
<g transform="translate(53.000000, 0.000000)">
<g id="Oval-8">
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
<use fill="#4C97FF" fill-rule="evenodd" xlink:href="#path-1"></use>
</g>
<path d="M22.0099629,25.4173518 L25.5137501,28.189863 L22.0099629,30.9935753 L22.0099629,25.4173518 Z M22.0099629,13.6867091 L25.4986371,16.4743334 L22.0099629,19.2317315 L22.0099629,13.6867091 Z M20.3645918,9.17668603 C19.6289281,9.26931433 19.0790085,9.89821172 19.0848588,10.6397256 L19.0848588,19.3536109 L17.0738497,17.7691795 C16.7667137,17.518108 16.3708496,17.4030539 15.9769356,17.4493681 C15.173507,17.5337086 14.5904362,18.2527967 14.6747767,19.0562253 C14.7167032,19.4584271 14.9234106,19.8255277 15.2456596,20.0697739 L18.109824,22.3245416 L15.2456596,24.5944225 C14.6104244,25.0994904 14.5046332,26.0238234 15.0097012,26.658571 C15.5142816,27.2942936 16.4386145,27.3995973 17.0738497,26.8950169 L19.0848588,25.2954724 L19.0848588,34.0400713 C19.0848588,34.8478875 19.739107,35.5031109 20.5469233,35.5031109 C20.8794101,35.5035984 21.2021466,35.3904943 21.4615059,35.182812 L28.7742662,29.3326037 C29.4095014,28.8338734 29.5196803,27.9139282 29.02095,27.278693 C28.9487974,27.1870398 28.8659195,27.1041618 28.7742662,27.0320093 L22.8324047,22.3396547 L28.7742662,17.6321871 C29.4051137,17.1280941 29.5079799,16.2071738 29.0038869,15.5763264 C28.936122,15.4914984 28.8590943,15.4144706 28.7742662,15.3467057 L21.4615059,9.49649742 C21.1543699,9.2459135 20.7585058,9.13037188 20.3645918,9.17668603 Z" id="Fill-1" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>cancel</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="cancel" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M15.464935,15.467297 C14.7644059,16.1777705 13.6185877,16.1777705 12.9081142,15.467297 L9.99881899,12.5580018 L7.08841885,15.467297 C6.38236506,16.1733508 5.23765187,16.1733508 4.53159807,15.467297 C4.17912364,15.1148226 4.00012409,14.6485398 4.00012409,14.1888866 C4.00012409,13.7281285 4.17912364,13.2629506 4.53159807,12.9104762 L7.44089328,10.001181 L4.52717833,7.08636112 C4.17359897,6.73278176 3.99459941,6.26760391 4.00012409,5.80242606 C4.00012409,5.34166795 4.17359897,4.88201477 4.52717833,4.52954034 C5.23212719,3.82348655 6.37684038,3.82348655 7.08399911,4.52954034 L9.99881899,7.44325529 L12.9125339,4.52954034 C13.6185877,3.82348655 14.7644059,3.82348655 15.4704597,4.52954034 C16.1765134,5.2344892 16.1765134,6.38030733 15.4704597,7.08636112 L12.5545348,10.001181 L15.4704597,12.914896 C16.1765134,13.6209497 16.1765134,14.7557185 15.464935,15.467297" id="close" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>close</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="close" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M15.464935,15.467297 C14.7644059,16.1777705 13.6185877,16.1777705 12.9081142,15.467297 L9.99881899,12.5580018 L7.08841885,15.467297 C6.38236506,16.1733508 5.23765187,16.1733508 4.53159807,15.467297 C4.17912364,15.1148226 4.00012409,14.6485398 4.00012409,14.1888866 C4.00012409,13.7281285 4.17912364,13.2629506 4.53159807,12.9104762 L7.44089328,10.001181 L4.52717833,7.08636112 C4.17359897,6.73278176 3.99459941,6.26760391 4.00012409,5.80242606 C4.00012409,5.34166795 4.17359897,4.88201477 4.52717833,4.52954034 C5.23212719,3.82348655 6.37684038,3.82348655 7.08399911,4.52954034 L9.99881899,7.44325529 L12.9125339,4.52954034 C13.6185877,3.82348655 14.7644059,3.82348655 15.4704597,4.52954034 C16.1765134,5.2344892 16.1765134,6.38030733 15.4704597,7.08636112 L12.5545348,10.001181 L15.4704597,12.914896 C16.1765134,13.6209497 16.1765134,14.7557185 15.464935,15.467297" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>help</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="help" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M9.99905449,18 C5.58160974,18 2,14.4173461 2,10.0009453 C2,5.58265391 5.58160974,2 9.99905449,2 C14.4183903,2 18,5.58265391 18,10.0009453 C18,14.4173461 14.4183903,18 9.99905449,18 Z M9.85325612,12.3615266 C9.26892802,12.3615266 8.80562581,12.8360629 8.80562581,13.4221434 C8.80562581,13.9930994 9.26892802,14.4676356 9.85325612,14.4676356 C10.4394752,14.4676356 10.9141236,13.9930994 10.9141236,13.4221434 C10.9141236,12.8360629 10.4394752,12.3615266 9.85325612,12.3615266 Z M9.46181302,11.6431053 L10.1596029,11.6431053 C10.3487058,11.6431053 10.5245716,11.5239986 10.5699563,11.3406121 C10.6588347,10.9700579 10.9141236,10.7167198 11.2545089,10.4614912 L11.7159201,10.1211863 C12.4458575,9.57291741 12.8978135,8.90554177 12.8978135,7.93000118 C12.8978135,6.71435661 11.9106961,5.53274253 9.93835244,5.53274253 C8.16078478,5.53274253 7.1018083,6.7370436 7.1018083,8.16065225 C7.1018083,8.1984639 7.10369933,8.23627555 7.10369933,8.27597779 C7.11315447,8.49717594 7.28902021,8.67867187 7.51027065,8.6862342 L8.38392625,8.71648352 C8.62408699,8.72593643 8.81886302,8.53309701 8.81886302,8.29299303 L8.81886302,8.28354012 C8.81886302,7.71258419 9.23299846,7.13973768 9.93835244,7.13973768 C10.6701808,7.13973768 11.0219123,7.60103982 11.0219123,8.07557604 C11.0219123,8.3931939 10.9141236,8.6862342 10.5850845,8.93011934 L9.96482685,9.39331206 C9.26892802,9.91511284 9.0363314,10.5352239 9.0363314,11.1080704 C9.0363314,11.1515538 9.03822243,11.1950372 9.03822243,11.2347395 C9.04578655,11.4634999 9.23299846,11.6431053 9.46181302,11.6431053 Z" id="help-icon" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>refresh</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="refresh" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M6.74838617,14.6292372 C6.74838617,14.993319 6.45171145,15.2885205 6.08581264,15.2885205 C5.71991382,15.2885205 5.41334994,14.993319 5.41334994,14.6292372 C5.41334994,14.2553154 5.71991382,13.95913 6.08581264,13.95913 C6.45171145,13.95913 6.74838617,14.2553154 6.74838617,14.6292372 Z M11.3771051,6.13737425 C10.04108,5.91105315 8.70703266,6.34401526 7.79723019,7.18041934 L8.95426159,8.33072095 C9.39927367,8.78336315 9.08282064,9.5518709 8.4390365,9.5518709 L3.7228974,9.5518709 C3.32634219,9.5518709 3,9.22714931 3,8.83256339 L3,4.13984452 C3,3.4992574 3.77234318,3.18535987 4.22724442,3.62717802 L5.21616014,4.61118282 C6.11607346,3.91253941 7.16333521,3.41069697 8.26202058,3.17453581 C9.53772187,2.89999848 10.86188,2.94919872 12.0792353,3.32312054 C14.5307574,4.06112414 16.4294756,6.10785411 16.9733792,8.44978553 C17.1029272,9.01066826 16.7469175,9.58139104 16.1822466,9.70931166 C15.6581213,9.83723229 15.144874,9.54203085 14.9569801,9.0598685 L14.9569801,9.05002845 C14.3537415,7.48447682 12.9000353,6.36271135 11.3771051,6.13737425 Z M9.45821305,16.1446046 C9.45821305,16.5578866 9.1219817,16.8924483 8.7066371,16.8924483 C8.29129249,16.8924483 7.9649503,16.5578866 7.9649503,16.1446046 C7.9649503,15.7313226 8.29129249,15.406601 8.7066371,15.406601 C9.1219817,15.406601 9.45821305,15.7313226 9.45821305,16.1446046 Z M15.2932114,14.7176009 C15.2932114,15.2096033 14.887756,15.6130452 14.3834089,15.6130452 C13.878073,15.6130452 13.4736065,15.2096033 13.4736065,14.7176009 C13.4736065,14.2157584 13.878073,13.8123165 14.3834089,13.8123165 C14.887756,13.8123165 15.2932114,14.2157584 15.2932114,14.7176009 Z M16.9542932,12.1394099 C16.9542932,12.6904526 16.5092811,13.1234147 15.9653774,13.1234147 C15.4214738,13.1234147 14.9764617,12.6904526 14.9764617,12.1394099 C14.9764617,11.5982073 15.4214738,11.1554051 15.9653774,11.1554051 C16.5092811,11.1554051 16.9542932,11.5982073 16.9542932,12.1394099 Z M12.5640018,16.173436 C12.5640018,16.6260782 12.1872249,17 11.7323237,17 C11.2774224,17 10.9115236,16.6260782 10.9115236,16.173436 C10.9115236,15.7207938 11.2774224,15.3468719 11.7323237,15.3468719 C12.1872249,15.3468719 12.5640018,15.7207938 12.5640018,16.173436 Z" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg width="52px" height="52px" viewBox="0 0 52 52" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
style="enable-background:new 0 0 52 52;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0FBD8C;}
.st1{fill:#FFFFFF;}
.st2{fill:#F9A83A;}
</style>
<title>Scratch Link</title>
<desc>Created with Sketch.</desc>
<g id="_x35_2x52-for-the-dialog">
<g id="Group">
<path id="bg" class="st0" d="M41.4,5c3.1,0,5.6,2.5,5.6,5.6v30.8c0,3.1-2.5,5.6-5.6,5.6H10.6C7.5,47,5,44.5,5,41.4V10.6
C5,7.5,7.5,5,10.6,5H41.4z"/>
<path id="scratch-outline-2" class="st1" d="M28.8,28.9c0,2.7-1.1,5.4-3.2,7.2c-1.7,1.5-3.8,2.3-5.9,2.4c-1,0.8-2.2,1.3-3.5,1.4
c-0.1,0-0.3,0-0.4,0c-3.3,0-6.1-2.7-6.4-6.2c0,0,0-0.1,0-0.2c0,0,0,0,0-0.1c-0.1-1.5,0-2.7,0-3.4c0-0.7,0.1-2,0.1-2.3v0
c0-1,0.3-2,0.7-2.9c-0.1-1-0.1-2.1,0-3.2l0-0.2c0,0,0,0,0-0.1c0,0,0,0,0-0.1c0.1-1,0.3-2.5,1.1-4.2c1.4-2.7,4-4.3,7-4.3h0.1
c0.9-0.5,2-0.8,3.1-0.8h0.1c3.5,0.1,6.3,3.1,6.3,6.7c0,0-0.1,4.4-0.3,5.9C28.4,25.9,28.7,27.4,28.8,28.9"/>
<path id="scratch-outline-1" class="st2" d="M25.8,29c0,1.8-0.8,3.6-2.2,4.8c-1.2,1.1-2.8,1.7-4.3,1.7c-0.3,0-0.5,0-0.8-0.1
c-0.1,0.1-0.1,0.2-0.2,0.2c-0.6,0.7-1.5,1.1-2.4,1.2c-0.1,0-0.1,0-0.2,0c-1.8,0-3.3-1.5-3.4-3.3c0,0,0-0.1,0-0.1l0,0
c-0.1-1.3,0-2.4,0-3.1c0-0.8,0.1-2.1,0.1-2.4c0-0.9,0.3-1.7,0.9-2.4c-0.3-1-0.4-2.1-0.2-3.5l0-0.2c0,0,0,0,0,0
c0.1-0.8,0.2-2,0.8-3.1c0.9-1.7,2.5-2.7,4.4-2.7c0.1,0,0.2,0,0.3,0c0.2,0,0.4,0,0.6,0.1c0.6-0.6,1.5-0.9,2.4-0.9
c1.9,0,3.4,1.6,3.4,3.6c0,0-0.2,5.2-0.2,5.7c0,0.3-0.1,0.5-0.2,0.8C25.3,26.2,25.8,27.5,25.8,29"/>
<path id="scratch-fill" class="st1" d="M18.3,25.3c-0.9-0.1-1.4-0.8-1.1-2.7l0-0.2c0.2-1.7,0.4-2,1.1-2c0.2,0,0.5,0.2,0.7,0.4
c0.2,0.3,0.8,0.7,1.1,1.4c0.2,0.5,0.3,0.9,0.3,1.3l0,0.5v0c0.1,0.3,0.3,0.6,0.6,0.6c0.4,0.1,0.8-0.2,0.9-0.6
c0-0.1,0.2-5.1,0.2-5.2c0-0.4-0.3-0.8-0.8-0.8c-0.4,0-0.8,0.4-0.8,0.8c0,0,0,0.7,0,1.4c-0.6-0.7-1.4-1.3-2.3-1.4
c-2.3-0.1-2.6,2.1-2.8,3.4l0,0.2c-0.3,2.5,0.5,4.2,2.4,4.5c2.1,0.3,3.5,0.8,3.5,2.2c0,0.5-0.3,1.1-0.7,1.5c-0.6,0.5-1.3,0.7-2,0.6
c-0.2,0-0.4-0.1-0.6-0.2c-0.3-0.2-1-0.6-1.3-1.1c-0.3-0.4-0.4-1.1-0.4-1.5c0-0.2,0-0.3,0-0.3c0-0.4-0.3-0.8-0.8-0.8
c-0.4,0-0.8,0.3-0.8,0.8c0,0,0,1.6-0.1,2.5c-0.1,1.5,0,2.8,0,2.9c0,0.4,0.4,0.8,0.8,0.8c0.4,0,0.8-0.4,0.7-0.9c0,0,0-0.6,0-1.5
c0.6,0.4,1.3,0.7,2.2,0.9c1.2,0.2,2.3-0.1,3.3-1c0.8-0.7,1.3-1.7,1.3-2.7C23.1,26,19.9,25.5,18.3,25.3"/>
<path id="signal" class="st1" d="M37.7,36.9c-0.2,0-0.4-0.1-0.5-0.2c-0.3-0.3-0.3-0.8,0-1.1c2.6-2.6,4-6,4-9.6
c0-3.6-1.4-7.1-4-9.7c-0.3-0.3-0.3-0.8,0-1.1c0.3-0.3,0.8-0.3,1,0c2.8,2.9,4.4,6.7,4.4,10.7c0,4-1.6,7.9-4.4,10.7
C38.1,36.9,37.9,36.9,37.7,36.9z M35,33.5c-0.2,0-0.4-0.1-0.5-0.2c-0.3-0.3-0.3-0.8,0-1.1c1.7-1.7,2.6-3.9,2.6-6.3
c0-2.4-0.9-4.6-2.6-6.3c-0.3-0.3-0.3-0.8,0-1.1c0.3-0.3,0.8-0.3,1,0c1.9,2,3,4.6,3,7.3c0,2.8-1.1,5.4-3,7.3
C35.4,33.5,35.2,33.5,35,33.5z M32.3,30.1c-0.2,0-0.4-0.1-0.5-0.2c-0.3-0.3-0.3-0.8,0-1.1c0.8-0.8,1.2-1.8,1.2-2.9
c0-1.1-0.4-2.1-1.2-2.9c-0.3-0.3-0.3-0.8,0-1.1c0.3-0.3,0.8-0.3,1,0c1,1,1.6,2.4,1.6,3.9c0,1.5-0.6,2.9-1.6,3.9
C32.7,30.1,32.5,30.1,32.3,30.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -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 PeripheralTile extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleConnecting'
]);
}
handleConnecting () {
this.props.onConnecting(this.props.peripheralId);
}
render () {
return (
<Box className={styles.peripheralTile}>
<Box className={styles.peripheralTileName}>
<img
className={styles.peripheralTileImage}
src={this.props.connectionSmallIconURL}
/>
<Box className={styles.peripheralTileNameWrapper}>
<Box className={styles.peripheralTileNameLabel}>
<FormattedMessage
defaultMessage="Device name"
description="Label for field showing the device name"
id="gui.connection.peripheral-name-label"
/>
</Box>
<Box className={styles.peripheralTileNameText}>
{this.props.name}
</Box>
</Box>
</Box>
<Box className={styles.peripheralTileWidgets}>
<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>
);
}
}
PeripheralTile.propTypes = {
connectionSmallIconURL: PropTypes.string,
name: PropTypes.string,
onConnecting: PropTypes.func,
peripheralId: PropTypes.string,
rssi: PropTypes.number
};
export default PeripheralTile;

View File

@ -0,0 +1,105 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import Box from '../box/box.jsx';
import PeripheralTile from './peripheral-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.peripheralList.length === 0 ? (
<div className={styles.activityAreaInfo}>
<div className={styles.centeredRow}>
<img
className={classNames(styles.radarSmall, styles.radarSpin)}
src={radarIcon}
/>
<FormattedMessage
defaultMessage="Looking for devices"
description="Text shown while scanning for devices"
id="gui.connection.scanning.lookingforperipherals"
/>
</div>
</div>
) : (
<div className={styles.peripheralTilePane}>
{props.peripheralList.map(peripheral =>
(<PeripheralTile
connectionSmallIconURL={props.connectionSmallIconURL}
key={peripheral.peripheralId}
name={peripheral.name}
peripheralId={peripheral.peripheralId}
rssi={peripheral.rssi}
onConnecting={props.onConnecting}
/>)
)}
</div>
)
) : (
<Box className={styles.instructions}>
<FormattedMessage
defaultMessage="No devices found"
description="Text shown when no devices could be found"
id="gui.connection.scanning.noPeripheralsFound"
/>
</Box>
)}
</Box>
<Box className={styles.bottomArea}>
<Box className={classNames(styles.bottomAreaItem, 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
className={styles.bottomAreaItem}
counter={0}
total={3}
/>
<button
className={classNames(styles.bottomAreaItem, 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 = {
connectionSmallIconURL: PropTypes.string,
onConnecting: PropTypes.func,
onRefresh: PropTypes.func,
peripheralList: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
rssi: PropTypes.number,
peripheralId: PropTypes.string
})),
scanning: PropTypes.bool.isRequired
};
ScanningStep.defaultProps = {
peripheralList: [],
scanning: true
};
export default ScanningStep;

View File

@ -0,0 +1,102 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import classNames from 'classnames';
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 bluetoothIcon from './icons/bluetooth.svg';
import scratchLinkIcon from './icons/scratchlink.svg';
import styles from './connection-modal.css';
const UnavailableStep = props => (
<Box className={styles.body}>
<Box className={styles.activityArea}>
<div className={styles.scratchLinkHelp}>
<div className={styles.scratchLinkHelpStep}>
<div className={styles.helpStepNumber}>
{'1'}
</div>
<div className={styles.helpStepImage}>
<img
className={styles.scratchLinkIcon}
src={scratchLinkIcon}
/>
</div>
<div className={styles.helpStepText}>
<FormattedMessage
defaultMessage="Make sure you have Scratch Link installed and running"
description="Message for getting Scratch Link installed"
id="gui.connection.unavailable.installscratchlink"
/>
</div>
</div>
<div className={styles.scratchLinkHelpStep}>
<div className={styles.helpStepNumber}>
{'2'}
</div>
<div className={styles.helpStepImage}>
<img
className={styles.scratchLinkIcon}
src={bluetoothIcon}
/>
</div>
<div className={styles.helpStepText}>
<FormattedMessage
defaultMessage="Check that Bluetooth is enabled"
description="Message for making sure Bluetooth is enabled"
id="gui.connection.unavailable.enablebluetooth"
/>
</div>
</div>
</div>
</Box>
<Box className={styles.bottomArea}>
<Dots
error
className={styles.bottomAreaItem}
total={3}
/>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
<button
className={styles.connectionButton}
onClick={props.onScanning}
>
<img
className={classNames(styles.buttonIconLeft, styles.buttonIconBack)}
src={backIcon}
/>
<FormattedMessage
defaultMessage="Try again"
description="Button to initiate trying the device connection again after an error"
id="gui.connection.unavailable.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.unavailable.helpbutton"
/>
</button>
</Box>
</Box>
</Box>
);
UnavailableStep.propTypes = {
onHelp: PropTypes.func,
onScanning: PropTypes.func
};
export default UnavailableStep;

View File

@ -0,0 +1,38 @@
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/z-index.css";
.context-menu {
min-width: 130px;
padding: 5px 0; /* The white strip at the top and bottom of the menu */
margin: 2px 0 0; /* To keep the menu below the cursor comfortably */
font-size: 0.85rem;
text-align: left;
background-color: $ui-white;
border: 1px solid $ui-black-transparent;
border-radius: calc($space / 2);
box-shadow: 0px 0px 5px 1px $ui-black-transparent;
pointer-events: none;
transition: opacity 0.2s ease;
z-index: $z-index-context-menu;
}
.menu-item {
padding: 8px 12px;
white-space: nowrap;
cursor: pointer;
transition: 0.1s ease;
}
.menu-item:hover {
background: $motion-primary;
color: white;
}
.menu-item-bordered {
border-top: 1px solid $ui-black-transparent;
}
.menu-item-danger:hover {
background: $error-primary;
}

View File

@ -0,0 +1,41 @@
import React from 'react';
import {ContextMenu, MenuItem} from 'react-contextmenu';
import classNames from 'classnames';
import styles from './context-menu.css';
const StyledContextMenu = props => (
<ContextMenu
{...props}
className={styles.contextMenu}
/>
);
const StyledMenuItem = props => (
<MenuItem
{...props}
attributes={{className: styles.menuItem}}
/>
);
const BorderedMenuItem = props => (
<MenuItem
{...props}
attributes={{className: classNames(styles.menuItem, styles.menuItemBordered)}}
/>
);
const DangerousMenuItem = props => (
<MenuItem
{...props}
attributes={{className: classNames(styles.menuItem, styles.menuItemBordered, styles.menuItemDanger)}}
/>
);
export {
BorderedMenuItem,
DangerousMenuItem,
StyledContextMenu as ContextMenu,
StyledMenuItem as MenuItem
};

View File

@ -0,0 +1,3 @@
.controls-container {
display: flex;
}

Some files were not shown because too many files have changed in this diff Show More