UI actions; Context menu
This commit is contained in:
7
src/js/bootstrap.js
vendored
7
src/js/bootstrap.js
vendored
@ -3,6 +3,7 @@ console.info('Bootstrapping...');
|
||||
|
||||
import { createStore, applyMiddleware, combineReducers } from 'redux'
|
||||
|
||||
import ui from './services/ui/reducer'
|
||||
import mopidy from './services/mopidy/reducer'
|
||||
import spotify from './services/spotify/reducer'
|
||||
|
||||
@ -12,6 +13,7 @@ import spotifyMiddleware from './services/spotify/middleware'
|
||||
import localstorageMiddleware from './services/localstorage/middleware'
|
||||
|
||||
let reducers = combineReducers({
|
||||
ui,
|
||||
mopidy,
|
||||
spotify
|
||||
});
|
||||
@ -28,6 +30,11 @@ var initialState = {
|
||||
country: 'NZ',
|
||||
locale: 'en_NZ',
|
||||
me: false
|
||||
},
|
||||
ui: {
|
||||
context_menu: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
142
src/js/components/ContextMenu.js
Executable file
142
src/js/components/ContextMenu.js
Executable file
@ -0,0 +1,142 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import TrackList from './TrackList'
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
|
||||
class ContextMenu extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
}
|
||||
|
||||
renderItems(){
|
||||
var items = [];
|
||||
|
||||
switch( this.props.state.context ){
|
||||
|
||||
case 'queue':
|
||||
items = [
|
||||
{ handleClick: 'playQueueItem', label: 'Play' },
|
||||
{ handleClick: 'removeFromQueue', label: 'Remove' }
|
||||
];
|
||||
break;
|
||||
|
||||
case 'editable-playlist':
|
||||
items = [
|
||||
{ handleClick: 'playItems', label: 'Play' },
|
||||
{ handleClick: 'removeFromPlaylist', label: 'Remove' }
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
items = [
|
||||
{ handleClick: 'playItems', label: 'Play' },
|
||||
{ handleClick: 'playItemsNext', label: 'Play next' },
|
||||
{ handleClick: 'addToQueue', label: 'Add to queue' }
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return <a key={item.handleClick} onClick={ (e) => this[item.handleClick](e) }>{ item.label }</a>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
playQueueItem(){
|
||||
var selectedTracks = this.props.state.data.selected_tracks;
|
||||
this.props.mopidyActions.changeTrack( selectedTracks[0].tlid );
|
||||
this.props.uiActions.hideContextMenu();
|
||||
}
|
||||
|
||||
removeFromQueue(){
|
||||
var selectedTracks = this.props.state.data.selected_tracks;
|
||||
var selectedTracksTlids = [];
|
||||
for( var i = 0; i < selectedTracks.length; i++ ){
|
||||
selectedTracksTlids.push( selectedTracks[i].tlid );
|
||||
}
|
||||
this.props.mopidyActions.removeTracks( selectedTracksTlids );
|
||||
this.props.uiActions.hideContextMenu();
|
||||
}
|
||||
|
||||
playItems(){
|
||||
var selectedTracks = this.props.state.data.selected_tracks;
|
||||
var selectedTracksUris = [];
|
||||
for( var i = 0; i < selectedTracks.length; i++ ){
|
||||
selectedTracksUris.push( selectedTracks[i].uri );
|
||||
}
|
||||
this.props.mopidyActions.playTracks(selectedTracksUris);
|
||||
this.props.uiActions.hideContextMenu();
|
||||
}
|
||||
|
||||
playItemsNext(){
|
||||
var selectedTracks = this.props.state.data.selected_tracks;
|
||||
var selectedTracksUris = [];
|
||||
for( var i = 0; i < selectedTracks.length; i++ ){
|
||||
selectedTracksUris.push( selectedTracks[i].uri );
|
||||
}
|
||||
|
||||
// TODO: figure out what index the currentTlTrack is (.indexOf()?)
|
||||
var currentTrackIndex = 1;
|
||||
|
||||
this.props.mopidyActions.enqueueTracks(selectedTracksUris, currentTrackIndex);
|
||||
this.props.uiActions.hideContextMenu();
|
||||
}
|
||||
|
||||
addToQueue(){
|
||||
var selectedTracks = this.props.state.data.selected_tracks;
|
||||
var selectedTracksUris = [];
|
||||
for( var i = 0; i < selectedTracks.length; i++ ){
|
||||
selectedTracksUris.push( selectedTracks[i].uri );
|
||||
}
|
||||
this.props.mopidyActions.enqueueTracks(selectedTracksUris);
|
||||
this.props.uiActions.hideContextMenu();
|
||||
}
|
||||
|
||||
removeFromPlaylist(){
|
||||
console.log('removeFromPlaylist')
|
||||
this.props.uiActions.hideContextMenu();
|
||||
}
|
||||
|
||||
render(){
|
||||
if( !this.props.state.show ) return null;
|
||||
|
||||
var style = {
|
||||
left: this.props.state.position_x,
|
||||
top: this.props.state.position_y,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="context-menu" style={style}>
|
||||
{ this.renderItems() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ContextMenu)
|
||||
@ -50,6 +50,10 @@ class Sidebar extends React.Component{
|
||||
<Icon name="star" className="white" />
|
||||
Featured playlists
|
||||
</Link>
|
||||
<Link activeClassName="active" to="/discover/new-releases">
|
||||
<Icon name="leaf" className="white" />
|
||||
New releases
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
@ -10,17 +10,27 @@ export default class Track extends React.Component{
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleClick( e ){
|
||||
handleClick(e){
|
||||
var target = $(e.target);
|
||||
if( !target.is('a') && target.closest('a').length <= 0 ){
|
||||
this.props.handleClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick( e ){
|
||||
handleDoubleClick(e){
|
||||
return this.props.handleDoubleClick(e);
|
||||
}
|
||||
|
||||
handleContextMenu(e){
|
||||
e.preventDefault();
|
||||
|
||||
// trigger a regular click event
|
||||
if( !this.props.track.selected ) this.handleClick(e);
|
||||
|
||||
// notify our tracklist
|
||||
this.props.handleContextMenu(e);
|
||||
}
|
||||
|
||||
formatDuration(){
|
||||
if( typeof(this.props.track.duration_ms) !== 'undefined' ){
|
||||
var ms = this.props.track.duration_ms;
|
||||
@ -52,7 +62,8 @@ export default class Track extends React.Component{
|
||||
<div
|
||||
className={className}
|
||||
onDoubleClick={ (e) => this.handleDoubleClick(e) }
|
||||
onClick={ (e) => this.handleClick(e) }>
|
||||
onClick={ (e) => this.handleClick(e) }
|
||||
onContextMenu={ (e) => this.handleContextMenu(e) }>
|
||||
{ this.props.track.selected ? <FontAwesome name="check" className="select-state" fixedWidth /> : null }
|
||||
<span className="col name">
|
||||
{this.props.track.name}
|
||||
|
||||
@ -3,6 +3,7 @@ import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
|
||||
import Track from './Track'
|
||||
|
||||
@ -22,6 +23,8 @@ class TrackList extends React.Component{
|
||||
}
|
||||
|
||||
handleClick( e, index ){
|
||||
if( this.props.ui.context_menu.show ) this.props.uiActions.hideContextMenu();
|
||||
|
||||
var tracks = this.state.tracks;
|
||||
|
||||
if( e.ctrlKey ){
|
||||
@ -54,10 +57,19 @@ class TrackList extends React.Component{
|
||||
}
|
||||
|
||||
handleDoubleClick( e, index ){
|
||||
if( this.props.ui.context_menu.show ) this.props.uiActions.hideContextMenu();
|
||||
|
||||
var tracks = this.state.tracks;
|
||||
this.playTrack( tracks[index] )
|
||||
}
|
||||
|
||||
handleContextMenu( e, index ){
|
||||
var data = {
|
||||
selected_tracks: this.selectedTracks()
|
||||
}
|
||||
this.props.uiActions.showContextMenu( e, this.props.context, data )
|
||||
}
|
||||
|
||||
selectedTracks(){
|
||||
function isSelected( track ){
|
||||
return ( typeof(track.selected) !== 'undefined' && track.selected );
|
||||
@ -103,46 +115,44 @@ class TrackList extends React.Component{
|
||||
let self = this;
|
||||
if( this.state.tracks ){
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
<li className="list-item header track">
|
||||
<span className="col name">Name</span>
|
||||
<span className="col artists">Artists</span>
|
||||
<span className="col album">Album</span>
|
||||
<span className="col duration">Duration</span>
|
||||
</li>
|
||||
{
|
||||
this.state.tracks.map(
|
||||
(track, index) => {
|
||||
<ul>
|
||||
<li className="list-item header track">
|
||||
<span className="col name">Name</span>
|
||||
<span className="col artists">Artists</span>
|
||||
<span className="col album">Album</span>
|
||||
<span className="col duration">Duration</span>
|
||||
</li>
|
||||
{
|
||||
this.state.tracks.map(
|
||||
(track, index) => {
|
||||
|
||||
// flatten nested track objects (as in the case of TlTracks)
|
||||
if( typeof(track.track) !== 'undefined' ){
|
||||
// flatten nested track objects (as in the case of TlTracks)
|
||||
if( typeof(track.track) !== 'undefined' ){
|
||||
|
||||
// see if we're the current tlTrack
|
||||
if( self.props.mopidy.trackInFocus && self.props.mopidy.trackInFocus.tlid == track.tlid ){
|
||||
track.playing = true;
|
||||
}else{
|
||||
track.playing = false;
|
||||
}
|
||||
// see if we're the current tlTrack
|
||||
// TODO: figure out why this isn't fired when the tracklist changes
|
||||
if( self.props.mopidy.trackInFocus && self.props.mopidy.trackInFocus.tlid == track.tlid ){
|
||||
track.playing = true;
|
||||
}else{
|
||||
track.playing = false;
|
||||
}
|
||||
|
||||
for( var property in track.track ){
|
||||
if( track.track.hasOwnProperty(property) ){
|
||||
track[property] = track.track[property]
|
||||
}
|
||||
for( var property in track.track ){
|
||||
if( track.track.hasOwnProperty(property) ){
|
||||
track[property] = track.track[property]
|
||||
}
|
||||
}
|
||||
return <Track
|
||||
key={index+'_'+track.uri}
|
||||
track={track}
|
||||
handleDoubleClick={(e) => self.handleDoubleClick(e, index)}
|
||||
handleClick={(e) => self.handleClick(e, index)} />
|
||||
}
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
<button onClick={() => this.removeTracks()}>Delete selected</button>
|
||||
<button onClick={() => this.playTracks()}>Play selected</button>
|
||||
</div>
|
||||
return <Track
|
||||
key={index+'_'+track.uri}
|
||||
track={track}
|
||||
handleDoubleClick={(e) => self.handleDoubleClick(e, index)}
|
||||
handleClick={(e) => self.handleClick(e, index)}
|
||||
handleContextMenu={(e) => self.handleContextMenu(e, index)} />
|
||||
}
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@ -162,7 +172,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ import Discover from './views/discover/Discover'
|
||||
import DiscoverFeatured from './views/discover/DiscoverFeatured'
|
||||
import DiscoverCategories from './views/discover/DiscoverCategories'
|
||||
import DiscoverCategory from './views/discover/DiscoverCategory'
|
||||
import DiscoverNewReleases from './views/discover/DiscoverNewReleases'
|
||||
|
||||
import LibraryArtists from './views/library/LibraryArtists'
|
||||
import LibraryAlbums from './views/library/LibraryAlbums'
|
||||
@ -41,6 +42,7 @@ ReactDOM.render(
|
||||
<Route path="/discover/featured" component={DiscoverFeatured} />
|
||||
<Route path="/discover/categories" component={DiscoverCategories} />
|
||||
<Route path="/discover/categories/:id" component={DiscoverCategory} />
|
||||
<Route path="/discover/new-releases" component={DiscoverNewReleases} />
|
||||
|
||||
<Route path="/library/artists" component={LibraryArtists} />
|
||||
<Route path="/library/albums" component={LibraryAlbums} />
|
||||
|
||||
@ -405,7 +405,7 @@ export function getCategory( id ){
|
||||
}
|
||||
|
||||
export function getCategoryPlaylists( id ){
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({ type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED', data: false });
|
||||
|
||||
@ -416,6 +416,21 @@ export function getCategoryPlaylists( id ){
|
||||
data: response.playlists
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getNewReleases(){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({ type: 'SPOTIFY_NEW_RELEASES_LOADED', data: false });
|
||||
|
||||
sendRequest( dispatch, getState, 'browse/new-releases?country='+getState().spotify.country+'&limit=50' )
|
||||
.then( response => {
|
||||
dispatch({
|
||||
type: 'SPOTIFY_NEW_RELEASES_LOADED',
|
||||
data: response.albums
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -82,6 +82,9 @@ export default function reducer(spotify = {}, action){
|
||||
case 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED':
|
||||
return Object.assign({}, spotify, { category_playlists: action.data });
|
||||
|
||||
case 'SPOTIFY_NEW_RELEASES_LOADED':
|
||||
return Object.assign({}, spotify, { new_releases: action.data });
|
||||
|
||||
default:
|
||||
return spotify
|
||||
}
|
||||
|
||||
19
src/js/services/ui/actions.js
Executable file
19
src/js/services/ui/actions.js
Executable file
@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
export function showContextMenu( e, context = false, data ){
|
||||
return {
|
||||
type: 'UI_SHOW_CONTEXT_MENU',
|
||||
position_x: e.clientX,
|
||||
position_y: e.clientY,
|
||||
context: context,
|
||||
data: data
|
||||
}
|
||||
}
|
||||
|
||||
export function hideContextMenu(){
|
||||
return {
|
||||
type: 'UI_HIDE_CONTEXT_MENU'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
src/js/services/ui/reducer.js
Executable file
25
src/js/services/ui/reducer.js
Executable file
@ -0,0 +1,25 @@
|
||||
|
||||
export default function reducer(ui = {}, action){
|
||||
switch (action.type) {
|
||||
|
||||
case 'UI_SHOW_CONTEXT_MENU':
|
||||
return Object.assign({}, ui, {
|
||||
context_menu: {
|
||||
show: true,
|
||||
position_x: action.position_x,
|
||||
position_y: action.position_y,
|
||||
context: action.context,
|
||||
data: action.data
|
||||
}
|
||||
});
|
||||
|
||||
case 'UI_HIDE_CONTEXT_MENU':
|
||||
return Object.assign({}, ui, { context_menu: { show: false } });
|
||||
|
||||
default:
|
||||
return ui
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -6,7 +6,9 @@ import { Link } from 'react-router'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Sidebar from '../components/Sidebar'
|
||||
import ContextMenu from '../components/ContextMenu'
|
||||
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
|
||||
@ -31,6 +33,8 @@ class App extends React.Component{
|
||||
<main>
|
||||
{this.props.children}
|
||||
</main>
|
||||
{ this.props.ui.context_menu.test }
|
||||
<ContextMenu state={this.props.ui.context_menu} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -48,6 +52,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
|
||||
@ -29,10 +29,14 @@ class Playlist extends React.Component{
|
||||
|
||||
render(){
|
||||
if( this.props.spotify.playlist ){
|
||||
|
||||
var context = null;
|
||||
if( this.props.spotify.playlist.owner.id == this.props.spotify.me.id ) context = 'editable-playlist'
|
||||
|
||||
return (
|
||||
<div className="view playlist-view">
|
||||
<Header icon="playlist" title={ this.props.spotify.playlist.name } />
|
||||
<TrackList tracks={this.props.spotify.playlist.tracks.items} />
|
||||
<TrackList context={context} tracks={this.props.spotify.playlist.tracks.items} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ class Queue extends React.Component{
|
||||
if( this.props.mopidy && this.props.mopidy.tracks ){
|
||||
return (
|
||||
<TrackList
|
||||
type="tltrack"
|
||||
context="queue"
|
||||
tracks={this.props.mopidy.tracks}
|
||||
removeTracks={ tracks => this.removeTracks( tracks ) }
|
||||
playTracks={ null }
|
||||
|
||||
49
src/js/views/discover/DiscoverNewReleases.js
Executable file
49
src/js/views/discover/DiscoverNewReleases.js
Executable file
@ -0,0 +1,49 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import Header from '../../components/Header'
|
||||
import AlbumGrid from '../../components/AlbumGrid'
|
||||
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class DiscoverNewReleases extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
// on render
|
||||
componentDidMount(){
|
||||
this.props.spotifyActions.getNewReleases();
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div className="view discover-new-releases-view">
|
||||
<Header icon="leaf" title="New Releases" />
|
||||
{ this.props.spotify.new_releases ? <AlbumGrid albums={this.props.spotify.new_releases.items} /> : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DiscoverNewReleases)
|
||||
@ -7,6 +7,7 @@
|
||||
@import 'global/reset';
|
||||
@import 'global/forms';
|
||||
|
||||
@import 'components/context-menu';
|
||||
@import 'components/images';
|
||||
@import 'components/icon';
|
||||
@import 'components/sidebar';
|
||||
|
||||
24
src/scss/components/_context-menu.scss
Executable file
24
src/scss/components/_context-menu.scss
Executable file
@ -0,0 +1,24 @@
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
background: $dark_grey;
|
||||
color: #FFFFFF;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
width: 120px;
|
||||
|
||||
&:not(:first-child){
|
||||
border-top: 1px solid lighten($dark_grey, 8%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: lighten($dark_grey, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
border-bottom: 0;
|
||||
|
||||
.thumbnail {
|
||||
@include animate();
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@ -31,7 +32,10 @@
|
||||
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
opacity: 0.75;
|
||||
|
||||
.thumbnail {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,10 +12,10 @@ aside{
|
||||
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
top: -30px;
|
||||
right: -30px;
|
||||
bottom: -30px;
|
||||
left: -30px;
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
@ -23,6 +23,7 @@ aside{
|
||||
opacity: 0.2;
|
||||
|
||||
.image {
|
||||
@include blur();
|
||||
padding-bottom: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -25,4 +25,9 @@ $red: #cf2d2d;
|
||||
-moz-transition: all $speed ease-in-out;
|
||||
-o-transition: all $speed ease-in-out;
|
||||
transition: all $speed ease-in-out;
|
||||
}
|
||||
|
||||
@mixin blur( $size: 10px ) {
|
||||
-webkit-filter: blur( $size );
|
||||
filter: blur( $size );
|
||||
}
|
||||
Reference in New Issue
Block a user