diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000000..d30cf2aa56
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,17 @@
+{
+	"parserOptions": {
+		"parser": "typescript-eslint-parser"
+	},
+	"extends": [
+		"eslint:recommended",
+		"plugin:vue/recommended"
+	],
+	"rules": {
+		"vue/require-v-for-key": false,
+		"vue/max-attributes-per-line": false,
+		"vue/html-indent": false,
+		"vue/html-self-closing": false,
+		"vue/no-unused-vars": false,
+		"no-console": 0
+	}
+}
diff --git a/package.json b/package.json
index e87be0ab21..727c4af716 100644
--- a/package.json
+++ b/package.json
@@ -99,6 +99,8 @@
 		"diskusage": "0.2.4",
 		"elasticsearch": "14.1.0",
 		"escape-regexp": "0.0.1",
+		"eslint": "^4.18.0",
+		"eslint-plugin-vue": "^4.2.2",
 		"eventemitter3": "3.0.0",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
@@ -174,6 +176,7 @@
 		"ts-node": "4.1.0",
 		"tslint": "5.9.1",
 		"typescript": "2.7.1",
+		"typescript-eslint-parser": "^13.0.0",
 		"uglify-es": "3.3.9",
 		"uglifyjs-webpack-plugin": "1.1.8",
 		"uuid": "3.2.1",
