diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 394577f378..680e70f61e 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -97,6 +97,7 @@ cantRenote: "この投稿はRenoteできません。"
 cantReRenote: "RenoteをRenoteすることはできません。"
 quote: "引用"
 pinnedNote: "ピン留めされたノート"
+pinned: "ピン留め"
 you: "あなた"
 clickToShow: "クリックして表示"
 sensitive: "閲覧注意"
diff --git a/src/client/components/ui/tooltip.vue b/src/client/components/ui/tooltip.vue
index 6ea344c54d..b220fe5d8c 100644
--- a/src/client/components/ui/tooltip.vue
+++ b/src/client/components/ui/tooltip.vue
@@ -1,6 +1,6 @@
 <template>
-<transition name="zoom-in-top" appear @after-leave="$emit('closed')">
-	<div class="buebdbiu _acrylic _shadow" v-if="showing">
+<transition name="tooltip" appear @after-leave="$emit('closed')">
+	<div class="buebdbiu _acrylic _shadow" v-show="showing" ref="content">
 		<slot>{{ text }}</slot>
 	</div>
 </transition>
@@ -35,19 +35,43 @@ export default defineComponent({
 
 			const rect = this.source.getBoundingClientRect();
 
-			let x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			let y = rect.top + window.pageYOffset + this.source.offsetHeight;
+			const contentWidth = this.$refs.content.offsetWidth;
+			const contentHeight = this.$refs.content.offsetHeight;
 
-			x -= (this.$el.offsetWidth / 2);
+			let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			let top = rect.top + window.pageYOffset + this.source.offsetHeight;
 
-			this.$el.style.left = x + 'px';
-			this.$el.style.top = y + 'px';
+			left -= (this.$el.offsetWidth / 2);
+
+			if (left + contentWidth - window.pageXOffset > window.innerWidth) {
+				left = window.innerWidth - contentWidth + window.pageXOffset - 1;
+			}
+
+			if (top + contentHeight - window.pageYOffset > window.innerHeight) {
+				top = rect.top + window.pageYOffset - contentHeight;
+				this.$refs.content.style.transformOrigin = 'center bottom';
+			}
+
+			this.$el.style.left = left + 'px';
+			this.$el.style.top = top + 'px';
 		});
 	},
 })
 </script>
 
 <style lang="scss" scoped>
