From fa36b88af41cf96bd975189f30ca5354d14679d9 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 3 Dec 2021 22:09:40 +0900
Subject: [PATCH] refactor(client): refactor ui components

---
 packages/client/src/menu.ts                   |   6 +-
 packages/client/src/os.ts                     |   2 +-
 .../src/ui/_common_/sidebar-for-mobile.vue    | 205 +++++++
 packages/client/src/ui/_common_/sidebar.vue   | 565 ++++++++----------
 packages/client/src/ui/classic.vue            | 141 +----
 packages/client/src/ui/deck.vue               | 289 +++++----
 packages/client/src/ui/universal.vue          | 295 +++++----
 7 files changed, 814 insertions(+), 689 deletions(-)
 create mode 100644 packages/client/src/ui/_common_/sidebar-for-mobile.vue

diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
index ae74740bb8..bd155ba16d 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -1,4 +1,4 @@
-import { computed, ref } from 'vue';
+import { computed, ref, reactive } from 'vue';
 import { search } from '@/scripts/search';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
@@ -7,7 +7,7 @@ import { $i } from './account';
 import { unisonReload } from '@/scripts/unison-reload';
 import { router } from './router';
 
-export const menuDef = {
+export const menuDef = reactive({
 	notifications: {
 		title: 'notifications',
 		icon: 'fas fa-bell',
@@ -221,4 +221,4 @@ export const menuDef = {
 			}*/], ev.currentTarget || ev.target);
 		},
 	},
-};
+});
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 30f6b35964..37b57557c3 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -556,7 +556,7 @@ export function contextMenu(items: any[], ev: MouseEvent) {
 	});
 }
 