diff --git a/src/web/app/desktop/-tags/home-widgets/timemachine.tag b/src/web/app/desktop/-tags/home-widgets/timemachine.tag
deleted file mode 100644
index 43f59fe674..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/timemachine.tag
+++ /dev/null
@@ -1,23 +0,0 @@
-<mk-timemachine-home-widget>
-	<mk-calendar-widget design={ data.design } warp={ warp }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.warp = date => {
-			this.opts.tl.warp(date);
-		};
-
-		this.func = () => {
-			if (++this.data.design == 6) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-timemachine-home-widget>
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
index 3380774028..e548a82c57 100644
--- a/src/web/app/desktop/views/components/calendar.vue
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -7,16 +7,15 @@
 	</template>
 
 	<div class="calendar">
+		<template v-if="design == 0 || design == 2 || design == 4">
 		<div class="weekday"
-			v-if="design == 0 || design == 2 || design == 4"
 			v-for="(day, i) in Array(7).fill(0)"
-			:key="i"
 			:data-today="year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i"
 			:data-is-donichi="i == 0 || i == 6"
 		>{{ weekdayText[i] }}</div>
-		<div each={ day, i in Array(paddingDays).fill(0) }></div>
-		<div class="day" v-for="(day, i) in Array(days).fill(0)"
-			:key="i"
+		</template>
+		<div v-for="n in paddingDays"></div>
+		<div class="day" v-for="(day, i) in days"
 			:data-today="isToday(i + 1)"
 			:data-selected="isSelected(i + 1)"
 			:data-is-out-of-range="isOutOfRange(i + 1)"
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 3a04e13cb2..e815239d3f 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -37,43 +37,21 @@
 		</div>
 	</div>
 	<div class="main">
-		<div class="left">
-			<div ref="left" data-place="left">
-				<template v-for="widget in leftWidgets">
+		<div v-for="place in ['left', 'main', 'right']" :class="place" :ref="place" :data-place="place">
+			<template v-if="place != 'main'">
+				<template v-for="widget in widgets[place]">
 					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
+						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
+						<component :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :ref="widget.id" @chosen="warp"/>
 					</template>
 				</template>
-			</div>
-		</div>
-		<main ref="main">
-			<div class="maintop" ref="maintop" data-place="main" v-if="customize">
-				<template v-for="widget in centerWidgets">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
-					</div>
-					<template v-else>
-						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
-					</template>
-				</template>
-			</div>
-			<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
-			<mk-mentions ref="tl" @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
-		</main>
-		<div class="right">
-			<div ref="right" data-place="right">
-				<template v-for="widget in rightWidgets">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
-					</div>
-					<template v-else>
-						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
-					</template>
-				</template>
-			</div>
+			</template>
+			<template v-else>
+				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="place == 'main' && mode == 'timeline'"/>
+				<mk-mentions @loaded="onTlLoaded" v-if="place == 'main' && mode == 'mentions'"/>
+			</template>
 		</div>
 	</div>
 </div>
@@ -99,6 +77,85 @@ export default Vue.extend({
 			widgetAdderSelected: null
 		};
 	},
+	computed: {
+		leftWidgets(): any {
+			return (this as any).os.i.client_settings.home.filter(w => w.place == 'left');
+		},
+		rightWidgets(): any {
+			return (this as any).os.i.client_settings.home.filter(w => w.place == 'right');
+		},
+		widgets(): any {
+			return {
+				left: this.leftWidgets,
+				right: this.rightWidgets,
+			};
+		},
+		leftEl(): Element {
+			return (this.$refs.left as Element[])[0];
+		},
+		rightEl(): Element {
+			return (this.$refs.right as Element[])[0];
+		}
+	},
+	created() {
+		this.bakedHomeData = this.bakeHomeData();
+	},
+	mounted() {
+		(this as any).os.i.on('refreshed', this.onMeRefreshed);
+
+		this.home = (this as any).os.i.client_settings.home;
+
+		this.$nextTick(() => {
+			if (!this.customize) {
+				if (this.leftEl.children.length == 0) {
+					this.leftEl.parentNode.removeChild(this.leftEl);
+				}
+				if (this.rightEl.children.length == 0) {
+					this.rightEl.parentNode.removeChild(this.rightEl);
+				}
+			}
+
+			if (this.customize) {
+				(this as any).apis.dialog({
+					title: '%fa:info-circle%カスタマイズのヒント',
+					text: '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
+						'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
+						'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
+						'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
+					actions: [{
+						text: 'Got it!'
+					}]
+				});
+
+				const sortableOption = {
+					group: 'kyoppie',
+					animation: 150,
+					onMove: evt => {
+						const id = evt.dragged.getAttribute('data-widget-id');
+						this.home.find(tag => tag.id == id).widget.place = evt.to.getAttribute('data-place');
+					},
+					onSort: () => {
+						this.saveHome();
+					}
+				};
+
+				new Sortable(this.leftEl, sortableOption);
+				new Sortable(this.rightEl, sortableOption);
+				new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
+					onAdd: evt => {
+						const el = evt.item;
+						const id = el.getAttribute('data-widget-id');
+						el.parentNode.removeChild(el);
+						(this as any).os.i.client_settings.home = (this as any).os.i.client_settings.home.filter(w => w.id != id);
+						this.saveHome();
+					}
+				}));
+			}
+		});
+	},
+	beforeDestroy() {
+		(this as any).os.i.off('refreshed', this.onMeRefreshed);
+	},
 	methods: {
 		bakeHomeData() {
 			return JSON.stringify((this as any).os.i.client_settings.home);
@@ -130,102 +187,27 @@ export default Vue.extend({
 		saveHome() {
 			const data = [];
 
-			Array.from((this.$refs.left as Element).children).forEach(el => {
+			Array.from(this.leftEl.children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
 				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'left';
 				data.push(widget);
 			});
 
-			Array.from((this.$refs.right as Element).children).forEach(el => {
+			Array.from(this.rightEl.children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
 				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'right';
 				data.push(widget);
 			});
 
-			Array.from((this.$refs.maintop as Element).children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
-				widget.place = 'main';
-				data.push(widget);
-			});
-
 			(this as any).api('i/update_home', {
 				home: data
 			});
-		}
-	},
-	computed: {
-		leftWidgets(): any {
-			return (this as any).os.i.client_settings.home.filter(w => w.place == 'left');
 		},
-		centerWidgets(): any {
-			return (this as any).os.i.client_settings.home.filter(w => w.place == 'center');
-		},
-		rightWidgets(): any {
-			return (this as any).os.i.client_settings.home.filter(w => w.place == 'right');
+		warp(date) {
+			(this.$refs.tl as any)[0].warp(date);
 		}
-	},
-	created() {
-		this.bakedHomeData = this.bakeHomeData();
-	},
-	mounted() {
-		(this as any).os.i.on('refreshed', this.onMeRefreshed);
-
-		this.home = (this as any).os.i.client_settings.home;
-
-		if (!this.customize) {
-			if ((this.$refs.left as Element).children.length == 0) {
-				(this.$refs.left as Element).parentNode.removeChild((this.$refs.left as Element));
-			}
-			if ((this.$refs.right as Element).children.length == 0) {
-				(this.$refs.right as Element).parentNode.removeChild((this.$refs.right as Element));
-			}
-		}
-
-		if (this.customize) {
-			/*dialog('%fa:info-circle%カスタマイズのヒント',
-				'<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
-				'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
-				'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
-				'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
-			[{
-				text: 'Got it!'
-			}]);*/
-
-			const sortableOption = {
-				group: 'kyoppie',
-				animation: 150,
-				onMove: evt => {
-					const id = evt.dragged.getAttribute('data-widget-id');
-					this.home.find(tag => tag.id == id).update({ place: evt.to.getAttribute('data-place') });
-				},
-				onSort: () => {
-					this.saveHome();
-				}
-			};
-
-			new Sortable(this.$refs.left, sortableOption);
-			new Sortable(this.$refs.right, sortableOption);
-			new Sortable(this.$refs.maintop, sortableOption);
-			new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
-				onAdd: evt => {
-					const el = evt.item;
-					const id = el.getAttribute('data-widget-id');
-					el.parentNode.removeChild(el);
-					(this as any).os.i.client_settings.home = (this as any).os.i.client_settings.home.filter(w => w.id != id);
-					this.saveHome();
-				}
-			}));
-		}
-	},
-	beforeDestroy() {
-		(this as any).os.i.off('refreshed', this.onMeRefreshed);
-
-		this.home.forEach(widget => {
-			widget.unmount();
-		});
 	}
 });
 </script>
