summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/global
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-06-05 19:47:08 +0900
committerGitHub <noreply@github.com>2023-06-05 19:47:08 +0900
commit407a965c1d78db9b13ec89a7be910b3c120aafcf (patch)
tree33e00f7a00c4e33b2c95a6e2aba85cea7b9f05f6 /packages/frontend/src/components/global
parentMerge pull request #10833 from misskey-dev/develop (diff)
parentMerge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff)
downloadmisskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.tar.gz
misskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.tar.bz2
misskey-407a965c1d78db9b13ec89a7be910b3c120aafcf.zip
Merge pull request #10932 from misskey-dev/develop
Release: 13.13.0
Diffstat (limited to 'packages/frontend/src/components/global')
-rw-r--r--packages/frontend/src/components/global/MkA.vue12
-rw-r--r--packages/frontend/src/components/global/MkAcct.vue2
-rw-r--r--packages/frontend/src/components/global/MkAd.vue12
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue3
-rw-r--r--packages/frontend/src/components/global/MkCondensedLine.vue9
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue4
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts367
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue171
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue4
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue4
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.vue9
-rw-r--r--packages/frontend/src/components/global/MkTime.vue1
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue2
-rw-r--r--packages/frontend/src/components/global/MkUserName.vue2
-rw-r--r--packages/frontend/src/components/global/i18n.ts58
16 files changed, 423 insertions, 239 deletions
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 40d134dffb..4e608c6efe 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -15,7 +15,7 @@ import { useRouter } from '@/router';
const props = withDefaults(defineProps<{
to: string;
activeClass?: null | string;
- behavior?: null | 'window' | 'browser' | 'modalWindow';
+ behavior?: null | 'window' | 'browser';
}>(), {
activeClass: null,
behavior: null,
@@ -70,14 +70,6 @@ function openWindow() {
os.pageWindow(props.to);
}
-function modalWindow() {
- os.modalPageWindow(props.to);
-}
-
-function popout() {
- popout_(props.to);
-}
-
function nav(ev: MouseEvent) {
if (props.behavior === 'browser') {
location.href = props.to;
@@ -87,8 +79,6 @@ function nav(ev: MouseEvent) {
if (props.behavior) {
if (props.behavior === 'window') {
return openWindow();
- } else if (props.behavior === 'modalWindow') {
- return modalWindow();
}
}
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index 59358aef70..f93659f5ed 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -1,5 +1,5 @@
<template>
-<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :min-scale="2 / 3">
+<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3">
<span>@{{ user.username }}</span>
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
</MkCondensedLine>
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index aa975600f0..8b25ab1b6a 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -1,6 +1,14 @@
<template>
<div v-if="chosen && !shouldHide" :class="$style.root">
- <div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]">
+ <div
+ v-if="!showMenu"
+ :class="[$style.main, {
+ [$style.form_square]: chosen.place === 'square',
+ [$style.form_horizontal]: chosen.place === 'horizontal',
+ [$style.form_horizontalBig]: chosen.place === 'horizontal-big',
+ [$style.form_vertical]: chosen.place === 'vertical',
+ }]"
+ >
<a :href="chosen.url" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
@@ -122,7 +130,7 @@ function reduceFrequency(): void {
}
}
- &.form_horizontal-big {
+ &.form_horizontalBig {
padding: 8px;
> .link,
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 42abdcbdcc..422b35c9dd 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -1,6 +1,6 @@
<template>
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
- <img :class="$style.inner" :src="url" decoding="async"/>
+ <MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
<div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft">
@@ -24,6 +24,7 @@
<script lang="ts" setup>
import { watch } from 'vue';
import * as misskey from 'misskey-js';
+import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
import { getStaticImageUrl } from '@/scripts/media-proxy';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue
index 1d46ff1ec9..4b2e8e4750 100644
--- a/packages/frontend/src/components/global/MkCondensedLine.vue
+++ b/packages/frontend/src/components/global/MkCondensedLine.vue
@@ -13,13 +13,20 @@ interface Props {
const contentSymbol = Symbol();
const observer = new ResizeObserver((entries) => {
+ const results: {
+ container: HTMLSpanElement;
+ transform: string;
+ }[] = [];
for (const entry of entries) {
const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement;
const props: Required<Props> = content[contentSymbol];
const container = content.parentElement as HTMLSpanElement;
const contentWidth = content.getBoundingClientRect().width;
const containerWidth = container.getBoundingClientRect().width;
- container.style.transform = `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})`;
+ results.push({ container, transform: `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})` });
+ }
+ for (const result of results) {
+ result.container.style.transform = result.transform;
}
});
</script>
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 0cb31ffcba..e8a7f17cc6 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -7,7 +7,7 @@
import { computed } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy';
import { defaultStore } from '@/store';
-import { customEmojis } from '@/custom-emojis';
+import { customEmojisMap } from '@/custom-emojis';
const props = defineProps<{
name: string;
@@ -26,7 +26,7 @@ const rawUrl = computed(() => {
return props.url;
}
if (isLocal.value) {
- return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null;
+ return customEmojisMap.get(customEmojiName.value)?.url ?? null;
}
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
});
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
index f6811b6747..685b3b8b8e 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
+import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.ts';
export const Default = {
render(args) {
return {
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
new file mode 100644
index 0000000000..2a50a34390
--- /dev/null
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -0,0 +1,367 @@
+import { VNode, h } from 'vue';
+import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
+import MkUrl from '@/components/global/MkUrl.vue';
+import MkLink from '@/components/MkLink.vue';
+import MkMention from '@/components/MkMention.vue';
+import MkEmoji from '@/components/global/MkEmoji.vue';
+import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
+import MkCode from '@/components/MkCode.vue';
+import MkGoogle from '@/components/MkGoogle.vue';
+import MkSparkle from '@/components/MkSparkle.vue';
+import MkA from '@/components/global/MkA.vue';
+import { host } from '@/config';
+import { defaultStore } from '@/store';
+
+const QUOTE_STYLE = `
+display: block;
+margin: 8px;
+padding: 6px 0 6px 12px;
+color: var(--fg);
+border-left: solid 3px var(--fg);
+opacity: 0.7;
+`.split('\n').join(' ');
+
+export default function(props: {
+ text: string;
+ plain?: boolean;
+ nowrap?: boolean;
+ author?: Misskey.entities.UserLite;
+ i?: Misskey.entities.UserLite;
+ isNote?: boolean;
+ emojiUrls?: string[];
+ rootScale?: number;
+}) {
+ const isNote = props.isNote !== undefined ? props.isNote : true;
+
+ if (props.text == null || props.text === '') return;
+
+ const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
+
+ const validTime = (t: string | null | undefined) => {
+ if (t == null) return null;
+ return t.match(/^[0-9.]+s$/) ? t : null;
+ };
+
+ const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
+
+ /**
+ * Gen Vue Elements from MFM AST
+ * @param ast MFM AST
+ * @param scale How times large the text is
+ */
+ const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
+ switch (token.type) {
+ case 'text': {
+ const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+
+ if (!props.plain) {
+ const res: (VNode | string)[] = [];
+ for (const t of text.split('\n')) {
+ res.push(h('br'));
+ res.push(t);
+ }
+ res.shift();
+ return res;
+ } else {
+ return [text.replace(/\n/g, ' ')];
+ }
+ }
+
+ case 'bold': {
+ return [h('b', genEl(token.children, scale))];
+ }
+
+ case 'strike': {
+ return [h('del', genEl(token.children, scale))];
+ }
+
+ case 'italic': {
+ return h('i', {
+ style: 'font-style: oblique;',
+ }, genEl(token.children, scale));
+ }
+
+ case 'fn': {
+ // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
+ let style;
+ switch (token.props.name) {
+ case 'tada': {
+ const speed = validTime(token.props.args.speed) ?? '1s';
+ style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
+ break;
+ }
+ case 'jelly': {
+ const speed = validTime(token.props.args.speed) ?? '1s';
+ style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
+ break;
+ }
+ case 'twitch': {
+ const speed = validTime(token.props.args.speed) ?? '0.5s';
+ style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
+ break;
+ }
+ case 'shake': {
+ const speed = validTime(token.props.args.speed) ?? '0.5s';
+ style = useAnim ? `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 = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
+ break;
+ }
+ case 'jump': {
+ const speed = validTime(token.props.args.speed) ?? '0.75s';
+ style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
+ break;
+ }
+ case 'bounce': {
+ const speed = validTime(token.props.args.speed) ?? '0.75s';
+ style = useAnim ? `animation: mfm-bounce ${speed} 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': {
+ return h('span', {
+ class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
+ }, genEl(token.children, scale * 2));
+ }
+ case 'x3': {
+ return h('span', {
+ class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
+ }, genEl(token.children, scale * 3));
+ }
+ case 'x4': {
+ return h('span', {
+ class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
+ }, genEl(token.children, scale * 4));
+ }
+ 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, scale));
+ }
+ case 'rainbow': {
+ const speed = validTime(token.props.args.speed) ?? '1s';
+ style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
+ break;
+ }
+ case 'sparkle': {
+ if (!useAnim) {
+ return genEl(token.children, scale);
+ }
+ return h(MkSparkle, {}, genEl(token.children, scale));
+ }
+ case 'rotate': {
+ const degrees = parseFloat(token.props.args.deg ?? '90');
+ style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
+ break;
+ }
+ case 'position': {
+ if (!defaultStore.state.advancedMfm) break;
+ const x = parseFloat(token.props.args.x ?? '0');
+ const y = parseFloat(token.props.args.y ?? '0');
+ style = `transform: translateX(${x}em) translateY(${y}em);`;
+ break;
+ }
+ case 'scale': {
+ if (!defaultStore.state.advancedMfm) {
+ style = '';
+ break;
+ }
+ const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
+ const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
+ style = `transform: scale(${x}, ${y});`;
+ scale = scale * Math.max(x, y);
+ break;
+ }
+ case 'fg': {
+ let color = token.props.args.color;
+ if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+ style = `color: #${color};`;
+ break;
+ }
+ case 'bg': {
+ let color = token.props.args.color;
+ if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+ style = `background-color: #${color};`;
+ break;
+ }
+ }
+ if (style == null) {
+ return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
+ } else {
+ return h('span', {
+ style: 'display: inline-block; ' + style,
+ }, genEl(token.children, scale));
+ }
+ }
+
+ case 'small': {
+ return [h('small', {
+ style: 'opacity: 0.7;',
+ }, genEl(token.children, scale))];
+ }
+
+ case 'center': {
+ return [h('div', {
+ style: 'text-align:center;',
+ }, genEl(token.children, scale))];
+ }
+
+ case 'url': {
+ return [h(MkUrl, {
+ key: Math.random(),
+ url: token.props.url,
+ rel: 'nofollow noopener',
+ })];
+ }
+
+ case 'link': {
+ return [h(MkLink, {
+ key: Math.random(),
+ url: token.props.url,
+ rel: 'nofollow noopener',
+ }, genEl(token.children, scale))];
+ }
+
+ case 'mention': {
+ return [h(MkMention, {
+ key: Math.random(),
+ host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) || host,
+ username: token.props.username,
+ })];
+ }
+
+ case 'hashtag': {
+ return [h(MkA, {
+ key: Math.random(),
+ to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
+ style: 'color:var(--hashtag);',
+ }, `#${token.props.hashtag}`)];
+ }
+
+ case 'blockCode': {
+ return [h(MkCode, {
+ key: Math.random(),
+ code: token.props.code,
+ lang: token.props.lang,
+ })];
+ }
+
+ case 'inlineCode': {
+ return [h(MkCode, {
+ key: Math.random(),
+ code: token.props.code,
+ inline: true,
+ })];
+ }
+
+ case 'quote': {
+ if (!props.nowrap) {
+ return [h('div', {
+ style: QUOTE_STYLE,
+ }, genEl(token.children, scale))];
+ } else {
+ return [h('span', {
+ style: QUOTE_STYLE,
+ }, genEl(token.children, scale))];
+ }
+ }
+
+ case 'emojiCode': {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (props.author?.host == null) {
+ return [h(MkCustomEmoji, {
+ key: Math.random(),
+ name: token.props.name,
+ normal: props.plain,
+ host: null,
+ useOriginalSize: scale >= 2.5,
+ })];
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
+ return [h('span', `:${token.props.name}:`)];
+ } else {
+ return [h(MkCustomEmoji, {
+ key: Math.random(),
+ name: token.props.name,
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ url: props.emojiUrls ? props.emojiUrls[token.props.name] : null,
+ normal: props.plain,
+ host: props.author.host,
+ useOriginalSize: scale >= 2.5,
+ })];
+ }
+ }
+ }
+
+ case 'unicodeEmoji': {
+ return [h(MkEmoji, {
+ key: Math.random(),
+ emoji: token.props.emoji,
+ })];
+ }
+
+ case 'mathInline': {
+ return [h('code', token.props.formula)];
+ }
+
+ case 'mathBlock': {
+ return [h('code', token.props.formula)];
+ }
+
+ case 'search': {
+ return [h(MkGoogle, {
+ key: Math.random(),
+ q: token.props.query,
+ })];
+ }
+
+ case 'plain': {
+ return [h('span', genEl(token.children, scale))];
+ }
+
+ default: {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ console.error('unrecognized ast type:', (token as any).type);
+
+ return [];
+ }
+ }
+ }).flat(Infinity) as (VNode | string)[];
+
+ return h('span', {
+ // https://codeday.me/jp/qa/20190424/690106.html
+ style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
+ }, genEl(ast, props.rootScale ?? 1));
+}
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue
deleted file mode 100644
index 28a0d1c986..0000000000
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-<template>
-<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import MfmCore from '@/components/mfm';
-
-const props = withDefaults(defineProps<{
- text: string;
- plain?: boolean;
- nowrap?: boolean;
- author?: any;
- isNote?: boolean;
-}>(), {
- plain: false,
- nowrap: false,
- author: null,
- isNote: true,
-});
-</script>
-
-<style lang="scss">
-._mfm_blur_ {
- filter: blur(6px);
- transition: filter 0.3s;
-
- &:hover {
- filter: blur(0px);
- }
-}
-
-.mfm-x2 {
- --mfm-zoom-size: 200%;
-}
-
-.mfm-x3 {
- --mfm-zoom-size: 400%;
-}
-
-.mfm-x4 {
- --mfm-zoom-size: 600%;
-}
-
-.mfm-x2, .mfm-x3, .mfm-x4 {
- font-size: var(--mfm-zoom-size);
-
- .mfm-x2, .mfm-x3, .mfm-x4 {
- /* only half effective */
- font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
-
- .mfm-x2, .mfm-x3, .mfm-x4 {
- /* disabled */
- font-size: 100%;
- }
- }
-}
-
-@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); }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-twitch {
- 0% { transform: translate(7px, -2px) }
- 5% { transform: translate(-3px, 1px) }
- 10% { transform: translate(-7px, -1px) }
- 15% { transform: translate(0px, -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, 0px) }
- 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) }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-shake {
- 0% { transform: translate(-3px, -1px) rotate(-8deg) }
- 5% { transform: translate(0px, -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(0px, -1px) rotate(-12deg) }
- 50% { transform: translate(1px, 2px) rotate(10deg) }
- 55% { transform: translate(0px, -3px) rotate(8deg) }
- 60% { transform: translate(1px, -1px) rotate(8deg) }
- 65% { transform: translate(0px, -1px) rotate(-7deg) }
- 70% { transform: translate(-1px, -3px) rotate(6deg) }
- 75% { transform: translate(0px, -2px) rotate(4deg) }
- 80% { transform: translate(-2px, -1px) rotate(3deg) }
- 85% { transform: translate(1px, -3px) rotate(-10deg) }
- 90% { transform: translate(1px, 0px) rotate(3deg) }
- 95% { transform: translate(-2px, 0px) 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%); }
-}
-</style>
-
-<style lang="scss" module>
-.root {
- white-space: pre-wrap;
-
- &.nowrap {
- white-space: pre;
- word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-</style>
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 9e1da64e61..d71343baf9 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -15,8 +15,8 @@
{{ t.title }}
</div>
<Transition
- v-else mode="in-out" @enter="enter" @after-enter="afterEnter" @leave="leave"
- @after-leave="afterLeave"
+ v-else mode="in-out" @enter="enter" @afterEnter="afterEnter" @leave="leave"
+ @afterLeave="afterLeave"
>
<div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div>
</Transition>
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index b91d378b17..0a21d39bca 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -21,7 +21,7 @@
</div>
</div>
</div>
- <XTabs v-if="!narrow || hideTitle" :class="$style.tabs" :tab="tab" :tabs="tabs" :root-el="el" @update:tab="key => emit('update:tab', key)" @tab-click="onTabClick"/>
+ <XTabs v-if="!narrow || hideTitle" :class="$style.tabs" :tab="tab" :tabs="tabs" :rootEl="el" @update:tab="key => emit('update:tab', key)" @tabClick="onTabClick"/>
</template>
<div v-if="(!thin_ && narrow && !hideTitle) || (actions && actions.length > 0)" :class="$style.buttonsRight">
<template v-for="action in actions">
@@ -30,7 +30,7 @@
</div>
</div>
<div v-if="(narrow && !hideTitle) && hasTabs" :class="[$style.lower, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
- <XTabs :class="$style.tabs" :tab="tab" :tabs="tabs" :root-el="el" @update:tab="key => emit('update:tab', key)" @tab-click="onTabClick"/>
+ <XTabs :class="$style.tabs" :tab="tab" :tabs="tabs" :rootEl="el" @update:tab="key => emit('update:tab', key)" @tabClick="onTabClick"/>
</div>
</div>
</template>
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 44c02088da..e5dba54b4e 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -14,6 +14,7 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
+import { $$ } from 'vue/macros';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
const rootEl = $shallowRef<HTMLElement>();
@@ -83,8 +84,8 @@ onMounted(() => {
onUnmounted(() => {
observer.disconnect();
});
-</script>
-
-<style lang="scss" module>
-</style>
+defineExpose({
+ rootEl: $$(rootEl),
+});
+</script>
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 261cc0ee18..dfc3c89798 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -58,7 +58,6 @@ function tick() {
if (props.mode === 'relative' || props.mode === 'detail') {
tick();
-
onUnmounted(() => {
window.clearTimeout(tickId);
});
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 2a92780306..c1efd9a06b 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -6,7 +6,7 @@
<template v-if="!self">
<span :class="$style.schema">{{ schema }}//</span>
<span :class="$style.hostname">{{ hostname }}</span>
- <span v-if="port != ''" :class="$style.port">:{{ port }}</span>
+ <span v-if="port != ''">:{{ port }}</span>
</template>
<template v-if="pathname === '/' && self">
<span :class="$style.self">{{ hostname }}</span>
diff --git a/packages/frontend/src/components/global/MkUserName.vue b/packages/frontend/src/components/global/MkUserName.vue
index 4186a4a4fb..c9e85c5460 100644
--- a/packages/frontend/src/components/global/MkUserName.vue
+++ b/packages/frontend/src/components/global/MkUserName.vue
@@ -1,5 +1,5 @@
<template>
-<Mfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emoji-urls="user.emojis"/>
+<Mfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emojiUrls="user.emojis"/>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts
index 1fd293ba10..2708b759aa 100644
--- a/packages/frontend/src/components/global/i18n.ts
+++ b/packages/frontend/src/components/global/i18n.ts
@@ -1,42 +1,24 @@
-import { h, defineComponent } from 'vue';
+import { h } from 'vue';
-export default defineComponent({
- props: {
- src: {
- type: String,
- required: true,
- },
- tag: {
- type: String,
- required: false,
- default: 'span',
- },
- textTag: {
- type: String,
- required: false,
- default: null,
- },
- },
- render() {
- let str = this.src;
- const parsed = [] as (string | { arg: string; })[];
- while (true) {
- const nextBracketOpen = str.indexOf('{');
- const nextBracketClose = str.indexOf('}');
+export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
+ let str = props.src;
+ const parsed = [] as (string | { arg: string; })[];
+ while (true) {
+ const nextBracketOpen = str.indexOf('{');
+ const nextBracketClose = str.indexOf('}');
- if (nextBracketOpen === -1) {
- parsed.push(str);
- break;
- } else {
- if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
- parsed.push({
- arg: str.substring(nextBracketOpen + 1, nextBracketClose),
- });
- }
-
- str = str.substr(nextBracketClose + 1);
+ if (nextBracketOpen === -1) {
+ parsed.push(str);
+ break;
+ } else {
+ if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
+ parsed.push({
+ arg: str.substring(nextBracketOpen + 1, nextBracketClose),
+ });
}
- return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]()));
- },
-});
+ str = str.substr(nextBracketClose + 1);
+ }
+
+ return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
+}