-export function post(props: Record<string, any>) {
+export function post(props: Record<string, any> = {}) {
 	return new Promise((resolve, reject) => {
 		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
 		// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
diff --git a/packages/client/src/ui/_common_/sidebar-for-mobile.vue b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
new file mode 100644
index 0000000000..5babdb98a8
--- /dev/null
+++ b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
@@ -0,0 +1,205 @@
+<template>
+<div class="kmwsukvl">
+	<div>
+		<button v-click-anime class="item _button account" @click="openAccountMenu">
+			<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
+		</button>
+		<MkA v-click-anime class="item index" active-class="active" to="/" exact>
+			<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
+		</MkA>
+		<template v-for="item in menu">
+			<div v-if="item === '-'" class="divider"></div>
+			<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
+				<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
+				<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+			</component>
+		</template>
+		<div class="divider"></div>
+		<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
+			<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
+		</MkA>
+		<button v-click-anime class="item _button" @click="more">
+			<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
+			<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
+		</button>
+		<MkA v-click-anime class="item" active-class="active" to="/settings">
+			<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
+		</MkA>
+		<button class="item _button post" data-cy-open-post-form @click="post">
+			<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
+		</button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, ref, toRef, watch } from 'vue';
+import { host } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import { openAccountMenu } from '@/account';
+import { defaultStore } from '@/store';
+
+export default defineComponent({
+	setup(props, context) {
+		const menu = toRef(defaultStore.state, 'menu');
+		const otherMenuItemIndicated = computed(() => {
+			for (const def in menuDef) {
+				if (menu.value.includes(def)) continue;
+				if (menuDef[def].indicated) return true;
+			}
+			return false;
+		});
+
+		return {
+			host: host,
+			accounts: [],
+			connection: null,
+			menu,
+			menuDef: menuDef,
+			otherMenuItemIndicated,
+			post: os.post,
+			search,
+			openAccountMenu,
+			more: () => {
+				os.popup(import('@/components/launch-pad.vue'), {}, {
+				}, 'closed');
+			},
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.kmwsukvl {
+	$ui-font-size: 1em; // TODO: どこかに集約したい
+	$avatar-size: 32px;
+	$avatar-margin: 8px;
+
+	> div {
+
+		> .divider {
+			margin: 16px 16px;
+			border-top: solid 0.5px var(--divider);
+		}
+
+		> .item {
+			position: relative;
+			display: block;
+			padding-left: 24px;
+			font-size: $ui-font-size;
+			line-height: 2.85rem;
+			text-overflow: ellipsis;
+			overflow: hidden;
+			white-space: nowrap;
+			width: 100%;
+			text-align: left;
+			box-sizing: border-box;
+			color: var(--navFg);
+
+			> i {
+				position: relative;
+				width: 32px;
+			}
+
+			> i,
+			> .avatar {
+				margin-right: $avatar-margin;
+			}
+
+			> .avatar {
+				width: $avatar-size;
+				height: $avatar-size;
+				vertical-align: middle;
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 0;
+				left: 20px;
+				color: var(--navIndicator);
+				font-size: 8px;
+				animation: blink 1s infinite;
+			}
+
+			> .text {
+				position: relative;
+				font-size: 0.9em;
+			}
+
+			&:hover {
+				text-decoration: none;
+				color: var(--navHoverFg);
+			}
+
+			&.active {
+				color: var(--navActive);
+			}
+
+			&:hover, &.active {
+				&:before {
+					content: "";
+					display: block;
+					width: calc(100% - 24px);
+					height: 100%;
+					margin: auto;
+					position: absolute;
+					top: 0;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-radius: 999px;
+					background: var(--accentedBg);
+				}
+			}
+
+			&:first-child, &:last-child {
+				position: sticky;
+				z-index: 1;
+				padding-top: 8px;
+				padding-bottom: 8px;
+				background: var(--X14);
+				-webkit-backdrop-filter: var(--blur, blur(8px));
+				backdrop-filter: var(--blur, blur(8px));
+			}
+
+			&:first-child {
+				top: 0;
+
+				&:hover, &.active {
+					&:before {
+						content: none;
+					}
+				}
+			}
+
+			&:last-child {
+				bottom: 0;
+				color: var(--fgOnAccent);
+
+				&:before {
+					content: "";
+					display: block;
+					width: calc(100% - 20px);
+					height: calc(100% - 20px);
+					margin: auto;
+					position: absolute;
+					top: 0;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-radius: 999px;
+					background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+				}
+				
+				&:hover, &.active {
+					&:before {
+						background: var(--accentLighten);
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue
index 6abb21d963..00e95d3663 100644
--- a/packages/client/src/ui/_common_/sidebar.vue
+++ b/packages/client/src/ui/_common_/sidebar.vue
@@ -1,385 +1,300 @@
 <template>
-<div class="mvcprjjd">
-	<transition name="nav-back">
-		<div v-if="showing"
-			class="nav-back _modalBg"
-			@click="showing = false"
-			@touchstart.passive="showing = false"
-		></div>
-	</transition>
-
-	<transition name="nav">
-		<nav v-show="showing" class="nav" :class="{ iconOnly, hidden }">
-			<div>
-				<button v-click-anime class="item _button account" @click="openAccountMenu">
-					<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
-				</button>
-				<MkA v-click-anime class="item index" active-class="active" to="/" exact>
-					<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
-				</MkA>
-				<template v-for="item in menu">
-					<div v-if="item === '-'" class="divider"></div>
-					<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
-						<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
-						<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
-					</component>
-				</template>
-				<div class="divider"></div>
-				<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
-					<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
-				</MkA>
-				<button v-click-anime class="item _button" @click="more">
-					<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
-					<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
-				</button>
-				<MkA v-click-anime class="item" active-class="active" to="/settings">
-					<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
-				</MkA>
-				<button class="item _button post" data-cy-open-post-form @click="post">
-					<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
-				</button>
-			</div>
-		</nav>
-	</transition>
+<div class="mvcprjjd" :class="{ iconOnly }">
+	<div>
+		<button v-click-anime class="item _button account" @click="openAccountMenu">
+			<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
+		</button>
+		<MkA v-click-anime class="item index" active-class="active" to="/" exact>
+			<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
+		</MkA>
+		<template v-for="item in menu">
+			<div v-if="item === '-'" class="divider"></div>
+			<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
+				<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
+				<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+			</component>
+		</template>
+		<div class="divider"></div>
+		<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
+			<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
+		</MkA>
+		<button v-click-anime class="item _button" @click="more">
+			<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
+			<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
+		</button>
+		<MkA v-click-anime class="item" active-class="active" to="/settings">
+			<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
+		</MkA>
+		<button class="item _button post" data-cy-open-post-form @click="post">
+			<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
+		</button>
+	</div>
 </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent, ref, watch } from 'vue';
 import { host } from '@/config';
 import { search } from '@/scripts/search';
 import * as os from '@/os';
 import { menuDef } from '@/menu';
 import { openAccountMenu } from '@/account';
+import { defaultStore } from '@/store';
 
 export default defineComponent({
-	props: {
-		defaultHidden: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
+	setup(props, context) {
+		const iconOnly = ref(false);
 
-	data() {
-		return {
-			host: host,
-			showing: false,
-			accounts: [],
-			connection: null,
-			menuDef: menuDef,
-			iconOnly: false,
-			hidden: this.defaultHidden,
-		};
-	},
-
-	computed: {
-		menu(): string[] {
-			return this.$store.state.menu;
-		},
-
-		otherNavItemIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (this.menu.includes(def)) continue;
-				if (this.menuDef[def].indicated) return true;
+		const menu = computed(() => defaultStore.state.menu);
+		const otherMenuItemIndicated = computed(() => {
+			for (const def in menuDef) {
+				if (menu.value.includes(def)) continue;
+				if (menuDef[def].indicated) return true;
 			}
 			return false;
-		},
+		});
+
+		const calcViewState = () => {
+			iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
+		};
+
+		calcViewState();
+
+		window.addEventListener('resize', calcViewState);
+
+		watch(defaultStore.reactiveState.menuDisplay, () => {
+			calcViewState();
+		});
+
+		return {
+			host: host,
+			accounts: [],
+			connection: null,
+			menu,
+			menuDef: menuDef,
+			otherMenuItemIndicated,
+			iconOnly,
+			post: os.post,
+			search,
+			openAccountMenu,
+			more: () => {
+				os.popup(import('@/components/launch-pad.vue'), {}, {
+				}, 'closed');
+			},
+		};
 	},
-
-	watch: {
-		$route(to, from) {
-			this.showing = false;
-		},
-
-		'$store.reactiveState.menuDisplay.value'() {
-			this.calcViewState();
-		},
-
-		iconOnly() {
-			this.$nextTick(() => {
-				this.$emit('change-view-mode');
-			});
-		},
-
-		hidden() {
-			this.$nextTick(() => {
-				this.$emit('change-view-mode');
-			});
-		}
-	},
-
-	created() {
-		window.addEventListener('resize', this.calcViewState);
-		this.calcViewState();
-	},
-
-	methods: {
-		calcViewState() {
-			this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.menuDisplay === 'sideIcon');
-			if (!this.defaultHidden) {
-				this.hidden = (window.innerWidth <= 650);
-			}
-		},
-
-		show() {
-			this.showing = true;
-		},
-
-		post() {
-			os.post();
-		},
-
-		search() {
-			search();
-		},
-
-		more(ev) {
-			os.popup(import('@/components/launch-pad.vue'), {}, {
-			}, 'closed');
-		},
-
-		openAccountMenu,
-	}
 });
 </script>
 
 <style lang="scss" scoped>
-.nav-enter-active,
-.nav-leave-active {
-	opacity: 1;
-	transform: translateX(0);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.nav-enter-from,
-.nav-leave-active {
-	opacity: 0;
-	transform: translateX(-240px);
-}
-
-.nav-back-enter-active,
-.nav-back-leave-active {
-	opacity: 1;
-	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.nav-back-enter-from,
-.nav-back-leave-active {
-	opacity: 0;
-}
-
 .mvcprjjd {
 	$ui-font-size: 1em; // TODO: どこかに集約したい
 	$nav-width: 250px;
 	$nav-icon-only-width: 86px;
+	$avatar-size: 32px;
+	$avatar-margin: 8px;
 
-	> .nav-back {
+	flex: 0 0 $nav-width;
+	width: $nav-width;
+	box-sizing: border-box;
+
+	> div {
+		position: fixed;
+		top: 0;
+		left: 0;
 		z-index: 1001;
-	}
-
-	> .nav {
-		$avatar-size: 32px;
-		$avatar-margin: 8px;
-
-		flex: 0 0 $nav-width;
 		width: $nav-width;
+		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+		height: calc(var(--vh, 1vh) * 100);
 		box-sizing: border-box;
+		overflow: auto;
+		overflow-x: clip;
+		background: var(--navBg);
 
-		&.iconOnly {
-			flex: 0 0 $nav-icon-only-width;
-			width: $nav-icon-only-width;
-
-			&:not(.hidden) {
-				> div {
-					width: $nav-icon-only-width;
-
-					> .divider {
-						margin: 8px auto;
-						width: calc(100% - 32px);
-					}
-
-					> .item {
-						padding-left: 0;
-						padding: 18px 0;
-						width: 100%;
-						text-align: center;
-						font-size: $ui-font-size * 1.1;
-						line-height: initial;
-
-						> i,
-						> .avatar {
-							display: block;
-							margin: 0 auto;
-						}
-
-						> i {
-							opacity: 0.7;
-						}
-
-						> .text {
-							display: none;
-						}
-
-						&:hover, &.active {
-							> i, > .text {
-								opacity: 1;
-							}
-						}
-
-						&:first-child {
-							margin-bottom: 8px;
-						}
-
-						&:last-child {
-							margin-top: 8px;
-						}
-					}
-				}
-			}
+		> .divider {
+			margin: 16px 16px;
+			border-top: solid 0.5px var(--divider);
 		}
 
-		&.hidden {
-			position: fixed;
-			top: 0;
-			left: 0;
-			z-index: 1001;
-		}
-
-		&:not(.hidden) {
-			display: block !important;
-		}
-
-		> div {
-			position: fixed;
-			top: 0;
-			left: 0;
-			z-index: 1001;
-			width: $nav-width;
-			// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-			height: calc(var(--vh, 1vh) * 100);
+		> .item {
+			position: relative;
+			display: block;
+			padding-left: 24px;
+			font-size: $ui-font-size;
+			line-height: 2.85rem;
+			text-overflow: ellipsis;
+			overflow: hidden;
+			white-space: nowrap;
+			width: 100%;
+			text-align: left;
 			box-sizing: border-box;
-			overflow: auto;
-			overflow-x: clip;
-			background: var(--navBg);
+			color: var(--navFg);
 
-			> .divider {
-				margin: 16px 16px;
-				border-top: solid 0.5px var(--divider);
+			> i {
+				position: relative;
+				width: 32px;
 			}
 
-			> .item {
+			> i,
+			> .avatar {
+				margin-right: $avatar-margin;
+			}
+
+			> .avatar {
+				width: $avatar-size;
+				height: $avatar-size;
+				vertical-align: middle;
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 0;
+				left: 20px;
+				color: var(--navIndicator);
+				font-size: 8px;
+				animation: blink 1s infinite;
+			}
+
+			> .text {
 				position: relative;
-				display: block;
-				padding-left: 24px;
-				font-size: $ui-font-size;
-				line-height: 2.85rem;
-				text-overflow: ellipsis;
-				overflow: hidden;
-				white-space: nowrap;
-				width: 100%;
-				text-align: left;
-				box-sizing: border-box;
-				color: var(--navFg);
+				font-size: 0.9em;
+			}
 
-				> i {
-					position: relative;
-					width: 32px;
-				}
+			&:hover {
+				text-decoration: none;
+				color: var(--navHoverFg);
+			}
 
-				> i,
-				> .avatar {
-					margin-right: $avatar-margin;
-				}
+			&.active {
+				color: var(--navActive);
+			}
 
-				> .avatar {
-					width: $avatar-size;
-					height: $avatar-size;
-					vertical-align: middle;
-				}
-
-				> .indicator {
+			&:hover, &.active {
+				&:before {
+					content: "";
+					display: block;
+					width: calc(100% - 24px);
+					height: 100%;
+					margin: auto;
 					position: absolute;
 					top: 0;
-					left: 20px;
-					color: var(--navIndicator);
-					font-size: 8px;
-					animation: blink 1s infinite;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-radius: 999px;
+					background: var(--accentedBg);
 				}
+			}
 
-				> .text {
-					position: relative;
-					font-size: 0.9em;
-				}
+			&:first-child, &:last-child {
+				position: sticky;
+				z-index: 1;
+				padding-top: 8px;
+				padding-bottom: 8px;
+				background: var(--X14);
+				-webkit-backdrop-filter: var(--blur, blur(8px));
+				backdrop-filter: var(--blur, blur(8px));
+			}
 
-				&:hover {
-					text-decoration: none;
-					color: var(--navHoverFg);
-				}
-
-				&.active {
-					color: var(--navActive);
-				}
+			&:first-child {
+				top: 0;
 
 				&:hover, &.active {
 					&:before {
-						content: "";
-						display: block;
-						width: calc(100% - 24px);
-						height: 100%;
-						margin: auto;
-						position: absolute;
-						top: 0;
-						left: 0;
-						right: 0;
-						bottom: 0;
-						border-radius: 999px;
-						background: var(--accentedBg);
+						content: none;
 					}
 				}
+			}
 
-				&:first-child, &:last-child {
-					position: sticky;
-					z-index: 1;
-					padding-top: 8px;
-					padding-bottom: 8px;
-					background: var(--X14);
-					-webkit-backdrop-filter: var(--blur, blur(8px));
-					backdrop-filter: var(--blur, blur(8px));
+			&:last-child {
+				bottom: 0;
+				color: var(--fgOnAccent);
+
+				&:before {
+					content: "";
+					display: block;
+					width: calc(100% - 20px);
+					height: calc(100% - 20px);
+					margin: auto;
+					position: absolute;
+					top: 0;
+					left: 0;
+					right: 0;
+					bottom: 0;
+					border-radius: 999px;
+					background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+				}
+				
+				&:hover, &.active {
+					&:before {
+						background: var(--accentLighten);
+					}
+				}
+			}
+		}
+	}
+
+	&.iconOnly {
+		flex: 0 0 $nav-icon-only-width;
+		width: $nav-icon-only-width;
+
+		> div {
+			width: $nav-icon-only-width;
+
+			> .divider {
+				margin: 8px auto;
+				width: calc(100% - 32px);
+			}
+
+			> .item {
+				padding-left: 0;
+				padding: 18px 0;
+				width: 100%;
+				text-align: center;
+				font-size: $ui-font-size * 1.1;
+				line-height: initial;
+
+				> i,
+				> .avatar {
+					display: block;
+					margin: 0 auto;
+				}
+
+				> i {
+					opacity: 0.7;
+				}
+
+				> .text {
+					display: none;
+				}
+
+				&:hover, &.active {
+					> i, > .text {
+						opacity: 1;
+					}
 				}
 
 				&:first-child {
-					top: 0;
-
-					&:hover, &.active {
-						&:before {
-							content: none;
-						}
-					}
+					margin-bottom: 8px;
 				}
 
 				&:last-child {
-					bottom: 0;
-					color: var(--fgOnAccent);
+					margin-top: 8px;
+				}
 
-					&:before {
-						content: "";
-						display: block;
-						width: calc(100% - 20px);
-						height: calc(100% - 20px);
-						margin: auto;
-						position: absolute;
-						top: 0;
-						left: 0;
-						right: 0;
-						bottom: 0;
-						border-radius: 999px;
-						background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-					}
-					
-					&:hover, &.active {
-						&:before {
-							background: var(--accentLighten);
-						}
-					}
+				&:before {
+					width: 100%;
+					border-radius: 0;
+				}
+
+				&.post {
+					height: $nav-icon-only-width;
+				}
+
+				&.post:before {
+					width: calc(100% - 32px);
+					height: calc(100% - 32px);
+					border-radius: 100%;
 				}
 			}
 		}
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 684a075c04..91dbe2462d 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -1,16 +1,14 @@
 <template>
-<div class="mk-app" :class="{ wallpaper, isMobile }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
+<div class="gbhvwtnk" :class="{ wallpaper }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
 	<XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/>
 
 	<div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }">
-		<template v-if="!isMobile">
-			<div v-if="!showMenuOnTop" class="sidebar">
-				<XSidebar/>
-			</div>
-			<div v-else ref="widgetsLeft" class="widgets left">
-				<XWidgets :place="'left'" @mounted="attachSticky('widgetsLeft')"/>
-			</div>
-		</template>
+		<div v-if="!showMenuOnTop" class="sidebar">
+			<XSidebar/>
+		</div>
+		<div v-else ref="widgetsLeft" class="widgets left">
+			<XWidgets :place="'left'" @mounted="attachSticky('widgetsLeft')"/>
+		</div>
 
 		<main class="main" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
 			<div class="content">
@@ -32,16 +30,6 @@
 		</div>
 	</div>
 
-	<div v-if="isMobile" class="buttons">
-		<button ref="navButton" class="button nav _button" @click="showDrawerNav"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
-		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
-		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
-		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
-		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
-	</div>
-
-	<XDrawerSidebar v-if="isMobile" ref="drawerNav" class="sidebar"/>
-
 	<transition name="tray-back">
 		<div v-if="widgetsShowing"
 			class="tray-back _modalBg"
@@ -65,20 +53,17 @@ import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
 import { instanceName } from '@/config';
 import { StickySidebar } from '@/scripts/sticky-sidebar';
 import XSidebar from './classic.sidebar.vue';
-import XDrawerSidebar from '@/ui/_common_/sidebar.vue';
 import XCommon from './_common_/common.vue';
 import * as os from '@/os';
 import { menuDef } from '@/menu';
 import * as symbols from '@/symbols';
 
 const DESKTOP_THRESHOLD = 1100;
-const MOBILE_THRESHOLD = 600;
 
 export default defineComponent({
 	components: {
 		XCommon,
 		XSidebar,
-		XDrawerSidebar,
 		XHeaderMenu: defineAsyncComponent(() => import('./classic.header.vue')),
 		XWidgets: defineAsyncComponent(() => import('./classic.widgets.vue')),
 	},
@@ -95,7 +80,6 @@ export default defineComponent({
 			pageInfo: null,
 			menuDef: menuDef,
 			globalHeaderHeight: 0,
-			isMobile: window.innerWidth <= MOBILE_THRESHOLD,
 			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
 			widgetsShowing: false,
 			fullView: false,
@@ -104,16 +88,8 @@ export default defineComponent({
 	},
 
 	computed: {
-		navIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
-				if (this.menuDef[def].indicated) return true;
-			}
-			return false;
-		},
-
 		showMenuOnTop(): boolean {
-			return !this.isMobile && this.$store.state.menuDisplay === 'top';
+			return this.$store.state.menuDisplay === 'top';
 		}
 	},
 
@@ -136,7 +112,6 @@ export default defineComponent({
 
 	mounted() {
 		window.addEventListener('resize', () => {
-			this.isMobile = (window.innerWidth <= MOBILE_THRESHOLD);
 			this.isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD);
 		}, { passive: true });
 
@@ -179,22 +154,10 @@ export default defineComponent({
 			}, { passive: true });
 		},
 
-		post() {
-			os.post();
-		},
-
 		top() {
 			window.scroll({ top: 0, behavior: 'smooth' });
 		},
 
-		back() {
-			history.back();
-		},
-
-		showDrawerNav() {
-			this.$refs.drawerNav.show();
-		},
-
 		onTransition() {
 			if (window._scroll) window._scroll();
 		},
@@ -258,10 +221,9 @@ export default defineComponent({
 	opacity: 0;
 }
 
-.mk-app {
+.gbhvwtnk {
 	$ui-font-size: 1em;
 	$widgets-hide-threshold: 1200px;
-	$nav-icon-only-width: 78px; // TODO: どこかに集約したい
 
 	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 	min-height: calc(var(--vh, 1vh) * 100);
@@ -272,21 +234,6 @@ export default defineComponent({
 		//backdrop-filter: var(--blur, blur(4px));
 	}
 
-	&.isMobile {
-		> .columns {
-			display: block;
-			margin: 0;
-
-			> .main {
-				margin: 0;
-				padding-bottom: 92px;
-				border: none;
-				width: 100%;
-				border-radius: 0;
-			}
-		}
-	}
-
 	> .columns {
 		display: flex;
 		justify-content: center;
@@ -372,76 +319,6 @@ export default defineComponent({
 		}
 	}
 
-	> .buttons {
-		position: fixed;
-		z-index: 1000;
-		bottom: 0;
-		padding: 16px;
-		display: flex;
-		width: 100%;
-		box-sizing: border-box;
-		-webkit-backdrop-filter: var(--blur, blur(32px));
-		backdrop-filter: var(--blur, blur(32px));
-		background-color: var(--header);
-		border-top: solid 0.5px var(--divider);
-
-		> .button {
-			position: relative;
-			flex: 1;
-			padding: 0;
-			margin: auto;
-			height: 64px;
-			border-radius: 8px;
-			background: var(--panel);
-			color: var(--fg);
-
-			&:not(:last-child) {
-				margin-right: 12px;
-			}
-
-			@media (max-width: 400px) {
-				height: 60px;
-
-				&:not(:last-child) {
-					margin-right: 8px;
-				}
-			}
-
-			&:hover {
-				background: var(--X2);
-			}
-
-			> .indicator {
-				position: absolute;
-				top: 0;
-				left: 0;
-				color: var(--indicator);
-				font-size: 16px;
-				animation: blink 1s infinite;
-			}
-
-			&:first-child {
-				margin-left: 0;
-			}
-
-			&:last-child {
-				margin-right: 0;
-			}
-
-			> * {
-				font-size: 22px;
-			}
-
-			&:disabled {
-				cursor: default;
-
-				> * {
-					opacity: 0.5;
-				}
-			}
-		}
-	}
-
 	> .tray-back {
 		z-index: 1001;
 	}
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 4f1efb0a4c..fb8f953625 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -1,8 +1,8 @@
 <template>
-<div class="mk-deck" :class="`${deckStore.reactiveState.columnAlign.value}`" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
+<div class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
 	@contextmenu.self.prevent="onContextmenu"
 >
-	<XSidebar ref="nav"/>
+	<XSidebar v-if="!isMobile"/>
 
 	<template v-for="ids in layout">
 		<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
@@ -22,94 +22,76 @@
 		/>
 	</template>
 
-	<button v-if="$i" class="nav _button" @click="showNav()"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
-	<button v-if="$i" class="post _buttonPrimary" @click="post()"><i class="fas fa-pencil-alt"></i></button>
+	<div v-if="isMobile" class="buttons">
+		<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
+		<button class="button home _button" @click="$router.push('/')"><i class="fas fa-home"></i></button>
+		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
+		<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
+	</div>
+
+	<transition name="menu-back">
+		<div v-if="drawerMenuShowing"
+			class="menu-back _modalBg"
+			@click="drawerMenuShowing = false"
+			@touchstart.passive="drawerMenuShowing = false"
+		></div>
+	</transition>
+
+	<transition name="menu">
+		<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
+	</transition>
 
 	<XCommon/>
 </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent, provide, ref, watch } from 'vue';
 import { v4 as uuid } from 'uuid';
-import { host } from '@/config';
 import DeckColumnCore from '@/ui/deck/column-core.vue';
 import XSidebar from '@/ui/_common_/sidebar.vue';
+import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
 import { getScrollContainer } from '@/scripts/scroll';
 import * as os from '@/os';
 import { menuDef } from '@/menu';
 import XCommon from './_common_/common.vue';
-import { deckStore, addColumn, loadDeck } from './deck/deck-store';
+import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
+import { useRoute } from 'vue-router';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
 
 export default defineComponent({
 	components: {
 		XCommon,
 		XSidebar,
+		XDrawerMenu,
 		DeckColumnCore,
 	},
 
-	provide() {
-		return {
-			shouldSpacerMin: true,
-			...deckStore.state.navWindow ? {
-				navHook: (url) => {
-					os.pageWindow(url);
-				}
-			} : {}
-		};
-	},
+	setup() {
+		const isMobile = ref(window.innerWidth <= 500);
+		window.addEventListener('resize', () => {
+			isMobile.value = window.innerWidth <= 500;
+		});
 
-	data() {
-		return {
-			deckStore,
-			host: host,
-			menuDef: menuDef,
-			wallpaper: localStorage.getItem('wallpaper') != null,
-		};
-	},
+		const drawerMenuShowing = ref(false);
 
-	computed: {
-		columns() {
-			return deckStore.reactiveState.columns.value;
-		},
-		layout() {
-			return deckStore.reactiveState.layout.value;
-		},
-		navIndicated(): boolean {
-			if (!this.$i) return false;
-			for (const def in this.menuDef) {
-				if (this.menuDef[def].indicated) return true;
+		const route = useRoute();
+		watch(route, () => {
+			drawerMenuShowing.value = false;
+		});
+
+		const columns = deckStore.reactiveState.columns;
+		const layout = deckStore.reactiveState.layout.value;
+		const menuIndicated = computed(() => {
+			if ($i == null) return false;
+			for (const def in menuDef) {
+				if (menuDef[def].indicated) return true;
 			}
 			return false;
-		},
-	},
+		});
 
-	created() {
-		document.documentElement.style.overflowY = 'hidden';
-		document.documentElement.style.scrollBehavior = 'auto';
-		window.addEventListener('wheel', this.onWheel);
-		loadDeck();
-	},
-
-	mounted() {
-	},
-
-	methods: {
-		onWheel(e) {
-			if (getScrollContainer(e.target) == null) {
-				document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
-			}
-		},
-
-		showNav() {
-			this.$refs.nav.show();
-		},
-
-		post() {
-			os.post();
-		},
-
-		async addColumn(ev) {
+		const addColumn = async (ev) => {
 			const columns = [
 				'main',
 				'widgets',
@@ -122,33 +104,83 @@ export default defineComponent({
 			];
 
 			const { canceled, result: column } = await os.select({
-				title: this.$ts._deck.addColumn,
+				title: i18n.locale._deck.addColumn,
 				items: columns.map(column => ({
-					value: column, text: this.$t('_deck._columns.' + column)
+					value: column, text: i18n.t('_deck._columns.' + column)
 				}))
 			});
 			if (canceled) return;
 
-			addColumn({
+			addColumnToStore({
 				type: column,
 				id: uuid(),
-				name: this.$t('_deck._columns.' + column),
+				name: i18n.t('_deck._columns.' + column),
 				width: 330,
 			});
-		},
+		};
 
-		onContextmenu(e) {
+		const onContextmenu = (ev) => {
 			os.contextMenu([{
-				text: this.$ts._deck.addColumn,
+				text: i18n.locale._deck.addColumn,
 				icon: null,
-				action: this.addColumn
-			}], e);
-		},
-	}
+				action: addColumn
+			}], ev);
+		};
+
+		provide('shouldSpacerMin', true);
+		if (deckStore.state.navWindow) {
+			provide('navHook', (url) => {
+				os.pageWindow(url);
+			});
+		}
+
+		document.documentElement.style.overflowY = 'hidden';
+		document.documentElement.style.scrollBehavior = 'auto';
+		window.addEventListener('wheel', (ev) => {
+			if (getScrollContainer(ev.target) == null) {
+				document.documentElement.scrollLeft += ev.deltaY > 0 ? 96 : -96;
+			}
+		});
+		loadDeck();
+
+		return {
+			isMobile,
+			deckStore,
+			drawerMenuShowing,
+			columns,
+			layout,
+			menuIndicated,
+			onContextmenu,
+			wallpaper: localStorage.getItem('wallpaper') != null,
+			post: os.post,
+		};
+	},
 });
 </script>
 
 <style lang="scss" scoped>
+.menu-enter-active,
+.menu-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.menu-enter-from,
+.menu-leave-active {
+	opacity: 0;
+	transform: translateX(-240px);
+}
+
+.menu-back-enter-active,
+.menu-back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.menu-back-enter-from,
+.menu-back-leave-active {
+	opacity: 0;
+}
+
 .mk-deck {
 	$nav-hide-threshold: 650px; // TODO: どこかに集約したい
 
@@ -172,6 +204,10 @@ export default defineComponent({
 		}
 	}
 
+	&.isMobile {
+		padding-bottom: 100px;
+	}
+
 	> .column {
 		flex-shrink: 0;
 		margin-right: var(--deckMargin);
@@ -186,43 +222,88 @@ export default defineComponent({
 		}
 	}
 
-	> .post,
-	> .nav {
+	> .buttons {
 		position: fixed;
 		z-index: 1000;
-		bottom: 32px;
-		width: 64px;
-		height: 64px;
-		border-radius: 100%;
-		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
-		font-size: 22px;
+		bottom: 0;
+		left: 0;
+		padding: 16px;
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
 
-		@media (min-width: ($nav-hide-threshold + 1px)) {
-			display: none;
+		> .button {
+			position: relative;
+			flex: 1;
+			padding: 0;
+			margin: auto;
+			height: 64px;
+			border-radius: 8px;
+			background: var(--panel);
+			color: var(--fg);
+
+			&:not(:last-child) {
+				margin-right: 12px;
+			}
+
+			@media (max-width: 400px) {
+				height: 60px;
+
+				&:not(:last-child) {
+					margin-right: 8px;
+				}
+			}
+
+			&:hover {
+				background: var(--X2);
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 0;
+				left: 0;
+				color: var(--indicator);
+				font-size: 16px;
+				animation: blink 1s infinite;
+			}
+
+			&:first-child {
+				margin-left: 0;
+			}
+
+			&:last-child {
+				margin-right: 0;
+			}
+
+			> * {
+				font-size: 22px;
+			}
+
+			&:disabled {
+				cursor: default;
+
+				> * {
+					opacity: 0.5;
+				}
+			}
 		}
 	}
 
-	> .post {
-		right: 32px;
+	> .menu-back {
+		z-index: 1001;
 	}
 
-	> .nav {
-		left: 32px;
-		background: var(--panel);
-		color: var(--fg);
-
-		&:hover {
-			background: var(--X2);
-		}
-
-		> .indicator {
-			position: absolute;
-			top: 0;
-			left: 0;
-			color: var(--indicator);
-			font-size: 16px;
-			animation: blink 1s infinite;
-		}
+	> .menu {
+		position: fixed;
+		top: 0;
+		left: 0;
+		z-index: 1001;
+		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+		height: calc(var(--vh, 1vh) * 100);
+		width: 240px;
+		box-sizing: border-box;
+		overflow: auto;
+		background: var(--bg);
 	}
 }
 </style>
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 011370f7f1..352163d050 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -1,9 +1,9 @@
 <template>
-<div class="mk-app" :class="{ wallpaper }">
-	<XSidebar ref="nav" class="sidebar"/>
+<div class="dkgtipfy" :class="{ wallpaper }">
+	<XSidebar v-if="!isMobile" class="sidebar"/>
 
-	<div ref="contents" class="contents" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
-		<main ref="main">
+	<div class="contents" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
+		<main>
 			<div class="content">
 				<MkStickyContainer>
 					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
@@ -20,32 +20,44 @@
 		</main>
 	</div>
 
-	<XSide v-if="isDesktop" ref="side" class="side"/>
+	<XSideView v-if="isDesktop" ref="side" class="side"/>
 
-	<div v-if="isDesktop" ref="widgets" class="widgets">
+	<div v-if="isDesktop" ref="widgetsEl" class="widgets">
 		<XWidgets @mounted="attachSticky"/>
 	</div>
 
-	<div class="buttons" :class="{ navHidden }">
-		<button ref="navButton" class="button nav _button" @click="showNav"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
+	<button class="widgetButton _button" :class="{ show: true }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
+
+	<div v-if="isMobile" class="buttons">
+		<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
 		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
 		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
 		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
-		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
+		<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
 	</div>
 
-	<button class="widgetButton _button" :class="{ navHidden }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
+	<transition name="menuDrawer-back">
+		<div v-if="drawerMenuShowing"
+			class="menuDrawer-back _modalBg"
+			@click="drawerMenuShowing = false"
+			@touchstart.passive="drawerMenuShowing = false"
+		></div>
+	</transition>
 
-	<transition name="tray-back">
+	<transition name="menuDrawer">
+		<XDrawerMenu v-if="drawerMenuShowing" class="menuDrawer"/>
+	</transition>
+
+	<transition name="widgetsDrawer-back">
 		<div v-if="widgetsShowing"
-			class="tray-back _modalBg"
+			class="widgetsDrawer-back _modalBg"
 			@click="widgetsShowing = false"
 			@touchstart.passive="widgetsShowing = false"
 		></div>
 	</transition>
 
-	<transition name="tray">
-		<XWidgets v-if="widgetsShowing" class="tray"/>
+	<transition name="widgetsDrawer">
+		<XWidgets v-if="widgetsShowing" class="widgetsDrawer"/>
 	</transition>
 
 	<XCommon/>
@@ -53,60 +65,69 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+import { defineComponent, defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue';
 import { instanceName } from '@/config';
 import { StickySidebar } from '@/scripts/sticky-sidebar';
 import XSidebar from '@/ui/_common_/sidebar.vue';
+import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
 import XCommon from './_common_/common.vue';
-import XSide from './classic.side.vue';
+import XSideView from './classic.side.vue';
 import * as os from '@/os';
-import { menuDef } from '@/menu';
 import * as symbols from '@/symbols';
+import { defaultStore } from '@/store';
+import * as EventEmitter from 'eventemitter3';
+import { menuDef } from '@/menu';
+import { useRoute } from 'vue-router';
+import { i18n } from '@/i18n';
 
 const DESKTOP_THRESHOLD = 1100;
+const MOBILE_THRESHOLD = 500;
 
 export default defineComponent({
 	components: {
 		XCommon,
 		XSidebar,
+		XDrawerMenu,
 		XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
-		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
+		XSideView, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
 	},
 
-	provide() {
-		return {
-			sideViewHook: this.isDesktop ? (url) => {
-				this.$refs.side.navigate(url);
-			} : null
-		};
-	},
+	setup() {
+		const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
+		const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD);
+		window.addEventListener('resize', () => {
+			isMobile.value = window.innerWidth <= MOBILE_THRESHOLD;
+		});
 
-	data() {
-		return {
-			pageInfo: null,
-			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
-			menuDef: menuDef,
-			navHidden: false,
-			widgetsShowing: false,
-			wallpaper: localStorage.getItem('wallpaper') != null,
-		};
-	},
+		const pageInfo = ref();
+		const widgetsEl = ref<HTMLElement>();
+		const widgetsShowing = ref(false);
 
-	computed: {
-		navIndicated(): boolean {
-			for (const def in this.menuDef) {
+		const sideViewController = new EventEmitter();
+
+		provide('sideViewHook', isDesktop.value ? (url) => {
+			sideViewController.emit('navigate', url);
+		} : null);
+
+		const menuIndicated = computed(() => {
+			for (const def in menuDef) {
 				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
-				if (this.menuDef[def].indicated) return true;
+				if (menuDef[def].indicated) return true;
 			}
 			return false;
-		}
-	},
+		});
+
+		const drawerMenuShowing = ref(false);
+
+		const route = useRoute();
+		watch(route, () => {
+			drawerMenuShowing.value = false;
+		});
 
-	created() {
 		document.documentElement.style.overflowY = 'scroll';
 
-		if (this.$store.state.widgets.length === 0) {
-			this.$store.set('widgets', [{
+		if (defaultStore.state.widgets.length === 0) {
+			defaultStore.set('widgets', [{
 				name: 'calendar',
 				id: 'a', place: 'right', data: {}
 			}, {
@@ -117,123 +138,129 @@ export default defineComponent({
 				id: 'c', place: 'right', data: {}
 			}]);
 		}
-	},
 
-	mounted() {
-		this.adjustUI();
-
-		const ro = new ResizeObserver((entries, observer) => {
-			this.adjustUI();
+		onMounted(() => {
+			if (!isDesktop.value) {
+				window.addEventListener('resize', () => {
+					if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
+				}, { passive: true });
+			}
 		});
 
-		ro.observe(this.$refs.contents);
-
-		window.addEventListener('resize', this.adjustUI, { passive: true });
-
-		if (!this.isDesktop) {
-			window.addEventListener('resize', () => {
-				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
-			}, { passive: true });
-		}
-	},
-
-	methods: {
-		changePage(page) {
+		const changePage = (page) => {
 			if (page == null) return;
 			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-				document.title = `${this.pageInfo.title} | ${instanceName}`;
+				pageInfo.value = page[symbols.PAGE_INFO];
+				document.title = `${pageInfo.value.title} | ${instanceName}`;
 			}
-		},
+		};
 
-		adjustUI() {
-			const navWidth = this.$refs.nav.$el.offsetWidth;
-			this.navHidden = navWidth === 0;
-		},
-
-		showNav() {
-			this.$refs.nav.show();
-		},
-
-		attachSticky(el) {
-			const sticky = new StickySidebar(this.$refs.widgets);
-			window.addEventListener('scroll', () => {
-				sticky.calc(window.scrollY);
-			}, { passive: true });
-		},
-
-		post() {
-			os.post();
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		back() {
-			history.back();
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-
-		onContextmenu(e) {
+		const onContextmenu = (ev) => {
 			const isLink = (el: HTMLElement) => {
 				if (el.tagName === 'A') return true;
 				if (el.parentElement) {
 					return isLink(el.parentElement);
 				}
 			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+			if (isLink(ev.target)) return;
+			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
 			if (window.getSelection().toString() !== '') return;
-			const path = this.$route.path;
+			const path = route.path;
 			os.contextMenu([{
 				type: 'label',
 				text: path,
 			}, {
 				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
+				text: i18n.locale.openInSideView,
 				action: () => {
 					this.$refs.side.navigate(path);
 				}
 			}, {
 				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
+				text: i18n.locale.openInWindow,
 				action: () => {
 					os.pageWindow(path);
 				}
-			}], e);
-		},
-	}
+			}], ev);
+		};
+
+		const attachSticky = (el) => {
+			const sticky = new StickySidebar(widgetsEl.value);
+			window.addEventListener('scroll', () => {
+				sticky.calc(window.scrollY);
+			}, { passive: true });
+		};
+
+		return {
+			pageInfo,
+			isDesktop,
+			isMobile,
+			widgetsEl,
+			widgetsShowing,
+			drawerMenuShowing,
+			menuIndicated,
+			wallpaper: localStorage.getItem('wallpaper') != null,
+			changePage,
+			top: () => {
+				window.scroll({ top: 0, behavior: 'smooth' });
+			},
+			onTransition: () => {
+				if (window._scroll) window._scroll();
+			},
+			post: os.post,
+			onContextmenu,
+			attachSticky,
+		};
+	},
 });
 </script>
 
 <style lang="scss" scoped>
-.tray-enter-active,
-.tray-leave-active {
+.widgetsDrawer-enter-active,
+.widgetsDrawer-leave-active {
 	opacity: 1;
 	transform: translateX(0);
 	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 }
-.tray-enter-from,
-.tray-leave-active {
+.widgetsDrawer-enter-from,
+.widgetsDrawer-leave-active {
 	opacity: 0;
 	transform: translateX(240px);
 }
 
-.tray-back-enter-active,
-.tray-back-leave-active {
+.widgetsDrawer-back-enter-active,
+.widgetsDrawer-back-leave-active {
 	opacity: 1;
 	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
 }
-.tray-back-enter-from,
-.tray-back-leave-active {
+.widgetsDrawer-back-enter-from,
+.widgetsDrawer-back-leave-active {
 	opacity: 0;
 }
 
-.mk-app {
+.menuDrawer-enter-active,
+.menuDrawer-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.menuDrawer-enter-from,
+.menuDrawer-leave-active {
+	opacity: 0;
+	transform: translateX(-240px);
+}
+
+.menuDrawer-back-enter-active,
+.menuDrawer-back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.menuDrawer-back-enter-from,
+.menuDrawer-back-leave-active {
+	opacity: 0;
+}
+
+.dkgtipfy {
 	$ui-font-size: 1em; // TODO: どこかに集約したい
 	$widgets-hide-threshold: 1090px;
 
@@ -285,6 +312,7 @@ export default defineComponent({
 		}
 	}
 
+/*
 	> .widgetButton {
 		display: block;
 		position: fixed;
@@ -305,12 +333,34 @@ export default defineComponent({
 		@media (min-width: ($widgets-hide-threshold + 1px)) {
 			display: none;
 		}
+	}*/
+
+	> .widgetButton {
+		display: none;
+	}
+
+	> .widgetsDrawer-back {
+		z-index: 1001;
+	}
+
+	> .widgetsDrawer {
+		position: fixed;
+		top: 0;
+		right: 0;
+		z-index: 1001;
+		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+		height: calc(var(--vh, 1vh) * 100);
+		padding: var(--margin);
+		box-sizing: border-box;
+		overflow: auto;
+		background: var(--bg);
 	}
 
 	> .buttons {
 		position: fixed;
 		z-index: 1000;
 		bottom: 0;
+		left: 0;
 		padding: 16px;
 		display: flex;
 		width: 100%;
@@ -319,10 +369,6 @@ export default defineComponent({
 		backdrop-filter: var(--blur, blur(32px));
 		background-color: var(--header);
 
-		&:not(.navHidden) {
-			display: none;
-		}
-
 		> .button {
 			position: relative;
 			flex: 1;
@@ -380,22 +426,23 @@ export default defineComponent({
 		}
 	}
 
-	> .tray-back {
+	> .menuDrawer-back {
 		z-index: 1001;
 	}
 
-	> .tray {
+	> .menuDrawer {
 		position: fixed;
 		top: 0;
-		right: 0;
+		left: 0;
 		z-index: 1001;
 		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 		height: calc(var(--vh, 1vh) * 100);
-		padding: var(--margin);
+		width: 240px;
 		box-sizing: border-box;
 		overflow: auto;
 		background: var(--bg);
 	}
+
 }
 </style>