@@ -324,26 +306,16 @@ export default Vue.extend({
 				> *
 					pointer-events none
 
-		> main
+		> .main
 			padding 16px
 			width calc(100% - 275px * 2)
 
-			> *:not(.maintop):not(:last-child)
-			> .maintop > *:not(:last-child)
-				margin-bottom 16px
-
-			> .maintop
-				min-height 64px
-				margin-bottom 16px
-
 		> *:not(main)
 			width 275px
+			padding 16px 0 16px 0
 
-			> *
-				padding 16px 0 16px 0
-
-				> *:not(:last-child)
-					margin-bottom 16px
+			> *:not(:last-child)
+				margin-bottom 16px
 
 		> .left
 			padding-left 16px
@@ -355,7 +327,7 @@ export default Vue.extend({
 			> *:not(main)
 				display none
 
-			> main
+			> .main
 				float none
 				width 100%
 				max-width 700px
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 151ebf296b..9a27369547 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -33,6 +33,7 @@ import driveFolder from './drive-folder.vue';
 import driveNavFolder from './drive-nav-folder.vue';
 import postDetail from './post-detail.vue';
 import settings from './settings.vue';
+import calendar from './calendar.vue';
 import wNav from './widgets/nav.vue';
 import wCalendar from './widgets/calendar.vue';
 import wPhotoStream from './widgets/photo-stream.vue';
@@ -41,6 +42,7 @@ import wTips from './widgets/tips.vue';
 import wDonation from './widgets/donation.vue';
 import wNotifications from './widgets/notifications.vue';
 import wBroadcast from './widgets/broadcast.vue';
+import wTimemachine from './widgets/timemachine.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -75,6 +77,7 @@ Vue.component('mk-drive-folder', driveFolder);
 Vue.component('mk-drive-nav-folder', driveNavFolder);
 Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-settings', settings);
+Vue.component('mk-calendar', calendar);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
@@ -83,3 +86,4 @@ Vue.component('mkw-tips', wTips);
 Vue.component('mkw-donation', wDonation);
 Vue.component('mkw-notifications', wNotifications);
 Vue.component('mkw-broadcast', wBroadcast);
+Vue.component('mkw-timemachine', wTimemachine);
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 3d792436e0..66d70a9578 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -13,19 +13,14 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: {
-		date: {
-			type: Date,
-			required: false
-		}
-	},
 	data() {
 		return {
 			fetching: true,
 			moreFetching: false,
 			posts: [],
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			date: null
 		};
 	},
 	computed: {
@@ -60,7 +55,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('posts/timeline', {
-				until_date: this.date ? (this.date as any).getTime() : undefined
+				until_date: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
 				this.fetching = false;
 				this.posts = posts;
@@ -93,6 +88,10 @@ export default Vue.extend({
 					(this.$refs.timeline as any).focus();
 				}
 			}
+		},
+		warp(date) {
+			this.date = date;
+			this.fetch();
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/widgets/timemachine.vue b/src/web/app/desktop/views/components/widgets/timemachine.vue
new file mode 100644
index 0000000000..d484ce6d74
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/timemachine.vue
@@ -0,0 +1,28 @@
+<template>
+<div class="mkw-timemachine">
+	<mk-calendar :design="props.design" @chosen="chosen"/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'timemachine',
+	props: {
+		design: 0
+	}
+}).extend({
+	methods: {
+		chosen(date) {
+			this.$emit('chosen', date);
+		},
+		func() {
+			if (this.props.design == 5) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>