From 87c6d0cbeef22a902e4b9827d41b450853b0edde Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Mar 2023 12:29:34 +0900
Subject: [PATCH] :art:

---
 .../frontend/src/components/MkUserPopup.vue   | 222 ++++++++++++++++++
 .../frontend/src/components/MkUserPreview.vue | 199 ----------------
 .../frontend/src/directives/user-preview.ts   |   2 +-
 3 files changed, 223 insertions(+), 200 deletions(-)
 create mode 100644 packages/frontend/src/components/MkUserPopup.vue
 delete mode 100644 packages/frontend/src/components/MkUserPreview.vue

diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
new file mode 100644
index 0000000000..0720c8eea6
--- /dev/null
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -0,0 +1,222 @@
+<template>
+<Transition
+	:enter-active-class="$store.state.animation ? $style.transition_popup_enterActive : ''"
+	:leave-active-class="$store.state.animation ? $style.transition_popup_leaveActive : ''"
+	:enter-from-class="$store.state.animation ? $style.transition_popup_enterFrom : ''"
+	:leave-to-class="$store.state.animation ? $style.transition_popup_leaveTo : ''"
+	appear @after-leave="emit('closed')"
+>
+	<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
+		<div v-if="user != null">
+			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
+				<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ $ts.followsYou }}</span>
+			</div>
+			<svg viewBox="0 0 128 128" :class="$style.avatarBack">
+				<g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
+					<path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/>
+				</g>
+			</svg>
+			<MkAvatar :class="$style.avatar" :user="user" indicator/>
+			<div :class="$style.title">
+				<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
+				<div :class="$style.username"><MkAcct :user="user"/></div>
+			</div>
+			<div :class="$style.description">
+				<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
+				<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
+			</div>
+			<div :class="$style.status">
+				<div :class="$style.statusItem">
+					<div :class="$style.statusItemLabel">{{ $ts.notes }}</div>
+					<div>{{ number(user.notesCount) }}</div>
+				</div>
+				<div :class="$style.statusItem">
+					<div :class="$style.statusItemLabel">{{ $ts.following }}</div>
+					<div>{{ number(user.followingCount) }}</div>
+				</div>
+				<div :class="$style.statusItem">
+					<div :class="$style.statusItemLabel">{{ $ts.followers }}</div>
+					<div>{{ number(user.followersCount) }}</div>
+				</div>
+			</div>
+			<button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button>
+			<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
+		</div>
+		<div v-else>
+			<MkLoading/>
+		</div>
+	</div>
+</Transition>
+</template>
+
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
+import * as misskey from 'misskey-js';
+import MkFollowButton from '@/components/MkFollowButton.vue';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+import { getUserMenu } from '@/scripts/get-user-menu';
+import number from '@/filters/number';
+import { i18n } from '@/i18n';
+
+const props = defineProps<{
+	showing: boolean;
+	q: string;
+	source: HTMLElement;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+	(ev: 'mouseover'): void;
+	(ev: 'mouseleave'): void;
+}>();
+
+const zIndex = os.claimZIndex('middle');
+let user = $ref<misskey.entities.UserDetailed | null>(null);
+let top = $ref(0);
+let left = $ref(0);
+
+function showMenu(ev: MouseEvent) {
+	os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
+}
+
+onMounted(() => {
+	if (typeof props.q === 'object') {
+		user = props.q;
+	} else {
+		const query = props.q.startsWith('@') ?
+			Acct.parse(props.q.substr(1)) :
+			{ userId: props.q };
+
+		os.api('users/show', query).then(res => {
+			if (!props.showing) return;
+			user = res;
+		});
+	}
+
+	const rect = props.source.getBoundingClientRect();
+	const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
+	const y = rect.top + props.source.offsetHeight + window.pageYOffset;
+
+	top = y;
+	left = x;
+});
+</script>
+
+<style lang="scss" module>
+.transition_popup_enterActive,
+.transition_popup_leaveActive {
+	transition: opacity 0.15s, transform 0.15s !important;
+}
+.transition_popup_enterFrom,
+.transition_popup_leaveTo {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.root {
+	position: absolute;
+	width: 300px;
+	overflow: clip;
+	transform-origin: center top;
+}
+
+.banner {
+	height: 78px;
+	background-color: rgba(0, 0, 0, 0.1);
+	background-size: cover;
+	background-position: center;
+}
+
+.followed {
+	position: absolute;
+	top: 12px;
+	left: 12px;
+	padding: 4px 8px;
+	color: #fff;
+	background: rgba(0, 0, 0, 0.7);
+	font-size: 0.7em;
+	border-radius: 6px;
+}
+
+.avatarBack {
+	width: 100px;
+	position: absolute;
+	top: 28px;
+	left: 0;
+	right: 0;
+	margin: 0 auto;
+}
+
+.avatar {
+	display: block;
+	position: absolute;
+	top: 38px;
+	left: 0;
+	right: 0;
+	margin: 0 auto;
+	z-index: 2;
+	width: 58px;
+	height: 58px;
+}
+
+.title {
+	position: relative;
+	z-index: 3;
+	display: block;
+	padding: 8px 26px 16px 26px;
+	margin-top: 16px;
+	text-align: center;
+}
+
+.name {
+	display: inline-block;
+	font-weight: bold;
+	word-break: break-all;
+}
+
+.username {
+	display: block;
+	font-size: 0.8em;
+	opacity: 0.7;
+}
+
+.description {
+	padding: 16px 26px;
+	font-size: 0.8em;
+	text-align: center;
+	border-top: solid 1px var(--divider);
+	border-bottom: solid 1px var(--divider);
+}
+
+.status {
+	padding: 16px 26px 16px 26px;
+}
+
+.statusItem {
+	display: inline-block;
+	width: 33%;
+	text-align: center;
+}
+
+.statusItemLabel {
+	font-size: 0.7em;
+	color: var(--fgTransparentWeak);
+}
+
+.menu {
+	position: absolute;
+	top: 8px;
+	right: 44px;
+	padding: 6px;
+	background: var(--panel);
+	border-radius: 999px;
+}
+
+.follow {
+	position: absolute;
+	top: 8px;
+	right: 8px;
+}
+</style>
diff --git a/packages/frontend/src/components/MkUserPreview.vue b/packages/frontend/src/components/MkUserPreview.vue
deleted file mode 100644
index 1086a2c651..0000000000
--- a/packages/frontend/src/components/MkUserPreview.vue
+++ /dev/null
@@ -1,199 +0,0 @@
-<template>
-<Transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="emit('closed')">
-	<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
-		<div v-if="user != null" class="info">
-			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
-				<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
-			</div>
-			<MkAvatar class="avatar" :user="user" indicator/>
-			<div class="title">
-				<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
-				<p class="username"><MkAcct :user="user"/></p>
-			</div>
-			<div class="description">
-				<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
-			</div>
-			<div class="status">
-				<div>
-					<p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span>
-				</div>
-				<div>
-					<p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span>
-				</div>
-				<div>
-					<p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span>
-				</div>
-			</div>
-			<button class="menu _button" @click="showMenu"><i class="ti ti-dots"></i></button>
-			<MkFollowButton v-if="$i && user.id != $i.id" class="koudoku-button" :user="user" mini/>
-		</div>
-		<div v-else>
-			<MkLoading/>
-		</div>
-	</div>
-</Transition>
-</template>
-
-<script lang="ts" setup>
-import { onMounted } from 'vue';
-import * as Acct from 'misskey-js/built/acct';
-import * as misskey from 'misskey-js';
-import MkFollowButton from '@/components/MkFollowButton.vue';
-import { userPage } from '@/filters/user';
-import * as os from '@/os';
-import { getUserMenu } from '@/scripts/get-user-menu';
-
-const props = defineProps<{
-	showing: boolean;
-	q: string;
-	source: HTMLElement;
-}>();
-
-const emit = defineEmits<{
-	(ev: 'closed'): void;
-	(ev: 'mouseover'): void;
-	(ev: 'mouseleave'): void;
-}>();
-
-const zIndex = os.claimZIndex('middle');
-let user = $ref<misskey.entities.UserDetailed | null>(null);
-let top = $ref(0);
-let left = $ref(0);
-
-function showMenu(ev: MouseEvent) {
-	os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
-}
-
-onMounted(() => {
-	if (typeof props.q === 'object') {
-		user = props.q;
-	} else {
-		const query = props.q.startsWith('@') ?
-			Acct.parse(props.q.substr(1)) :
-			{ userId: props.q };
-
-		os.api('users/show', query).then(res => {
-			if (!props.showing) return;
-			user = res;
-		});
-	}
-
-	const rect = props.source.getBoundingClientRect();
-	const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
-	const y = rect.top + props.source.offsetHeight + window.pageYOffset;
-
-	top = y;
-	left = x;
-});
-</script>
-
-<style lang="scss" scoped>
-.popup-enter-active, .popup-leave-active {
-	transition: opacity 0.15s, transform 0.15s !important;
-}
-.popup-enter-from, .popup-leave-to {
-	opacity: 0;
-	transform: scale(0.9);
-}
-
-.fxxzrfni {
-	position: absolute;
-	width: 300px;
-	overflow: hidden;
-	transform-origin: center top;
-
-	> .info {
-		> .banner {
-			height: 84px;
-			background-color: rgba(0, 0, 0, 0.1);
-			background-size: cover;
-			background-position: center;
-			> .followed {
-					position: absolute;
-					top: 12px;
-					left: 12px;
-					padding: 4px 8px;
-					color: #fff;
-					background: rgba(0, 0, 0, 0.7);
-					font-size: 0.7em;
-					border-radius: 6px;
-			}
-		}
-
-		> .avatar {
-			display: block;
-			position: absolute;
-			top: 62px;
-			left: 13px;
-			z-index: 2;
-			width: 58px;
-			height: 58px;
-			border: solid 3px var(--face);
-			border-radius: 8px;
-		}
-
-		> .title {
-			display: block;
-			padding: 8px 0 8px 82px;
-
-			> .name {
-				display: inline-block;
-				margin: 0;
-				font-weight: bold;
-				line-height: 16px;
-				word-break: break-all;
-			}
-
-			> .username {
-				display: block;
-				margin: 0;
-				line-height: 16px;
-				font-size: 0.8em;
-				color: var(--fg);
-				opacity: 0.7;
-			}
-		}
-
-		> .description {
-			padding: 0 16px;
-			font-size: 0.8em;
-			color: var(--fg);
-		}
-
-		> .status {
-			padding: 8px 16px;
-
-			> div {
-				display: inline-block;
-				width: 33%;
-
-				> p {
-					margin: 0;
-					font-size: 0.7em;
-					color: var(--fg);
-				}
-
-				> span {
-					font-size: 1em;
-					color: var(--accent);
-				}
-			}
-		}
-
-		> .menu {
-			position: absolute;
-			top: 8px;
-			right: 44px;
-			padding: 6px;
-			background: var(--panel);
-			border-radius: 999px;
-		}
-
-		> .koudoku-button {
-			position: absolute;
-			top: 8px;
-			right: 8px;
-		}
-	}
-}
-</style>
diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts
index ed5f00ca65..2f5936de3d 100644
--- a/packages/frontend/src/directives/user-preview.ts
+++ b/packages/frontend/src/directives/user-preview.ts
@@ -24,7 +24,7 @@ export class UserPreview {
 
 		const showing = ref(true);
 
-		popup(defineAsyncComponent(() => import('@/components/MkUserPreview.vue')), {
+		popup(defineAsyncComponent(() => import('@/components/MkUserPopup.vue')), {
 			showing,
 			q: this.user,
 			source: this.el,