mirror of
https://github.com/kevin-DL/vue-audio-recorder.git
synced 2026-01-19 14:05:20 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6d6260b1b | |||
| adf3fba1b3 | |||
| eb674f00f0 | |||
| b1ff2cd677 | |||
| f680d5f47d | |||
| 4cde0ea657 | |||
| d836371eaa | |||
|
|
84fb2fd6f6 | ||
|
|
2b12f06640 | ||
|
|
cf7df625ca | ||
|
|
a51fa4fdf2 | ||
|
|
94fc582eef | ||
|
|
1f8aad013a | ||
|
|
22d1b524a4 | ||
|
|
a9a98d2e33 | ||
|
|
e482c3bea7 | ||
|
|
0c4c0cd091 | ||
|
|
77f3460825 | ||
|
|
7fdf3f745b | ||
|
|
87f9703529 |
12
README.md
12
README.md
@@ -14,7 +14,7 @@
|
|||||||
- Records limit
|
- Records limit
|
||||||
- A lot of callbacks
|
- A lot of callbacks
|
||||||
- Individual an audio player
|
- Individual an audio player
|
||||||
- MP3 support
|
- MP3/WAV support
|
||||||
|
|
||||||
### Tested in (desktop)
|
### Tested in (desktop)
|
||||||
|
|
||||||
@@ -35,10 +35,14 @@ npm i vue-audio-recorder --save
|
|||||||
| attempts | Number | Number of recording attempts |
|
| attempts | Number | Number of recording attempts |
|
||||||
| headers | Object | HTTP headers |
|
| headers | Object | HTTP headers |
|
||||||
| time | Number | Time limit for the record (minutes) |
|
| time | Number | Time limit for the record (minutes) |
|
||||||
|
| bit-rate | Number | Default: 128 (only for MP3) |
|
||||||
|
| sample-rate | Number | Default: 44100 |
|
||||||
| filename | String | Download/Upload filename |
|
| filename | String | Download/Upload filename |
|
||||||
|
| format | String | WAV/MP3. Default: mp3 |
|
||||||
| upload-url | String | URL for uploading |
|
| upload-url | String | URL for uploading |
|
||||||
| show-download-button | Boolean | If it is true show a download button. Default: true |
|
| show-download-button | Boolean | If it is true show a download button. Default: true |
|
||||||
| show-upload-button | Boolean | If it is true show an upload button. Default: true |
|
| show-upload-button | Boolean | If it is true show an upload button. Default: true |
|
||||||
|
| show-custom-button | Boolean | If true show another button linked to specific action. Default: true |
|
||||||
| before-upload | Function | Callback fires before uploading |
|
| before-upload | Function | Callback fires before uploading |
|
||||||
| successful-upload | Function | Callback fires after successful uploading |
|
| successful-upload | Function | Callback fires after successful uploading |
|
||||||
| failed-upload | Function | Callback fires after failure uploading |
|
| failed-upload | Function | Callback fires after failure uploading |
|
||||||
@@ -47,6 +51,7 @@ npm i vue-audio-recorder --save
|
|||||||
| pause-recording | Function | Callback fires after pause recording |
|
| pause-recording | Function | Callback fires after pause recording |
|
||||||
| after-recording | Function | Callback fires after click the stop button or exceeding the time limit |
|
| after-recording | Function | Callback fires after click the stop button or exceeding the time limit |
|
||||||
| select-record | Function | Callback fires after choise a record. Returns the record |
|
| select-record | Function | Callback fires after choise a record. Returns the record |
|
||||||
|
| custom-callback | Function | Callback fires when clicking on the custom button |
|
||||||
|
|
||||||
## AudioPlayer props
|
## AudioPlayer props
|
||||||
| Prop | Type | Description |
|
| Prop | Type | Description |
|
||||||
@@ -101,6 +106,11 @@ npm run dev
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- Clear record list
|
||||||
|
- Responsive design
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
[Gennady Grishkovtsov](https://www.linkedin.com/in/grishkovtsov/) - Developer
|
[Gennady Grishkovtsov](https://www.linkedin.com/in/grishkovtsov/) - Developer
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
<audio-recorder v-if="showRecorder"
|
<audio-recorder v-if="showRecorder"
|
||||||
upload-url="some url"
|
upload-url="some url"
|
||||||
|
filename="ninja"
|
||||||
|
format="wav"
|
||||||
:attempts="3"
|
:attempts="3"
|
||||||
:time="2"
|
:time="2"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
@@ -20,7 +22,8 @@
|
|||||||
:select-record="callback"
|
:select-record="callback"
|
||||||
:before-upload="callback"
|
:before-upload="callback"
|
||||||
:successful-upload="callback"
|
:successful-upload="callback"
|
||||||
:failed-upload="callback"/>
|
:failed-upload="callback"
|
||||||
|
:bit-rate="192"/>
|
||||||
|
|
||||||
<audio-player :src="mp3" v-if="!showRecorder"/>
|
<audio-player :src="mp3" v-if="!showRecorder"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
dist/vue-audio-recorder.min.js
vendored
2
dist/vue-audio-recorder.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/vue-audio-recorder.min.js.map
vendored
2
dist/vue-audio-recorder.min.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-audio-recorder",
|
"name": "vue-audio-recorder",
|
||||||
"description": "Audio recorder for Vue.js. It allows to create, play, download and store records on a server",
|
"description": "Audio recorder for Vue.js. It allows to create, play, download and store records on a server",
|
||||||
"version": "3.0.1",
|
"version": "4.0.1",
|
||||||
"author": "Gennady Grishkovtsov <grishkovelli@gmail.com>",
|
"author": "Gennady Grishkovtsov <grishkovelli@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -27,9 +27,10 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const type = this.record.blob.type.split('/')[1]
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = this.record.url
|
link.href = this.record.url
|
||||||
link.download = `${this.filename}.mp3`
|
link.download = `${this.filename}.${type}`
|
||||||
link.click()
|
link.click()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
icons: {
|
icons: {
|
||||||
|
upload: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">\n' +
|
||||||
|
' <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />\n' +
|
||||||
|
'</svg>',
|
||||||
download: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/><path fill="none" d="M0 0h24v24H0z"/></svg>',
|
download: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/><path fill="none" d="M0 0h24v24H0z"/></svg>',
|
||||||
mic: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
mic: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
||||||
pause: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
pause: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
|
||||||
|
|||||||
@@ -56,6 +56,10 @@
|
|||||||
&--active {
|
&--active {
|
||||||
fill: white !important;
|
fill: white !important;
|
||||||
background-color: #05CBCD !important;
|
background-color: #05CBCD !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: #505050 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../scss/icons';
|
||||||
.ar {
|
.ar {
|
||||||
width: 420px;
|
width: 420px;
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', sans-serif;
|
||||||
@@ -237,6 +238,7 @@
|
|||||||
:filename="filename"
|
:filename="filename"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:upload-url="uploadUrl"/>
|
:upload-url="uploadUrl"/>
|
||||||
|
<icon-button name="upload" class="ar__uploader ar-icon ar-icon__xs" v-if="showCustomButton" @click.native="customAction(record)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -260,8 +262,12 @@
|
|||||||
attempts : { type: Number },
|
attempts : { type: Number },
|
||||||
time : { type: Number },
|
time : { type: Number },
|
||||||
|
|
||||||
|
bitRate : { type: Number, default: 128 },
|
||||||
|
sampleRate : { type: Number, default: 44100 },
|
||||||
|
|
||||||
showDownloadButton : { type: Boolean, default: true },
|
showDownloadButton : { type: Boolean, default: true },
|
||||||
showUploadButton : { type: Boolean, default: true },
|
showUploadButton : { type: Boolean, default: true },
|
||||||
|
showCustomButton : {type: Boolean, default: false},
|
||||||
|
|
||||||
micFailed : { type: Function },
|
micFailed : { type: Function },
|
||||||
beforeRecording : { type: Function },
|
beforeRecording : { type: Function },
|
||||||
@@ -270,7 +276,9 @@
|
|||||||
failedUpload : { type: Function },
|
failedUpload : { type: Function },
|
||||||
beforeUpload : { type: Function },
|
beforeUpload : { type: Function },
|
||||||
successfulUpload : { type: Function },
|
successfulUpload : { type: Function },
|
||||||
selectRecord : { type: Function }
|
selectRecord : { type: Function },
|
||||||
|
customCallback : { type: Function },
|
||||||
|
format : { type: String }
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -343,8 +351,18 @@
|
|||||||
beforeRecording : this.beforeRecording,
|
beforeRecording : this.beforeRecording,
|
||||||
afterRecording : this.afterRecording,
|
afterRecording : this.afterRecording,
|
||||||
pauseRecording : this.pauseRecording,
|
pauseRecording : this.pauseRecording,
|
||||||
micFailed : this.micFailed
|
micFailed : this.micFailed,
|
||||||
|
bitRate : this.bitRate,
|
||||||
|
sampleRate : this.sampleRate,
|
||||||
|
format : this.format
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
customAction(record) {
|
||||||
|
if (this.customCallback) {
|
||||||
|
this.customCallback(record)
|
||||||
|
} else {
|
||||||
|
console.log(record)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Mp3Encoder } from 'lamejs'
|
|||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.bitRate = config.bitRate || 128
|
this.bitRate = config.bitRate
|
||||||
this.sampleRate = config.sampleRate || 44100
|
this.sampleRate = config.sampleRate
|
||||||
this.dataBuffer = []
|
this.dataBuffer = []
|
||||||
this.encoder = new Mp3Encoder(1, this.sampleRate, this.bitRate)
|
this.encoder = new Mp3Encoder(1, this.sampleRate, this.bitRate)
|
||||||
}
|
}
|
||||||
@@ -27,9 +27,9 @@ export default class {
|
|||||||
this.dataBuffer = []
|
this.dataBuffer = []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id : Date.now(),
|
id : Date.now(),
|
||||||
blob : blob,
|
blob : blob,
|
||||||
url : URL.createObjectURL(blob)
|
url : URL.createObjectURL(blob)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Encoder from './encoder'
|
import Mp3Encoder from './mp3-encoder'
|
||||||
|
import WavEncoder from './wav-encoder'
|
||||||
import { convertTimeMMSS } from './utils'
|
import { convertTimeMMSS } from './utils'
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
@@ -7,6 +8,12 @@ export default class {
|
|||||||
this.pauseRecording = options.pauseRecording
|
this.pauseRecording = options.pauseRecording
|
||||||
this.afterRecording = options.afterRecording
|
this.afterRecording = options.afterRecording
|
||||||
this.micFailed = options.micFailed
|
this.micFailed = options.micFailed
|
||||||
|
this.format = options.format
|
||||||
|
|
||||||
|
this.encoderOptions = {
|
||||||
|
bitRate : options.bitRate,
|
||||||
|
sampleRate : options.sampleRate
|
||||||
|
}
|
||||||
|
|
||||||
this.bufferSize = 4096
|
this.bufferSize = 4096
|
||||||
this.records = []
|
this.records = []
|
||||||
@@ -17,6 +24,8 @@ export default class {
|
|||||||
this.duration = 0
|
this.duration = 0
|
||||||
this.volume = 0
|
this.volume = 0
|
||||||
|
|
||||||
|
this.wavSamples = []
|
||||||
|
|
||||||
this._duration = 0
|
this._duration = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +44,13 @@ export default class {
|
|||||||
.getUserMedia(constraints)
|
.getUserMedia(constraints)
|
||||||
.then(this._micCaptured.bind(this))
|
.then(this._micCaptured.bind(this))
|
||||||
.catch(this._micError.bind(this))
|
.catch(this._micError.bind(this))
|
||||||
this.isPause = false
|
|
||||||
|
this.isPause = false
|
||||||
this.isRecording = true
|
this.isRecording = true
|
||||||
this.lameEncoder = new Encoder({})
|
|
||||||
|
if (this._isMp3() && !this.lameEncoder) {
|
||||||
|
this.lameEncoder = new Mp3Encoder(this.encoderOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop () {
|
stop () {
|
||||||
@@ -46,7 +59,20 @@ export default class {
|
|||||||
this.processor.disconnect()
|
this.processor.disconnect()
|
||||||
this.context.close()
|
this.context.close()
|
||||||
|
|
||||||
const record = this.lameEncoder.finish()
|
let record = null
|
||||||
|
|
||||||
|
if (this._isMp3()) {
|
||||||
|
record = this.lameEncoder.finish()
|
||||||
|
} else {
|
||||||
|
let wavEncoder = new WavEncoder({
|
||||||
|
bufferSize : this.bufferSize,
|
||||||
|
sampleRate : this.encoderOptions.sampleRate,
|
||||||
|
samples : this.wavSamples
|
||||||
|
})
|
||||||
|
record = wavEncoder.finish()
|
||||||
|
this.wavSamples = []
|
||||||
|
}
|
||||||
|
|
||||||
record.duration = convertTimeMMSS(this.duration)
|
record.duration = convertTimeMMSS(this.duration)
|
||||||
this.records.push(record)
|
this.records.push(record)
|
||||||
|
|
||||||
@@ -63,7 +89,6 @@ export default class {
|
|||||||
this.stream.getTracks().forEach((track) => track.stop())
|
this.stream.getTracks().forEach((track) => track.stop())
|
||||||
this.input.disconnect()
|
this.input.disconnect()
|
||||||
this.processor.disconnect()
|
this.processor.disconnect()
|
||||||
this.context.close()
|
|
||||||
|
|
||||||
this._duration = this.duration
|
this._duration = this.duration
|
||||||
this.isPause = true
|
this.isPause = true
|
||||||
@@ -76,7 +101,7 @@ export default class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastRecord () {
|
lastRecord () {
|
||||||
return this.records.slice(-1)
|
return this.records.slice(-1).pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
_micCaptured (stream) {
|
_micCaptured (stream) {
|
||||||
@@ -90,7 +115,11 @@ export default class {
|
|||||||
const sample = ev.inputBuffer.getChannelData(0)
|
const sample = ev.inputBuffer.getChannelData(0)
|
||||||
let sum = 0.0
|
let sum = 0.0
|
||||||
|
|
||||||
this.lameEncoder.encode(sample)
|
if (this._isMp3()) {
|
||||||
|
this.lameEncoder.encode(sample)
|
||||||
|
} else {
|
||||||
|
this.wavSamples.push(new Float32Array(sample))
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < sample.length; ++i) {
|
for (let i = 0; i < sample.length; ++i) {
|
||||||
sum += sample[i] * sample[i]
|
sum += sample[i] * sample[i]
|
||||||
@@ -107,4 +136,8 @@ export default class {
|
|||||||
_micError (error) {
|
_micError (error) {
|
||||||
this.micFailed && this.micFailed(error)
|
this.micFailed && this.micFailed(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isMp3 () {
|
||||||
|
return this.format.toLowerCase() === 'mp3'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/lib/wav-encoder.js
Normal file
65
src/lib/wav-encoder.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
export default class {
|
||||||
|
constructor (options) {
|
||||||
|
this.bufferSize = options.bufferSize || 4096
|
||||||
|
this.sampleRate = options.sampleRate
|
||||||
|
this.samples = options.samples
|
||||||
|
}
|
||||||
|
|
||||||
|
finish () {
|
||||||
|
this._joinSamples()
|
||||||
|
|
||||||
|
let buffer = new ArrayBuffer(44 + this.samples.length * 2)
|
||||||
|
let view = new DataView(buffer)
|
||||||
|
|
||||||
|
this._writeString(view, 0, 'RIFF') // RIFF identifier
|
||||||
|
view.setUint32(4, 36 + this.samples.length * 2, true) // RIFF chunk length
|
||||||
|
this._writeString(view, 8, 'WAVE') // RIFF type
|
||||||
|
this._writeString(view, 12, 'fmt ') // format chunk identifier
|
||||||
|
view.setUint32(16, 16, true) // format chunk length
|
||||||
|
view.setUint16(20, 1, true) // sample format (raw)
|
||||||
|
view.setUint16(22, 1, true) // channel count
|
||||||
|
view.setUint32(24, this.sampleRate, true) // sample rate
|
||||||
|
view.setUint32(28, this.sampleRate * 4, true) // byte rate (sample rate * block align)
|
||||||
|
view.setUint16(32, 4, true) // block align (channel count * bytes per sample)
|
||||||
|
view.setUint16(34, 16, true) // bits per sample
|
||||||
|
this._writeString(view, 36, 'data') // data chunk identifier
|
||||||
|
view.setUint32(40, this.samples.length * 2, true) // data chunk length
|
||||||
|
|
||||||
|
this._floatTo16BitPCM(view, 44, this.samples)
|
||||||
|
|
||||||
|
const blob = new Blob([view], {type: 'audio/wav'})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id : Date.now(),
|
||||||
|
blob : blob,
|
||||||
|
url : URL.createObjectURL(blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_floatTo16BitPCM (output, offset, input) {
|
||||||
|
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||||
|
let s = Math.max(-1, Math.min(1, input[i]))
|
||||||
|
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_joinSamples () {
|
||||||
|
let recordLength = this.samples.length * this.bufferSize
|
||||||
|
let joinedSamples = new Float64Array(recordLength)
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < this.samples.length; i++) {
|
||||||
|
let sample = this.samples[i]
|
||||||
|
joinedSamples.set(sample, offset)
|
||||||
|
offset += sample.length
|
||||||
|
}
|
||||||
|
|
||||||
|
this.samples = joinedSamples
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeString (view, offset, string) {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
filename : { type: String, default: 'record' },
|
filename : { type: String, default: 'record' },
|
||||||
|
format : { type: String, default: 'mp3' },
|
||||||
headers : { type: Object, default: () => ({}) },
|
headers : { type: Object, default: () => ({}) },
|
||||||
uploadUrl : { type: String }
|
uploadUrl : { type: String }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: .2s;
|
transition: .2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
&--no-border {
|
&--no-border {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user