+.tooltip-enter-active,
+.tooltip-leave-active {
+	opacity: 1;
+	transform: scale(1);
+	transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), opacity 200ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.tooltip-enter-from,
+.tooltip-leave-active {
+	opacity: 0;
+	transform: scale(0.75);
+}
+
 .buebdbiu {
 	position: absolute;
 	z-index: 11000;
@@ -57,6 +81,6 @@ export default defineComponent({
 	text-align: center;
 	border-radius: 4px;
 	pointer-events: none;
-	transform-origin: center -16px;
+	transform-origin: center top;
 }
 </style>
diff --git a/src/client/components/widgets.vue b/src/client/components/widgets.vue
new file mode 100644
index 0000000000..23fce7d714
--- /dev/null
+++ b/src/client/components/widgets.vue
@@ -0,0 +1,153 @@
+<template>
+<div class="vjoppmmu">
+	<template v-if="edit">
+		<header>
+			<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)">
+				<template #label>{{ $ts.selectWidget }}</template>
+				<option v-for="widget in widgetDefs" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
+			</MkSelect>
+			<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $ts.add }}</MkButton>
+			<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
+		</header>
+		<XDraggable
+			v-model="_widgets"
+			item-key="id"
+			animation="150"
+		>
+			<template #item="{element}">
+				<div class="customize-container">
+					<button class="config _button" @click.prevent.stop="configWidget(element.id)"><Fa :icon="faCog"/></button>
+					<button class="remove _button" @click.prevent.stop="removeWidget(element)"><Fa :icon="faTimes"/></button>
+					<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column" @updateProps="updateWidget(element.id, $event)"/>
+				</div>
+			</template>
+		</XDraggable>
+	</template>
+	<component v-else class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column" @updateProps="updateWidget(widget.id, $event)"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import { faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
+import MkSelect from '@/components/ui/select.vue';
+import MkButton from '@/components/ui/button.vue';
+import { widgets as widgetDefs } from '@/widgets';
+
+export default defineComponent({
+	components: {
+		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
+		MkSelect,
+		MkButton,
+	},
+
+	props: {
+		widgets: {
+			required: true,
+		},
+		edit: {
+			type: Boolean,
+			required: true,
+		},
+	},
+
+	emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
+
+	data() {
+		return {
+			widgetAdderSelected: null,
+			widgetDefs,
+			settings: {},
+			faTimes, faPlus, faCog
+		};
+	},
+
+	computed: {
+		_widgets: {
+			get() {
+				return this.widgets;
+			},
+			set(value) {
+				this.$emit('updateWidgets', value);
+			}
+		}
+	},
+
+	methods: {
+		configWidget(id) {
+			this.settings[id]();
+		},
+
+		addWidget() {
+			if (this.widgetAdderSelected == null) return;
+
+			this.$emit('addWidget', {
+				name: this.widgetAdderSelected,
+				id: uuid(),
+				data: {}
+			});
+
+			this.widgetAdderSelected = null;
+		},
+
+		removeWidget(widget) {
+			this.$emit('removeWidget', widget);
+		},
+
+		updateWidget(id, data) {
+			this.$emit('updateWidget', { id, data });
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vjoppmmu {
+	> header {
+		margin: 16px 0;
+
+		> * {
+			width: 100%;
+			padding: 4px;
+		}
+	}
+
+	> .widget, .customize-container {
+		margin: var(--margin) 0;
+
+		&:first-of-type {
+			margin-top: 0;
+		}
+	}
+
+	.customize-container {
+		position: relative;
+		cursor: move;
+
+		> *:not(.remove):not(.config) {
+			pointer-events: none;
+		}
+
+		> .config,
+		> .remove {
+			position: absolute;
+			z-index: 10000;
+			top: 8px;
+			width: 32px;
+			height: 32px;
+			color: #fff;
+			background: rgba(#000, 0.7);
+			border-radius: 4px;
+		}
+
+		> .config {
+			right: 8px + 8px + 32px;
+		}
+
+		> .remove {
+			right: 8px;
+		}
+	}
+}
+</style>
diff --git a/src/client/directives/tooltip.ts b/src/client/directives/tooltip.ts
index faeeef79a7..2a0a13663c 100644
--- a/src/client/directives/tooltip.ts
+++ b/src/client/directives/tooltip.ts
@@ -4,6 +4,7 @@ import { popup } from '@/os';
 
 const start = isDeviceTouch ? 'touchstart' : 'mouseover';
 const end = isDeviceTouch ? 'touchend' : 'mouseleave';
+const delay = 100;
 
 export default {
 	mounted(el: HTMLElement, binding, vn) {
@@ -47,13 +48,13 @@ export default {
 		el.addEventListener(start, () => {
 			clearTimeout(self.showTimer);
 			clearTimeout(self.hideTimer);
-			self.showTimer = setTimeout(show, 300);
+			self.showTimer = setTimeout(show, delay);
 		}, { passive: true });
 
 		el.addEventListener(end, () => {
 			clearTimeout(self.showTimer);
 			clearTimeout(self.hideTimer);
-			self.hideTimer = setTimeout(self.close, 300);
+			self.hideTimer = setTimeout(self.close, delay);
 		}, { passive: true });
 
 		el.addEventListener('click', () => {
diff --git a/src/client/style.scss b/src/client/style.scss
index 14e8c87314..de548cc9c9 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -488,19 +488,6 @@ hr {
 	transform: scale(0.9);
 }
 
-.zoom-in-top-enter-active,
-.zoom-in-top-leave-active {
-	opacity: 1;
-	transform: scaleY(1);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-	transform-origin: center top;
-}
-.zoom-in-top-enter-from,
-.zoom-in-top-leave-active {
-	opacity: 0;
-	transform: scaleY(0);
-}
-
 @keyframes blink {
 	0% { opacity: 1; transform: scale(1); }
 	30% { opacity: 1; transform: scale(1); }
diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue
index a54e5f97ff..d1579038c7 100644
--- a/src/client/ui/chat/index.vue
+++ b/src/client/ui/chat/index.vue
@@ -10,10 +10,10 @@
 				</button>
 			</div>
 			<div class="right">
-				<MkA class="item" to="/my/messaging"><Fa class="icon" :icon="faComments"/><i v-if="$i.hasUnreadMessagingMessage"><Fa :icon="faCircle"/></i></MkA>
-				<MkA class="item" to="/my/messages"><Fa class="icon" :icon="faEnvelope"/><i v-if="$i.hasUnreadSpecifiedNotes"><Fa :icon="faCircle"/></i></MkA>
-				<MkA class="item" to="/my/mentions"><Fa class="icon" :icon="faAt"/><i v-if="$i.hasUnreadMentions"><Fa :icon="faCircle"/></i></MkA>
-				<MkA class="item" to="/my/notifications"><Fa class="icon" :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></MkA>
+				<MkA class="item" to="/my/messaging" v-tooltip="$ts.messaging"><Fa class="icon" :icon="faComments"/><i v-if="$i.hasUnreadMessagingMessage"><Fa :icon="faCircle"/></i></MkA>
+				<MkA class="item" to="/my/messages" v-tooltip="$ts.directNotes"><Fa class="icon" :icon="faEnvelope"/><i v-if="$i.hasUnreadSpecifiedNotes"><Fa :icon="faCircle"/></i></MkA>
+				<MkA class="item" to="/my/mentions" v-tooltip="$ts.mentions"><Fa class="icon" :icon="faAt"/><i v-if="$i.hasUnreadMentions"><Fa :icon="faCircle"/></i></MkA>
+				<MkA class="item" to="/my/notifications" v-tooltip="$ts.notifications"><Fa class="icon" :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></MkA>
 			</div>
 		</header>
 		<div class="body">
@@ -63,10 +63,10 @@
 				</button>
 			</div>
 			<div class="right">
-				<button class="_button item search" @click="search">
+				<button class="_button item search" @click="search" v-tooltip="$ts.search">
 					<Fa :icon="faSearch"/>
 				</button>
-				<MkA class="item" to="/settings"><Fa class="icon" :icon="faCog"/></MkA>
+				<MkA class="item" to="/settings" v-tooltip="$ts.settings"><Fa class="icon" :icon="faCog"/></MkA>
 			</div>
 		</footer>
 	</div>
@@ -97,11 +97,12 @@
 			</div>
 
 			<div class="right">
+				<div class="instance">{{ instanceName }}</div>
 				<XHeaderClock class="clock"/>
-				<button class="_button button search" @click="search">
+				<button class="_button button search" @click="search" v-tooltip="$ts.search">
 					<Fa :icon="faSearch"/>
 				</button>
-				<button class="_button button follow" v-if="tl.startsWith('channel:') && currentChannel" :class="{ followed: currentChannel.isFollowing }" @click="toggleChannelFollow">
+				<button class="_button button follow" v-if="tl.startsWith('channel:') && currentChannel" :class="{ followed: currentChannel.isFollowing }" @click="toggleChannelFollow" v-tooltip="currentChannel.isFollowing ? $ts.unfollow : $ts.follow">
 					<Fa v-if="currentChannel.isFollowing" :icon="faStar"/>
 					<Fa v-else :icon="farStar"/>
 				</button>
@@ -121,6 +122,9 @@
 	</main>
 
 	<XSide class="side" ref="side"/>
+	<div class="side">
+		<XWidgets/>
+	</div>
 
 	<XCommon/>
 </div>
@@ -132,6 +136,7 @@ import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, fa
 import { faBell, faStar as farStar, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
 import { instanceName, url } from '@/config';
 import XSidebar from '@/components/sidebar.vue';
+import XWidgets from './widgets.vue';
 import XCommon from '../_common_/common.vue';
 import XSide from './side.vue';
 import XTimeline from './timeline.vue';
@@ -147,6 +152,7 @@ export default defineComponent({
 	components: {
 		XCommon,
 		XSidebar,
+		XWidgets,
 		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
 		XTimeline,
 		XPostForm,
@@ -187,6 +193,7 @@ export default defineComponent({
 			featuredChannels: null,
 			currentChannel: null,
 			menuDef: sidebarDef,
+			instanceName,
 			faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, farStar, faAt, faLink, faEllipsisH, faGlobe, faComments, faEnvelope,
 		};
 	},
@@ -310,8 +317,7 @@ export default defineComponent({
 	$ui-font-size: 1em; // TODO: どこかに集約したい
 
 	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-	min-height: calc(var(--vh, 1vh) * 100);
-	box-sizing: border-box;
+	height: calc(var(--vh, 1vh) * 100);
 	display: flex;
 
 	> .nav {
@@ -518,6 +524,10 @@ export default defineComponent({
 				margin-left: auto;
 				padding-left: 8px;
 
+				> .instance {
+					margin-right: 16px;
+				}
+
 				> .clock {
 					margin-right: 16px;
 				}
@@ -552,6 +562,7 @@ export default defineComponent({
 	}
 
 	> .side {
+		width: 350px;
 		border-left: solid 1px var(--divider);
 	}
 }
diff --git a/src/client/ui/chat/side.vue b/src/client/ui/chat/side.vue
index 188123deb9..9b15c72841 100644
--- a/src/client/ui/chat/side.vue
+++ b/src/client/ui/chat/side.vue
@@ -120,12 +120,8 @@ export default defineComponent({
 	--section-padding: 16px;
 	--margin: var(--marginHalf);
 
-	width: 390px;
-
 	> .container {
-		position: fixed;
-		width: 390px;
-		height: 100vh;
+		height: 100%;
 		overflow: auto;
 		box-sizing: border-box;
 
diff --git a/src/client/ui/chat/store.ts b/src/client/ui/chat/store.ts
new file mode 100644
index 0000000000..a869debd61
--- /dev/null
+++ b/src/client/ui/chat/store.ts
@@ -0,0 +1,13 @@
+import { markRaw } from 'vue';
+import { Storage } from '../../pizzax';
+
+export const store = markRaw(new Storage('chatUi', {
+	widgets: {
+		where: 'account',
+		default: [] as {
+			name: string;
+			id: string;
+			data: Record<string, any>;
+		}[]
+	},
+}));
diff --git a/src/client/ui/chat/widgets.vue b/src/client/ui/chat/widgets.vue
new file mode 100644
index 0000000000..6becaa22e3
--- /dev/null
+++ b/src/client/ui/chat/widgets.vue
@@ -0,0 +1,61 @@
+<template>
+<div class="qydbhufi">
+	<XWidgets :edit="edit" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
+
+	<button v-if="edit" @click="edit = false" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgetsExit }}</button>
+	<button v-else @click="edit = true" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgets }}</button>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import XWidgets from '@/components/widgets.vue';
+import { store } from './store.ts';
+
+export default defineComponent({
+	components: {
+		XWidgets,
+	},
+
+	data() {
+		return {
+			edit: false,
+			widgets: store.reactiveState.widgets
+		};
+	},
+
+	methods: {
+		addWidget(widget) {
+			store.set('widgets', [widget, ...store.state.widgets]);
+		},
+
+		removeWidget(widget) {
+			store.set('widgets', store.state.widgets.filter(w => w.id != widget.id));
+		},
+
+		updateWidget({ id, data }) {
+			store.set('widgets', store.state.widgets.map(w => w.id === id ? {
+				...w,
+				data: data
+			} : w));
+		},
+
+		updateWidgets(widgets) {
+			store.set('widgets', widgets);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qydbhufi {
+	height: 100%;
+	box-sizing: border-box;
+	overflow: auto;
+	padding: var(--margin);
+
+	::v-deep(._panel) {
+		box-shadow: none;
+	}
+}
+</style>
diff --git a/src/client/ui/deck/widgets-column.vue b/src/client/ui/deck/widgets-column.vue
index 5cf7dde26f..b7740c270d 100644
--- a/src/client/ui/deck/widgets-column.vue
+++ b/src/client/ui/deck/widgets-column.vue
@@ -3,49 +3,22 @@
 	<template #header><Fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template>
 
 	<div class="wtdtxvec">
-		<template v-if="edit">
-			<header>
-				<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)">
-					<template #label>{{ $ts.selectWidget }}</template>
-					<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
-				</MkSelect>
-				<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $ts.add }}</MkButton>
-				<MkButton inline @click="edit = false">{{ $ts.close }}</MkButton>
-			</header>
-			<XDraggable
-				v-model="_widgets"
-				item-key="id"
-				animation="150"
-			>
-				<template #item="{element}">
-					<div class="customize-container" @click="widgetFunc(element.id)">
-						<button class="remove _button" @click.prevent.stop="removeWidget(element)"><Fa :icon="faTimes"/></button>
-						<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column" @updateProps="saveWidget(element.id, $event)"/>
-					</div>
-				</template>
-			</XDraggable>
-		</template>
-		<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column" @updateProps="saveWidget(widget.id, $event)"/>
+		<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
 	</div>
 </XColumn>
 </template>
 
 <script lang="ts">
 import { defineComponent, defineAsyncComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
 import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
-import MkSelect from '@/components/ui/select.vue';
-import MkButton from '@/components/ui/button.vue';
+import XWidgets from '@/components/widgets.vue';
 import XColumn from './column.vue';
-import { widgets } from '../../widgets';
 import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
 
 export default defineComponent({
 	components: {
 		XColumn,
-		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
-		MkSelect,
-		MkButton,
+		XWidgets,
 	},
 
 	props: {
@@ -62,49 +35,27 @@ export default defineComponent({
 	data() {
 		return {
 			edit: false,
-			widgetAdderSelected: null,
-			widgets,
-			settings: {},
 			faWindowMaximize, faTimes, faPlus
 		};
 	},
 
-	computed: {
-		_widgets: {
-			get() {
-				return this.column.widgets;
-			},
-			set(value) {
-				setColumnWidgets(this.column.id, value);
-			}
-		}
-	},
-
 	methods: {
-		widgetFunc(id) {
-			this.settings[id]();
-		},
-
-		addWidget() {
-			if (this.widgetAdderSelected == null) return;
-
-			addColumnWidget(this.column.id, {
-				name: this.widgetAdderSelected,
-				id: uuid(),
-				data: {}
-			});
-
-			this.widgetAdderSelected = null;
+		addWidget(widget) {
+			addColumnWidget(this.column.id, widget);
 		},
 
 		removeWidget(widget) {
 			removeColumnWidget(this.column.id, widget);
 		},
 
-		saveWidget(id, data) {
+		updateWidget({ id, data }) {
 			updateColumnWidget(this.column.id, id, data);
 		},
 
+		updateWidgets(widgets) {
+			setColumnWidgets(this.column.id, widgets);
+		},
+
 		func() {
 			this.edit = !this.edit;
 		}
@@ -114,46 +65,12 @@ export default defineComponent({
 
 <style lang="scss" scoped>
 .wtdtxvec {
-	._panel {
+	--margin: 8px;
+
+	padding: 0 var(--margin);
+
+	::v-deep(._panel) {
 		box-shadow: none;
 	}
-
-	> header {
-		padding: 16px;
-
-		> * {
-			width: 100%;
-			padding: 4px;
-		}
-	}
-
-	> .widget, .customize-container {
-		margin: 8px;
-
-		&:first-of-type {
-			margin-top: 0;
-		}
-	}
-
-	.customize-container {
-		position: relative;
-		cursor: move;
-
-		> *:not(.remove) {
-			pointer-events: none;
-		}
-
-		> .remove {
-			position: absolute;
-			z-index: 2;
-			top: 8px;
-			right: 8px;
-			width: 32px;
-			height: 32px;
-			color: #fff;
-			background: rgba(#000, 0.7);
-			border-radius: 4px;
-		}
-	}
 }
 </style>
diff --git a/src/client/ui/default.widgets.vue b/src/client/ui/default.widgets.vue
index ec73c42777..ff7cdf1140 100644
--- a/src/client/ui/default.widgets.vue
+++ b/src/client/ui/default.widgets.vue
@@ -1,46 +1,21 @@
 <template>
 <div class="efzpzdvf">
-	<template v-if="editMode">
-		<MkButton primary @click="addWidget" class="add"><Fa :icon="faPlus"/></MkButton>
-		<XDraggable
-			v-model="widgets"
-			item-key="id"
-			handle=".handle"
-			animation="150"
-			class="sortable"
-		>
-			<template #item="{element}">
-				<div class="customize-container _panel">
-					<header>
-						<span class="handle"><Fa :icon="faBars"/></span>{{ $t('_widgets.' + element.name) }}<button class="remove _button" @click="removeWidget(element)"><Fa :icon="faTimes"/></button>
-					</header>
-					<div @click="widgetFunc(element.id)">
-						<component class="_inContainer_ _forceContainerFull_" :is="`mkw-${element.name}`" :widget="element" :ref="element.id" :setting-callback="setting => settings[element.id] = setting" @updateProps="saveWidget(element.id, $event)"/>
-					</div>
-				</div>
-			</template>
-		</XDraggable>
-		<button @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button>
-	</template>
-	<template v-else>
-		<component v-for="widget in widgets" class="_inContainer_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" @updateProps="saveWidget(widget.id, $event)"/>
-		<button @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button>
-	</template>
+	<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
+
+	<button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button>
+	<button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button>
 </div>
 </template>
 
 <script lang="ts">
 import { defineComponent, defineAsyncComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
 import { faPencilAlt, faPlus, faBars, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons';
-import { widgets } from '@/widgets';
+import XWidgets from '@/components/widgets.vue';
 import * as os from '@/os';
-import MkButton from '@/components/ui/button.vue';
 
 export default defineComponent({
 	components: {
-		MkButton,
-		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
+		XWidgets
 	},
 
 	emits: ['mounted'],
@@ -48,62 +23,35 @@ export default defineComponent({
 	data() {
 		return {
 			editMode: false,
-			settings: {},
 			faPencilAlt, faPlus, faBars, faTimes, faCheck,
 		};
 	},
 
-	computed: {
-		widgets: {
-			get() {
-				return this.$store.reactiveState.widgets.value;
-			},
-			set(value) {
-				this.$store.set('widgets', value);
-			}
-		},
-	},
-
 	mounted() {
 		this.$emit('mounted', this.$el);
 	},
 
 	methods: {
-		widgetFunc(id) {
-			this.settings[id]();
-		},
-
-		async addWidget() {
-			const { canceled, result: widget } = await os.dialog({
-				type: null,
-				title: this.$ts.chooseWidget,
-				select: {
-					items: widgets.map(widget => ({
-						value: widget,
-						text: this.$t('_widgets.' + widget),
-					}))
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			this.$store.set('widgets', [...this.$store.state.widgets, {
-				name: widget,
-				id: uuid(),
+		addWidget(widget) {
+			this.$store.set('widgets', [{
+				...widget,
 				place: null,
-				data: {}
-			}]);
+			}, ...this.$store.state.widgets]);
 		},
 
 		removeWidget(widget) {
 			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
 		},
 
-		saveWidget(id, data) {
+		updateWidget({ id, data }) {
 			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? {
 				...w,
 				data: data
 			} : w));
+		},
+
+		updateWidgets(widgets) {
+			this.$store.set('widgets', widgets);
 		}
 	}
 });
@@ -129,35 +77,5 @@ export default defineComponent({
 	> .add {
 		margin: 0 auto;
 	}
-
-	.customize-container {
-		margin: 8px 0;
-
-		> header {
-			position: relative;
-			line-height: 32px;
-
-			> .handle {
-				padding: 0 8px;
-				cursor: move;
-			}
-
-			> .remove {
-				position: absolute;
-				top: 0;
-				right: 0;
-				padding: 0 8px;
-				line-height: 32px;
-			}
-		}
-
-		> div {
-			padding: 8px;
-
-			> * {
-				pointer-events: none;
-			}
-		}
-	}
 }
 </style>