allow MFM authoring
This commit is contained in:
parent
8f1aaf617a
commit
ed42c476fb
14 changed files with 464 additions and 15 deletions
|
@ -8,6 +8,7 @@
|
||||||
<link rel="stylesheet" href="/static/font/css/animation.css">
|
<link rel="stylesheet" href="/static/font/css/animation.css">
|
||||||
<link rel="stylesheet" href="/static/font/tiresias.css">
|
<link rel="stylesheet" href="/static/font/tiresias.css">
|
||||||
<link rel="stylesheet" href="/static/font/css/lato.css">
|
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||||
|
<link rel="stylesheet" href="/static/mfm.css">
|
||||||
<!--server-generated-meta-->
|
<!--server-generated-meta-->
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
|
"mfm-js": "^0.22.1",
|
||||||
"parse-link-header": "1.0.1",
|
"parse-link-header": "1.0.1",
|
||||||
"phoenix": "1.6.2",
|
"phoenix": "1.6.2",
|
||||||
"punycode.js": "2.1.0",
|
"punycode.js": "2.1.0",
|
||||||
|
|
264
src/components/mfm_content/mfm_content.jsx
Normal file
264
src/components/mfm_content/mfm_content.jsx
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
import { defineComponent, h } from 'vue'
|
||||||
|
import * as mfm from 'mfm-js'
|
||||||
|
import MentionLink from '../mention_link/mention_link.vue'
|
||||||
|
import mention_link from '../mention_link/mention_link'
|
||||||
|
|
||||||
|
function concat (xss) {
|
||||||
|
return ([]).concat(...xss)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle', 'rotate'];
|
||||||
|
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
status: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
if (!this.status) return null
|
||||||
|
const ast = mfm.parse(this.status.mfm_content, { fnNameList: MFM_TAGS })
|
||||||
|
const validTime = (t) => {
|
||||||
|
if (t == null) return null
|
||||||
|
return t.match(/^[0-9.]+s$/) ? t : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const genEl = (ast) => concat(ast.map((token) => {
|
||||||
|
switch (token.type) {
|
||||||
|
case 'text': {
|
||||||
|
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n')
|
||||||
|
|
||||||
|
const res = []
|
||||||
|
for (const t of text.split('\n')) {
|
||||||
|
res.push(h('br'))
|
||||||
|
res.push(t)
|
||||||
|
}
|
||||||
|
res.shift()
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'bold': {
|
||||||
|
return [h('b', genEl(token.children))]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'strike': {
|
||||||
|
return [h('del', genEl(token.children))]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'italic': {
|
||||||
|
return h('i', {
|
||||||
|
style: 'font-style: oblique;'
|
||||||
|
}, genEl(token.children))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'fn': {
|
||||||
|
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
|
||||||
|
let style
|
||||||
|
switch (token.props.name) {
|
||||||
|
case 'tada': {
|
||||||
|
style = `font-size: 150%;` + 'animation: tada 1s linear infinite both;'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'jelly': {
|
||||||
|
const speed = validTime(token.props.args.speed) || '1s'
|
||||||
|
style = `animation: mfm-rubberBand ${speed} linear infinite both;`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'twitch': {
|
||||||
|
const speed = validTime(token.props.args.speed) || '0.5s'
|
||||||
|
style = `animation: mfm-twitch ${speed} ease infinite;`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'shake': {
|
||||||
|
const speed = validTime(token.props.args.speed) || '0.5s'
|
||||||
|
style = `animation: mfm-shake ${speed} ease infinite;`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'spin': {
|
||||||
|
const direction =
|
||||||
|
token.props.args.left ? 'reverse'
|
||||||
|
: token.props.args.alternate ? 'alternate'
|
||||||
|
: 'normal'
|
||||||
|
const anime =
|
||||||
|
token.props.args.x ? 'mfm-spinX'
|
||||||
|
: token.props.args.y ? 'mfm-spinY'
|
||||||
|
: 'mfm-spin'
|
||||||
|
const speed = validTime(token.props.args.speed) || '1.5s'
|
||||||
|
style = `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'jump': {
|
||||||
|
style = 'animation: mfm-jump 0.75s linear infinite;'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'bounce': {
|
||||||
|
style = 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'flip': {
|
||||||
|
const transform =
|
||||||
|
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)'
|
||||||
|
: token.props.args.v ? 'scaleY(-1)'
|
||||||
|
: 'scaleX(-1)'
|
||||||
|
style = `transform: ${transform};`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'x2': {
|
||||||
|
style = `font-size: 200%;`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'x3': {
|
||||||
|
style = `font-size: 400%;`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'x4': {
|
||||||
|
style = `font-size: 600%;`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'font': {
|
||||||
|
const family =
|
||||||
|
token.props.args.serif ? 'serif'
|
||||||
|
: token.props.args.monospace ? 'monospace'
|
||||||
|
: token.props.args.cursive ? 'cursive'
|
||||||
|
: token.props.args.fantasy ? 'fantasy'
|
||||||
|
: token.props.args.emoji ? 'emoji'
|
||||||
|
: token.props.args.math ? 'math'
|
||||||
|
: null
|
||||||
|
if (family) style = `font-family: ${family};`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'blur': {
|
||||||
|
return h('span', {
|
||||||
|
class: '_mfm_blur_'
|
||||||
|
}, genEl(token.children))
|
||||||
|
}
|
||||||
|
case 'rainbow': {
|
||||||
|
style = 'animation: mfm-rainbow 1s linear infinite;'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'sparkle': {
|
||||||
|
return h(MkSparkle, {}, genEl(token.children))
|
||||||
|
}
|
||||||
|
case 'rotate': {
|
||||||
|
const degrees = parseInt(token.props.args.deg) || '90'
|
||||||
|
style = `transform: rotate(${degrees}deg); transform-origin: center center;`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (style == null) {
|
||||||
|
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']'])
|
||||||
|
} else {
|
||||||
|
return h('span', {
|
||||||
|
style: 'display: inline-block;' + style
|
||||||
|
}, genEl(token.children))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'small': {
|
||||||
|
return [h('small', {
|
||||||
|
style: 'opacity: 0.7;'
|
||||||
|
}, genEl(token.children))]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'center': {
|
||||||
|
return [h('div', {
|
||||||
|
style: 'text-align:center;'
|
||||||
|
}, genEl(token.children))]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'url': {
|
||||||
|
return [h('a', {
|
||||||
|
key: Math.random(),
|
||||||
|
href: token.props.url,
|
||||||
|
rel: 'nofollow noopener'
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'link': {
|
||||||
|
return [h('a', {
|
||||||
|
key: Math.random(),
|
||||||
|
href: token.props.url,
|
||||||
|
rel: 'nofollow noopener'
|
||||||
|
}, genEl(token.children))]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'mention': {
|
||||||
|
const user = this.status.attentions.find((mention) => `@${mention.screen_name}` === token.props.acct || mention.screen_name === token.props.username)
|
||||||
|
if (user) {
|
||||||
|
return [h(MentionLink, {
|
||||||
|
url: user.statusnet_profile_url,
|
||||||
|
content: token.props.acct,
|
||||||
|
userScreenName: token.props.acct
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'hashtag': {
|
||||||
|
return [h('a', {
|
||||||
|
rel: 'noopener noreferrer',
|
||||||
|
target: '_blank',
|
||||||
|
key: token.props.hashtag,
|
||||||
|
href: this.status.tags.find((hash) => hash.name === token.props.hashtag).url
|
||||||
|
}, `#${token.props.hashtag}`)]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'blockCode': {
|
||||||
|
return [h('pre', {
|
||||||
|
key: Math.random(),
|
||||||
|
lang: token.props.lang
|
||||||
|
}, token.props.code)]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'inlineCode': {
|
||||||
|
return [h('pre', {
|
||||||
|
key: Math.random(),
|
||||||
|
code: token.props.code,
|
||||||
|
inline: true
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'quote': {
|
||||||
|
if (!this.nowrap) {
|
||||||
|
return [h('div', {
|
||||||
|
class: 'quote'
|
||||||
|
}, genEl(token.children))]
|
||||||
|
} else {
|
||||||
|
return [h('span', {
|
||||||
|
class: 'quote'
|
||||||
|
}, genEl(token.children))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'emojiCode': {
|
||||||
|
return [h('div', {
|
||||||
|
class: 'still-image emoji img'
|
||||||
|
},
|
||||||
|
[h('img', {
|
||||||
|
key: Math.random(),
|
||||||
|
title: token.props.name,
|
||||||
|
alt: token.props.name,
|
||||||
|
src: this.status.emojis.find((emoji) => emoji.shortcode === token.props.name).static_url
|
||||||
|
})]
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'unicodeEmoji': {
|
||||||
|
return token.props.emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
console.error('unrecognized ast type:', token.type)
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Parse ast to DOM
|
||||||
|
return h('span', genEl(ast))
|
||||||
|
}
|
||||||
|
})
|
|
@ -99,6 +99,11 @@
|
||||||
{{ $t('settings.sensitive_if_subject') }}
|
{{ $t('settings.sensitive_if_subject') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="renderMisskeyMarkdown">
|
||||||
|
{{ $t('settings.render_mfm') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="alwaysShowNewPostButton"
|
path="alwaysShowNewPostButton"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
import fileType from 'src/services/file_type/file_type.service'
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
|
import MFMContent from 'src/components/mfm_content/mfm_content.jsx'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -35,9 +36,11 @@ const StatusContent = {
|
||||||
'toggleShowingLongSubject'
|
'toggleShowingLongSubject'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
|
const { renderMisskeyMarkdown } = this.$store.getters.mergedConfig
|
||||||
return {
|
return {
|
||||||
postLength: this.status.text.length,
|
postLength: this.status.text.length,
|
||||||
parseReadyDone: false
|
parseReadyDone: false,
|
||||||
|
renderMisskeyMarkdown
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -81,7 +84,8 @@ const StatusContent = {
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
RichContent
|
RichContent,
|
||||||
|
MFMContent
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.status.attentions && this.status.attentions.forEach(attn => {
|
this.status.attentions && this.status.attentions.forEach(attn => {
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&.-tall-status {
|
&.-tall-status {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -41,18 +41,26 @@
|
||||||
>
|
>
|
||||||
{{ $t("general.show_more") }}
|
{{ $t("general.show_more") }}
|
||||||
</button>
|
</button>
|
||||||
<RichContent
|
<div
|
||||||
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
|
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
|
||||||
:class="{ '-single-line': singleLine }"
|
>
|
||||||
class="text media-body"
|
<MFMContent
|
||||||
:html="status.raw_html"
|
v-if="renderMisskeyMarkdown && status.mfm_content"
|
||||||
:emoji="status.emojis"
|
class="RichContent text media-body"
|
||||||
:handle-links="true"
|
:status="status"
|
||||||
:greentext="mergedConfig.greentext"
|
/>
|
||||||
:attentions="status.attentions"
|
<RichContent
|
||||||
@parseReady="onParseReady"
|
v-else
|
||||||
/>
|
:class="{ '-single-line': singleLine }"
|
||||||
|
class="text media-body"
|
||||||
|
:html="status.raw_html"
|
||||||
|
:emoji="status.emojis"
|
||||||
|
:handle-links="true"
|
||||||
|
:greentext="mergedConfig.greentext"
|
||||||
|
:attentions="status.attentions"
|
||||||
|
@parseReady="onParseReady"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
v-show="hideSubjectStatus"
|
v-show="hideSubjectStatus"
|
||||||
class="button-unstyled -link cw-status-hider"
|
class="button-unstyled -link cw-status-hider"
|
||||||
|
|
|
@ -209,7 +209,8 @@
|
||||||
"text/plain": "Plain text",
|
"text/plain": "Plain text",
|
||||||
"text/html": "HTML",
|
"text/html": "HTML",
|
||||||
"text/markdown": "Markdown",
|
"text/markdown": "Markdown",
|
||||||
"text/bbcode": "BBCode"
|
"text/bbcode": "BBCode",
|
||||||
|
"text/x.misskeymarkdown": "MFM"
|
||||||
},
|
},
|
||||||
"content_warning": "Subject (optional)",
|
"content_warning": "Subject (optional)",
|
||||||
"default": "Just landed in L.A.",
|
"default": "Just landed in L.A.",
|
||||||
|
@ -507,6 +508,7 @@
|
||||||
"post_status_content_type": "Post status content type",
|
"post_status_content_type": "Post status content type",
|
||||||
"sensitive_by_default": "Mark posts as sensitive by default",
|
"sensitive_by_default": "Mark posts as sensitive by default",
|
||||||
"sensitive_if_subject": "Automatically mark images as sensitive if a subject line is specified",
|
"sensitive_if_subject": "Automatically mark images as sensitive if a subject line is specified",
|
||||||
|
"render_mfm": "Render Misskey Markdown",
|
||||||
"useStreamingApiWarning": "It's cool use it. If it breaks refresh I guess?",
|
"useStreamingApiWarning": "It's cool use it. If it breaks refresh I guess?",
|
||||||
"stop_gifs": "Pause animated images until you hover on them",
|
"stop_gifs": "Pause animated images until you hover on them",
|
||||||
"streaming": "Automatically show new posts when scrolled to the top",
|
"streaming": "Automatically show new posts when scrolled to the top",
|
||||||
|
|
|
@ -582,6 +582,7 @@
|
||||||
"greentext": "引用を緑色で表示",
|
"greentext": "引用を緑色で表示",
|
||||||
"sensitive_by_default": "はじめから投稿をセンシティブとして設定",
|
"sensitive_by_default": "はじめから投稿をセンシティブとして設定",
|
||||||
"sensitive_if_subject": "ステータスにサブジェクトをついたらNSFWにする",
|
"sensitive_if_subject": "ステータスにサブジェクトをついたらNSFWにする",
|
||||||
|
"render_mfm": "Misskey Markdownを表示",
|
||||||
"more_settings": "その他の設定",
|
"more_settings": "その他の設定",
|
||||||
"reply_visibility_self_short": "自分宛のリプライを見る",
|
"reply_visibility_self_short": "自分宛のリプライを見る",
|
||||||
"reply_visibility_following_short": "フォローしている人に宛てられたリプライを見る",
|
"reply_visibility_following_short": "フォローしている人に宛てられたリプライを見る",
|
||||||
|
|
|
@ -95,6 +95,7 @@ export const defaultState = {
|
||||||
virtualScrolling: undefined, // instance default
|
virtualScrolling: undefined, // instance default
|
||||||
sensitiveByDefault: undefined, // instance default
|
sensitiveByDefault: undefined, // instance default
|
||||||
sensitiveIfSubject: undefined,
|
sensitiveIfSubject: undefined,
|
||||||
|
renderMisskeyMarkdown: undefined,
|
||||||
conversationDisplay: undefined, // instance default
|
conversationDisplay: undefined, // instance default
|
||||||
conversationTreeAdvanced: undefined, // instance default
|
conversationTreeAdvanced: undefined, // instance default
|
||||||
conversationOtherRepliesButton: undefined, // instance default
|
conversationOtherRepliesButton: undefined, // instance default
|
||||||
|
|
|
@ -55,6 +55,7 @@ const defaultState = {
|
||||||
virtualScrolling: true,
|
virtualScrolling: true,
|
||||||
sensitiveByDefault: false,
|
sensitiveByDefault: false,
|
||||||
sensitiveIfSubject: false,
|
sensitiveIfSubject: false,
|
||||||
|
renderMisskeyMarkdown: false,
|
||||||
conversationDisplay: 'linear',
|
conversationDisplay: 'linear',
|
||||||
conversationTreeAdvanced: false,
|
conversationTreeAdvanced: false,
|
||||||
conversationOtherRepliesButton: 'below',
|
conversationOtherRepliesButton: 'below',
|
||||||
|
|
|
@ -280,6 +280,15 @@ export const parseStatus = (data) => {
|
||||||
output.summary = data.spoiler_text
|
output.summary = data.spoiler_text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.akkoma) {
|
||||||
|
const { akkoma } = data
|
||||||
|
if (akkoma && akkoma.source && akkoma.source.mediaType === 'text/x.misskeymarkdown') {
|
||||||
|
output.mfm_content = akkoma.source.content
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.mfm_content = null
|
||||||
|
}
|
||||||
|
|
||||||
output.in_reply_to_status_id = data.in_reply_to_id
|
output.in_reply_to_status_id = data.in_reply_to_id
|
||||||
output.in_reply_to_user_id = data.in_reply_to_account_id
|
output.in_reply_to_user_id = data.in_reply_to_account_id
|
||||||
output.replies_count = data.replies_count
|
output.replies_count = data.replies_count
|
||||||
|
|
139
static/mfm.css
Normal file
139
static/mfm.css
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
@keyframes tada {
|
||||||
|
from {
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
10%,
|
||||||
|
20% {
|
||||||
|
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
30%,
|
||||||
|
50%,
|
||||||
|
70%,
|
||||||
|
90% {
|
||||||
|
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
40%,
|
||||||
|
60%,
|
||||||
|
80% {
|
||||||
|
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0% {
|
||||||
|
transform: scaleX(0.9) scaleY(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
19% {
|
||||||
|
transform: scaleX(1.1) scaleY(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
48% {
|
||||||
|
transform: scaleX(0.95) scaleY(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scaleX(1) scaleY(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-spinX {
|
||||||
|
0% { transform: perspective(128px) rotateX(0deg); }
|
||||||
|
100% { transform: perspective(128px) rotateX(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-spinY {
|
||||||
|
0% { transform: perspective(128px) rotateY(0deg); }
|
||||||
|
100% { transform: perspective(128px) rotateY(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-jump {
|
||||||
|
0% { transform: translateY(0); }
|
||||||
|
25% { transform: translateY(-16px); }
|
||||||
|
50% { transform: translateY(0); }
|
||||||
|
75% { transform: translateY(-8px); }
|
||||||
|
100% { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-bounce {
|
||||||
|
0% { transform: translateY(0) scale(1, 1); }
|
||||||
|
25% { transform: translateY(-16px) scale(1, 1); }
|
||||||
|
50% { transform: translateY(0) scale(1, 1); }
|
||||||
|
75% { transform: translateY(0) scale(1.5, 0.75); }
|
||||||
|
100% { transform: translateY(0) scale(1, 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-twitch {
|
||||||
|
0% { transform: translate(7px, -2px); }
|
||||||
|
5% { transform: translate(-3px, 1px); }
|
||||||
|
10% { transform: translate(-7px, -1px); }
|
||||||
|
15% { transform: translate(0, -1px); }
|
||||||
|
20% { transform: translate(-8px, 6px); }
|
||||||
|
25% { transform: translate(-4px, -3px); }
|
||||||
|
30% { transform: translate(-4px, -6px); }
|
||||||
|
35% { transform: translate(-8px, -8px); }
|
||||||
|
40% { transform: translate(4px, 6px); }
|
||||||
|
45% { transform: translate(-3px, 1px); }
|
||||||
|
50% { transform: translate(2px, -10px); }
|
||||||
|
55% { transform: translate(-7px, 0); }
|
||||||
|
60% { transform: translate(-2px, 4px); }
|
||||||
|
65% { transform: translate(3px, -8px); }
|
||||||
|
70% { transform: translate(6px, 7px); }
|
||||||
|
75% { transform: translate(-7px, -2px); }
|
||||||
|
80% { transform: translate(-7px, -8px); }
|
||||||
|
85% { transform: translate(9px, 3px); }
|
||||||
|
90% { transform: translate(-3px, -2px); }
|
||||||
|
95% { transform: translate(-10px, 2px); }
|
||||||
|
100% { transform: translate(-2px, -6px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-shake {
|
||||||
|
0% { transform: translate(-3px, -1px) rotate(-8deg); }
|
||||||
|
5% { transform: translate(0, -1px) rotate(-10deg); }
|
||||||
|
10% { transform: translate(1px, -3px) rotate(0deg); }
|
||||||
|
15% { transform: translate(1px, 1px) rotate(11deg); }
|
||||||
|
20% { transform: translate(-2px, 1px) rotate(1deg); }
|
||||||
|
25% { transform: translate(-1px, -2px) rotate(-2deg); }
|
||||||
|
30% { transform: translate(-1px, 2px) rotate(-3deg); }
|
||||||
|
35% { transform: translate(2px, 1px) rotate(6deg); }
|
||||||
|
40% { transform: translate(-2px, -3px) rotate(-9deg); }
|
||||||
|
45% { transform: translate(0, -1px) rotate(-12deg); }
|
||||||
|
50% { transform: translate(1px, 2px) rotate(10deg); }
|
||||||
|
55% { transform: translate(0, -3px) rotate(8deg); }
|
||||||
|
60% { transform: translate(1px, -1px) rotate(8deg); }
|
||||||
|
65% { transform: translate(0, -1px) rotate(-7deg); }
|
||||||
|
70% { transform: translate(-1px, -3px) rotate(6deg); }
|
||||||
|
75% { transform: translate(0, -2px) rotate(4deg); }
|
||||||
|
80% { transform: translate(-2px, -1px) rotate(3deg); }
|
||||||
|
85% { transform: translate(1px, -3px) rotate(-10deg); }
|
||||||
|
90% { transform: translate(1px, 0) rotate(3deg); }
|
||||||
|
95% { transform: translate(-2px, 0) rotate(-3deg); }
|
||||||
|
100% { transform: translate(2px, 1px) rotate(2deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-rubberBand {
|
||||||
|
from { transform: scale3d(1, 1, 1); }
|
||||||
|
30% { transform: scale3d(1.25, 0.75, 1); }
|
||||||
|
40% { transform: scale3d(0.75, 1.25, 1); }
|
||||||
|
50% { transform: scale3d(1.15, 0.85, 1); }
|
||||||
|
65% { transform: scale3d(0.95, 1.05, 1); }
|
||||||
|
75% { transform: scale3d(1.05, 0.95, 1); }
|
||||||
|
to { transform: scale3d(1, 1, 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-rainbow {
|
||||||
|
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
|
||||||
|
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
|
||||||
|
}
|
12
yarn.lock
12
yarn.lock
|
@ -6559,6 +6559,13 @@ methods@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||||
|
|
||||||
|
mfm-js@^0.22.1:
|
||||||
|
version "0.22.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.22.1.tgz#ad5f0b95cc903ca5a5e414e2edf64ac4648dc8c2"
|
||||||
|
integrity sha512-UV5zvDKlWPpBFeABhyCzuOTJ3RwrNrmVpJ+zz/dFX6D/ntEywljgxkfsLamcy0ZSwUAr0O+WQxGHvAwyxUgsAQ==
|
||||||
|
dependencies:
|
||||||
|
twemoji-parser "14.0.x"
|
||||||
|
|
||||||
micromatch@^3.1.10, micromatch@^3.1.4:
|
micromatch@^3.1.10, micromatch@^3.1.4:
|
||||||
version "3.1.10"
|
version "3.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
|
||||||
|
@ -9647,6 +9654,11 @@ tty-browserify@0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||||
|
|
||||||
|
twemoji-parser@14.0.x:
|
||||||
|
version "14.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-14.0.0.tgz#13dabcb6d3a261d9efbf58a1666b182033bf2b62"
|
||||||
|
integrity sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==
|
||||||
|
|
||||||
type-check@~0.3.2:
|
type-check@~0.3.2:
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
|
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
|
||||||
|
|
Loading…
Reference in a new issue