diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue
index c3753076ac..d522503a14 100644
--- a/packages/client/src/components/notifications.vue
+++ b/packages/client/src/components/notifications.vue
@@ -17,7 +17,7 @@
 </template>
 
 <script lang="ts" setup>
-import { defineComponent, PropType, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
+import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
 import { notificationTypes } from 'misskey-js';
 import MkPagination from '@/components/ui/pagination.vue';
 import { Paging } from '@/components/ui/pagination.vue';
@@ -29,7 +29,7 @@ import { stream } from '@/stream';
 import { $i } from '@/account';
 
 const props = defineProps<{
-	includeTypes?: PropType<typeof notificationTypes[number][]>;
+	includeTypes?: typeof notificationTypes[number][];
 	unreadOnly?: boolean;
 }>();
 
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 5f1fb1ef96..43c110555f 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -293,23 +293,25 @@ export function inputDate(props: {
 	});
 }
 
-export function select(props: {
+export function select<C extends any = any>(props: {
 	title?: string | null;
 	text?: string | null;
 	default?: string | null;
-	items?: {
-		value: string;
+} & ({
+	items: {
+		value: C;
 		text: string;
 	}[];
-	groupedItems?: {
+} | {
+	groupedItems: {
 		label: string;
 		items: {
-			value: string;
+			value: C;
 			text: string;
 		}[];
 	}[];
-}): Promise<{ canceled: true; result: undefined; } | {
-	canceled: false; result: string;
+})): Promise<{ canceled: true; result: undefined; } | {
+	canceled: false; result: C;
 }> {
 	return new Promise((resolve, reject) => {
 		popup(import('@/components/dialog.vue'), {
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index e4571d4091..7e845feef4 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -17,7 +17,8 @@
 			:key="ids[0]"
 			class="column"
 			:column="columns.find(c => c.id === ids[0])"
-			:style="columns.find(c => c.id === ids[0]).flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0]).width + 'px' }"
+			 :is-stacked="false"
+			:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
 			@parent-focus="moveFocus(ids[0], $event)"
 		/>
 	</template>
@@ -25,8 +26,8 @@
 	<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>
+		<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="os.post()"><i class="fas fa-pencil-alt"></i></button>
 	</div>
 
 	<transition :name="$store.state.animation ? 'menu-back' : ''">
@@ -45,8 +46,8 @@
 </div>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, provide, ref, watch } from 'vue';
+<script lang="ts" setup>
+import { computed, provide, ref, watch } from 'vue';
 import { v4 as uuid } from 'uuid';
 import DeckColumnCore from '@/ui/deck/column-core.vue';
 import XSidebar from '@/ui/_common_/sidebar.vue';
@@ -60,102 +61,82 @@ import { useRoute } from 'vue-router';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XCommon,
-		XSidebar,
-		XDrawerMenu,
-		DeckColumnCore,
-	},
-
-	setup() {
-		const isMobile = ref(window.innerWidth <= 500);
-		window.addEventListener('resize', () => {
-			isMobile.value = window.innerWidth <= 500;
-		});
-
-		const drawerMenuShowing = ref(false);
-
-		const route = useRoute();
-		watch(route, () => {
-			drawerMenuShowing.value = false;
-		});
-
-		const columns = deckStore.reactiveState.columns;
-		const layout = deckStore.reactiveState.layout;
-		const menuIndicated = computed(() => {
-			if ($i == null) return false;
-			for (const def in menuDef) {
-				if (menuDef[def].indicated) return true;
-			}
-			return false;
-		});
-
-		const addColumn = async (ev) => {
-			const columns = [
-				'main',
-				'widgets',
-				'notifications',
-				'tl',
-				'antenna',
-				'list',
-				'mentions',
-				'direct',
-			];
-
-			const { canceled, result: column } = await os.select({
-				title: i18n.ts._deck.addColumn,
-				items: columns.map(column => ({
-					value: column, text: i18n.t('_deck._columns.' + column)
-				}))
-			});
-			if (canceled) return;
-
-			addColumnToStore({
-				type: column,
-				id: uuid(),
-				name: i18n.t('_deck._columns.' + column),
-				width: 330,
-			});
-		};
-
-		const onContextmenu = (ev) => {
-			os.contextMenu([{
-				text: i18n.ts._deck.addColumn,
-				icon: null,
-				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,
-		};
-	},
+const isMobile = ref(window.innerWidth <= 500);
+window.addEventListener('resize', () => {
+	isMobile.value = window.innerWidth <= 500;
 });
+
+const drawerMenuShowing = ref(false);
+
+const route = useRoute();
+watch(route, () => {
+	drawerMenuShowing.value = false;
+});
+
+const columns = deckStore.reactiveState.columns;
+const layout = deckStore.reactiveState.layout;
+const menuIndicated = computed(() => {
+	if ($i == null) return false;
+	for (const def in menuDef) {
+		if (menuDef[def].indicated) return true;
+	}
+	return false;
+});
+
+const addColumn = async (ev) => {
+	const columns = [
+		'main',
+		'widgets',
+		'notifications',
+		'tl',
+		'antenna',
+		'list',
+		'mentions',
+		'direct',
+	];
+
+	const { canceled, result: column } = await os.select({
+		title: i18n.ts._deck.addColumn,
+		items: columns.map(column => ({
+			value: column, text: i18n.t('_deck._columns.' + column)
+		}))
+	});
+	if (canceled) return;
+
+	addColumnToStore({
+		type: column,
+		id: uuid(),
+		name: i18n.t('_deck._columns.' + column),
+		width: 330,
+	});
+};
+
+const onContextmenu = (ev) => {
+	os.contextMenu([{
+		text: i18n.ts._deck.addColumn,
+		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 as HTMLElement) == null) {
+		document.documentElement.scrollLeft += ev.deltaY > 0 ? 96 : -96;
+	}
+});
+loadDeck();
+
+function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
+	// TODO??
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/ui/deck/antenna-column.vue b/packages/client/src/ui/deck/antenna-column.vue
index 198ebbbefa..e0f56c2800 100644
--- a/packages/client/src/ui/deck/antenna-column.vue
+++ b/packages/client/src/ui/deck/antenna-column.vue
@@ -1,75 +1,62 @@
 <template>
-<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked">
+<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
 	<template #header>
 		<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
-	<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
+	<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => emit('loaded')"/>
 </XColumn>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
 import XColumn from './column.vue';
 import XTimeline from '@/components/timeline.vue';
 import * as os from '@/os';
-import { updateColumn } from './deck-store';
+import { updateColumn, Column } from './deck-store';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XColumn,
-		XTimeline,
-	},
+const props = defineProps<{
+	column: Column;
+	isStacked: boolean;
+}>();
 
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
+const emit = defineEmits<{
+	(e: 'loaded'): void;
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
 
-	data() {
-		return {
-		};
-	},
+let timeline = $ref<InstanceType<typeof XTimeline>>();
 
-	watch: {
-		mediaOnly() {
-			(this.$refs.timeline as any).reload();
-		}
-	},
-
-	mounted() {
-		if (this.column.antennaId == null) {
-			this.setAntenna();
-		}
-	},
-
-	methods: {
-		async setAntenna() {
-			const antennas = await os.api('antennas/list');
-			const { canceled, result: antenna } = await os.select({
-				title: this.$ts.selectAntenna,
-				items: antennas.map(x => ({
-					value: x, text: x.name
-				})),
-				default: this.column.antennaId
-			});
-			if (canceled) return;
-			updateColumn(this.column.id, {
-				antennaId: antenna.id
-			});
-		},
-
-		focus() {
-			(this.$refs.timeline as any).focus();
-		}
+onMounted(() => {
+	if (props.column.antennaId == null) {
+		setAntenna();
 	}
 });
+
+async function setAntenna() {
+	const antennas = await os.api('antennas/list');
+	const { canceled, result: antenna } = await os.select({
+		title: i18n.ts.selectAntenna,
+		items: antennas.map(x => ({
+			value: x, text: x.name
+		})),
+		default: props.column.antennaId
+	});
+	if (canceled) return;
+	updateColumn(props.column.id, {
+		antennaId: antenna.id
+	});
+}
+/*
+function focus() {
+	timeline.focus();
+}
+
+defineExpose({
+	focus,
+});
+*/
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/ui/deck/column-core.vue b/packages/client/src/ui/deck/column-core.vue
index 5393bac736..485e89a062 100644
--- a/packages/client/src/ui/deck/column-core.vue
+++ b/packages/client/src/ui/deck/column-core.vue
@@ -1,17 +1,18 @@
 <template>
 <!-- TODO: リファクタの余地がありそう -->
-<XMainColumn v-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+<div v-if="!column">たぶん見えちゃいけないやつ</div>
+<XMainColumn v-else-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
+<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
+<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
+<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
+<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
+<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
+<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
+<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XMainColumn from './main-column.vue';
 import XTlColumn from './tl-column.vue';
 import XAntennaColumn from './antenna-column.vue';
@@ -20,33 +21,24 @@ import XNotificationsColumn from './notifications-column.vue';
 import XWidgetsColumn from './widgets-column.vue';
 import XMentionsColumn from './mentions-column.vue';
 import XDirectColumn from './direct-column.vue';
+import { Column } from './deck-store';
 
+defineProps<{
+	column?: Column;
+	isStacked: boolean;
+}>();
+
+const emit = defineEmits<{
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
+
+/*
 export default defineComponent({
-	components: {
-		XMainColumn,
-		XTlColumn,
-		XAntennaColumn,
-		XListColumn,
-		XNotificationsColumn,
-		XWidgetsColumn,
-		XMentionsColumn,
-		XDirectColumn
-	},
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
 	methods: {
 		focus() {
 			this.$children[0].focus();
 		}
 	}
 });
+*/
 </script>
diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue
index f1ce3ca838..4f427b7624 100644
--- a/packages/client/src/ui/deck/column.vue
+++ b/packages/client/src/ui/deck/column.vue
@@ -31,238 +31,211 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+export type DeckFunc = {
+	title: string;
+	handler: (payload: MouseEvent) => void;
+	icon?: string;
+};
+</script>
+<script lang="ts" setup>
+import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
 import * as os from '@/os';
-import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from './deck-store';
+import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store';
 import { deckStore } from './deck-store';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	provide: {
-		shouldHeaderThin: true,
-		shouldOmitHeaderTitle: true,
-	},
+provide('shouldHeaderThin', true);
+provide('shouldOmitHeaderTitle', true);
 
-	props: {
-		column: {
-			type: Object,
-			required: false,
-			default: null
-		},
-		isStacked: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		func: {
-			type: Object,
-			required: false,
-			default: null
-		},
-		naked: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		indicated: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	data() {
-		return {
-			deckStore,
-			dragging: false,
-			draghover: false,
-			dropready: false,
-		};
-	},
-
-	computed: {
-		isMainColumn(): boolean {
-			return this.column.type === 'main';
-		},
-
-		active(): boolean {
-			return this.column.active !== false;
-		},
-
-		keymap(): any {
-			return {
-				'shift+up': () => this.$parent.$emit('parent-focus', 'up'),
-				'shift+down': () => this.$parent.$emit('parent-focus', 'down'),
-				'shift+left': () => this.$parent.$emit('parent-focus', 'left'),
-				'shift+right': () => this.$parent.$emit('parent-focus', 'right'),
-			};
-		}
-	},
-
-	watch: {
-		active(v) {
-			this.$emit('change-active-state', v);
-		},
-
-		dragging(v) {
-			os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd');
-		}
-	},
-
-	mounted() {
-		os.deckGlobalEvents.on('column.dragStart', this.onOtherDragStart);
-		os.deckGlobalEvents.on('column.dragEnd', this.onOtherDragEnd);
-	},
-
-	beforeUnmount() {
-		os.deckGlobalEvents.off('column.dragStart', this.onOtherDragStart);
-		os.deckGlobalEvents.off('column.dragEnd', this.onOtherDragEnd);
-	},
-
-	methods: {
-		onOtherDragStart() {
-			this.dropready = true;
-		},
-
-		onOtherDragEnd() {
-			this.dropready = false;
-		},
-
-		toggleActive() {
-			if (!this.isStacked) return;
-			updateColumn(this.column.id, {
-				active: !this.column.active
-			});
-		},
-
-		getMenu() {
-			const items = [{
-				icon: 'fas fa-pencil-alt',
-				text: this.$ts.edit,
-				action: async () => {
-					const { canceled, result } = await os.form(this.column.name, {
-						name: {
-							type: 'string',
-							label: this.$ts.name,
-							default: this.column.name
-						},
-						width: {
-							type: 'number',
-							label: this.$ts.width,
-							default: this.column.width
-						},
-						flexible: {
-							type: 'boolean',
-							label: this.$ts.flexible,
-							default: this.column.flexible
-						}
-					});
-					if (canceled) return;
-					updateColumn(this.column.id, result);
-				}
-			}, null, {
-				icon: 'fas fa-arrow-left',
-				text: this.$ts._deck.swapLeft,
-				action: () => {
-					swapLeftColumn(this.column.id);
-				}
-			}, {
-				icon: 'fas fa-arrow-right',
-				text: this.$ts._deck.swapRight,
-				action: () => {
-					swapRightColumn(this.column.id);
-				}
-			}, this.isStacked ? {
-				icon: 'fas fa-arrow-up',
-				text: this.$ts._deck.swapUp,
-				action: () => {
-					swapUpColumn(this.column.id);
-				}
-			} : undefined, this.isStacked ? {
-				icon: 'fas fa-arrow-down',
-				text: this.$ts._deck.swapDown,
-				action: () => {
-					swapDownColumn(this.column.id);
-				}
-			} : undefined, null, {
-				icon: 'fas fa-window-restore',
-				text: this.$ts._deck.stackLeft,
-				action: () => {
-					stackLeftColumn(this.column.id);
-				}
-			}, this.isStacked ? {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts._deck.popRight,
-				action: () => {
-					popRightColumn(this.column.id);
-				}
-			} : undefined, null, {
-				icon: 'fas fa-trash-alt',
-				text: this.$ts.remove,
-				danger: true,
-				action: () => {
-					removeColumn(this.column.id);
-				}
-			}];
-
-			return items;
-		},
-
-		onContextmenu(ev: MouseEvent) {
-			os.contextMenu(this.getMenu(), ev);
-		},
-
-		goTop() {
-			this.$refs.body.scrollTo({
-				top: 0,
-				behavior: 'smooth'
-			});
-		},
-
-		onDragstart(e) {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, this.column.id);
-
-			// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
-			// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
-			window.setTimeout(() => {
-				this.dragging = true;
-			}, 10);
-		},
-
-		onDragend(e) {
-			this.dragging = false;
-		},
-
-		onDragover(e) {
-			// 自分自身がドラッグされている場合
-			if (this.dragging) {
-				// 自分自身にはドロップさせない
-				e.dataTransfer.dropEffect = 'none';
-				return;
-			}
-
-			const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_;
-
-			e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
-
-			if (!this.dragging && isDeckColumn) this.draghover = true;
-		},
-
-		onDragleave() {
-			this.draghover = false;
-		},
-
-		onDrop(e) {
-			this.draghover = false;
-			os.deckGlobalEvents.emit('column.dragEnd');
-
-			const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
-			if (id != null && id != '') {
-				swapColumn(this.column.id, id);
-			}
-		}
-	}
+const props = withDefaults(defineProps<{
+	column: Column;
+	isStacked?: boolean;
+	func?: DeckFunc | null;
+	naked?: boolean;
+	indicated?: boolean;
+}>(), {
+	isStacked: false,
+	func: null,
+	naked: false,
+	indicated: false,
 });
+
+const emit = defineEmits<{
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+	(e: 'change-active-state', v: boolean): void;
+}>();
+
+let body = $ref<HTMLDivElement>();
+
+let dragging = $ref(false);
+watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
+
+let draghover = $ref(false);
+let dropready = $ref(false);
+
+const isMainColumn = $computed(() => props.column.type === 'main');
+const active = $computed(() => props.column.active !== false);
+watch($$(active), v => emit('change-active-state', v));
+
+const keymap = $computed(() => ({
+	'shift+up': () => emit('parent-focus', 'up'),
+	'shift+down': () => emit('parent-focus', 'down'),
+	'shift+left': () => emit('parent-focus', 'left'),
+	'shift+right': () => emit('parent-focus', 'right'),
+}));
+
+onMounted(() => {
+	os.deckGlobalEvents.on('column.dragStart', onOtherDragStart);
+	os.deckGlobalEvents.on('column.dragEnd', onOtherDragEnd);
+});
+
+onBeforeUnmount(() => {
+	os.deckGlobalEvents.off('column.dragStart', onOtherDragStart);
+	os.deckGlobalEvents.off('column.dragEnd', onOtherDragEnd);
+});
+
+
+function onOtherDragStart() {
+	dropready = true;
+}
+
+function onOtherDragEnd() {
+	dropready = false;
+}
+
+function toggleActive() {
+	if (!props.isStacked) return;
+	updateColumn(props.column.id, {
+		active: !props.column.active
+	});
+}
+
+function getMenu() {
+	const items = [{
+		icon: 'fas fa-pencil-alt',
+		text: i18n.ts.edit,
+		action: async () => {
+			const { canceled, result } = await os.form(props.column.name, {
+				name: {
+					type: 'string',
+					label: i18n.ts.name,
+					default: props.column.name
+				},
+				width: {
+					type: 'number',
+					label: i18n.ts.width,
+					default: props.column.width
+				},
+				flexible: {
+					type: 'boolean',
+					label: i18n.ts.flexible,
+					default: props.column.flexible
+				}
+			});
+			if (canceled) return;
+			updateColumn(props.column.id, result);
+		}
+	}, null, {
+		icon: 'fas fa-arrow-left',
+		text: i18n.ts._deck.swapLeft,
+		action: () => {
+			swapLeftColumn(props.column.id);
+		}
+	}, {
+		icon: 'fas fa-arrow-right',
+		text: i18n.ts._deck.swapRight,
+		action: () => {
+			swapRightColumn(props.column.id);
+		}
+	}, props.isStacked ? {
+		icon: 'fas fa-arrow-up',
+		text: i18n.ts._deck.swapUp,
+		action: () => {
+			swapUpColumn(props.column.id);
+		}
+	} : undefined, props.isStacked ? {
+		icon: 'fas fa-arrow-down',
+		text: i18n.ts._deck.swapDown,
+		action: () => {
+			swapDownColumn(props.column.id);
+		}
+	} : undefined, null, {
+		icon: 'fas fa-window-restore',
+		text: i18n.ts._deck.stackLeft,
+		action: () => {
+			stackLeftColumn(props.column.id);
+		}
+	}, props.isStacked ? {
+		icon: 'fas fa-window-maximize',
+		text: i18n.ts._deck.popRight,
+		action: () => {
+			popRightColumn(props.column.id);
+		}
+	} : undefined, null, {
+		icon: 'fas fa-trash-alt',
+		text: i18n.ts.remove,
+		danger: true,
+		action: () => {
+			removeColumn(props.column.id);
+		}
+	}];
+	return items;
+}
+
+function onContextmenu(ev: MouseEvent) {
+	os.contextMenu(getMenu(), ev);
+}
+
+function goTop() {
+	body.scrollTo({
+		top: 0,
+		behavior: 'smooth'
+	});
+}
+
+function onDragstart(e) {
+	e.dataTransfer.effectAllowed = 'move';
+	e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
+
+	// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
+	// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
+	window.setTimeout(() => {
+		dragging = true;
+	}, 10);
+}
+
+function onDragend(e) {
+	dragging = false;
+}
+
+function onDragover(e) {
+	// 自分自身がドラッグされている場合
+	if (dragging) {
+		// 自分自身にはドロップさせない
+		e.dataTransfer.dropEffect = 'none';
+		return;
+	}
+
+	const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_;
+
+	e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
+
+	if (!dragging && isDeckColumn) draghover = true;
+}
+
+function onDragleave() {
+	draghover = false;
+}
+
+function onDrop(e) {
+	draghover = false;
+	os.deckGlobalEvents.emit('column.dragEnd');
+
+	const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
+	if (id != null && id != '') {
+		swapColumn(props.column.id, id);
+	}
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts
index 66db5e83ed..f7c39ad8fd 100644
--- a/packages/client/src/ui/deck/deck-store.ts
+++ b/packages/client/src/ui/deck/deck-store.ts
@@ -1,8 +1,9 @@
 import { throttle } from 'throttle-debounce';
 import { i18n } from '@/i18n';
 import { api } from '@/os';
-import { markRaw, watch } from 'vue';
+import { markRaw } from 'vue';
 import { Storage } from '../../pizzax';
+import { notificationTypes } from 'misskey-js';
 
 type ColumnWidget = {
 	name: string;
@@ -10,13 +11,18 @@ type ColumnWidget = {
 	data: Record<string, any>;
 };
 
-type Column = {
+export type Column = {
 	id: string;
 	type: string;
 	name: string | null;
 	width: number;
 	widgets?: ColumnWidget[];
 	active?: boolean;
+	flexible?: boolean;
+	antennaId?: string;
+	listId?: string;
+	includingTypes?: typeof notificationTypes[number][];
+	tl?: 'home' | 'local' | 'social' | 'global';
 };
 
 function copy<T>(x: T): T {
diff --git a/packages/client/src/ui/deck/direct-column.vue b/packages/client/src/ui/deck/direct-column.vue
index ca70f693c3..ebaba574f4 100644
--- a/packages/client/src/ui/deck/direct-column.vue
+++ b/packages/client/src/ui/deck/direct-column.vue
@@ -1,5 +1,5 @@
 <template>
-<XColumn :column="column" :is-stacked="isStacked">
+<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
 	<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<XNotes :pagination="pagination"/>
@@ -7,21 +7,25 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue';
+import { } from 'vue';
 import XColumn from './column.vue';
 import XNotes from '@/components/notes.vue';
-import * as os from '@/os';
+import { Column } from './deck-store';
 
-const props = defineProps<{
-	column: Record<string, unknown>; // TODO
+defineProps<{
+	column: Column;
 	isStacked: boolean;
 }>();
 
+const emit = defineEmits<{
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
+
 const pagination = {
-	point: 'notes/mentions' as const,
+	endpoint: 'notes/mentions' as const,
 	limit: 10,
-	params: computed(() => ({
-		visibility: 'specified' as const,
-	})),
+	params: {
+		visibility: 'specified'
+	},
 };
 </script>
diff --git a/packages/client/src/ui/deck/list-column.vue b/packages/client/src/ui/deck/list-column.vue
index ab04aee4e7..b990516d05 100644
--- a/packages/client/src/ui/deck/list-column.vue
+++ b/packages/client/src/ui/deck/list-column.vue
@@ -1,75 +1,65 @@
 <template>
-<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked">
+<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
 	<template #header>
 		<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
-	<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
+	<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => emit('loaded')"/>
 </XColumn>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import {  } from 'vue';
 import XColumn from './column.vue';
 import XTimeline from '@/components/timeline.vue';
 import * as os from '@/os';
-import { updateColumn } from './deck-store';
+import { updateColumn, Column } from './deck-store';
+import { i18n } from '@/i18n';
+
+const props = defineProps<{
+	column: Column;
+	isStacked: boolean;
+}>();
+
+const emit = defineEmits<{
+	(e: 'loaded'): void;
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
+
+let timeline = $ref<InstanceType<typeof XTimeline>>();
+
+if (props.column.listId == null) {
+	setList();
+}
+
+async function setList() {
+	const lists = await os.api('users/lists/list');
+	const { canceled, result: list } = await os.select({
+		title: i18n.ts.selectList,
+		items: lists.map(x => ({
+			value: x, text: x.name
+		})),
+		default: props.column.listId
+	});
+	if (canceled) return;
+	updateColumn(props.column.id, {
+		listId: list.id
+	});
+}
+
+/*
+function focus() {
+	timeline.focus();
+}
 
 export default defineComponent({
-	components: {
-		XColumn,
-		XTimeline,
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-		};
-	},
-
 	watch: {
 		mediaOnly() {
 			(this.$refs.timeline as any).reload();
 		}
-	},
-
-	mounted() {
-		if (this.column.listId == null) {
-			this.setList();
-		}
-	},
-
-	methods: {
-		async setList() {
-			const lists = await os.api('users/lists/list');
-			const { canceled, result: list } = await os.select({
-				title: this.$ts.selectList,
-				items: lists.map(x => ({
-					value: x, text: x.name
-				})),
-				default: this.column.listId
-			});
-			if (canceled) return;
-			updateColumn(this.column.id, {
-				listId: list.id
-			});
-		},
-
-		focus() {
-			(this.$refs.timeline as any).focus();
-		}
 	}
 });
+*/
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue
index cb045e9a46..57caab44cb 100644
--- a/packages/client/src/ui/deck/main-column.vue
+++ b/packages/client/src/ui/deck/main-column.vue
@@ -1,5 +1,5 @@
 <template>
-<XColumn v-if="deckStore.state.alwaysShowMainColumn || $route.name !== 'index'" :column="column" :is-stacked="isStacked">
+<XColumn v-if="deckStore.state.alwaysShowMainColumn || $route.name !== 'index'" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
 	<template #header>
 		<template v-if="pageInfo">
 			<i :class="pageInfo.icon"></i>
@@ -20,72 +20,59 @@
 </XColumn>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XColumn from './column.vue';
-import XNotes from '@/components/notes.vue';
-import { deckStore } from '@/ui/deck/deck-store';
+import { deckStore, Column } from '@/ui/deck/deck-store';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { router } from '@/router';
 
-export default defineComponent({
-	components: {
-		XColumn,
-		XNotes
-	},
+defineProps<{
+	column: Column;
+	isStacked: boolean;
+}>();
 
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
+const emit = defineEmits<{
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
 
-	data() {
-		return {
-			deckStore,
-			pageInfo: null,
-		}
-	},
+let pageInfo = $ref<Record<string, any> | null>(null);
 
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		back() {
-			history.back();
-		},
-
-		onContextmenu(ev: MouseEvent) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			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;
-			os.contextMenu([{
-				type: 'label',
-				text: path,
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(path);
-				}
-			}], ev);
-		},
+function changePage(page) {
+	if (page == null) return;
+	if (page[symbols.PAGE_INFO]) {
+		pageInfo = page[symbols.PAGE_INFO];
 	}
-});
+}
+/*
+function back() {
+	history.back();
+}
+*/
+function onContextmenu(ev: MouseEvent) {
+	if (!ev.target) return;
+
+	const isLink = (el: HTMLElement) => {
+		if (el.tagName === 'A') return true;
+		if (el.parentElement) {
+			return isLink(el.parentElement);
+		}
+	};
+	if (isLink(ev.target as HTMLElement)) return;
+	if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return;
+	if (window.getSelection()?.toString() !== '') return;
+	const path = router.currentRoute.value.path;
+	os.contextMenu([{
+		type: 'label',
+		text: path,
+	}, {
+		icon: 'fas fa-window-maximize',
+		text: i18n.ts.openInWindow,
+		action: () => {
+			os.pageWindow(path);
+		}
+	}], ev);
+}
 </script>
diff --git a/packages/client/src/ui/deck/mentions-column.vue b/packages/client/src/ui/deck/mentions-column.vue
index 6822e7ef06..a7a012a7fb 100644
--- a/packages/client/src/ui/deck/mentions-column.vue
+++ b/packages/client/src/ui/deck/mentions-column.vue
@@ -1,5 +1,5 @@
 <template>
-<XColumn :column="column" :is-stacked="isStacked">
+<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
 	<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<XNotes :pagination="pagination"/>
@@ -10,13 +10,17 @@
 import { } from 'vue';
 import XColumn from './column.vue';
 import XNotes from '@/components/notes.vue';
-import * as os from '@/os';
+import { Column } from './deck-store';
 
-const props = defineProps<{
-	column: Record<string, unknown>; // TODO
+defineProps<{
+	column: Column;
 	isStacked: boolean;
 }>();
 
+const emit = defineEmits<{
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
+
 const pagination = {
 	endpoint: 'notes/mentions' as const,
 	limit: 10,
diff --git a/packages/client/src/ui/deck/notifications-column.vue b/packages/client/src/ui/deck/notifications-column.vue
index f8f406cdd1..50ee12a275 100644
--- a/packages/client/src/ui/deck/notifications-column.vue
+++ b/packages/client/src/ui/deck/notifications-column.vue
@@ -1,53 +1,38 @@
 <template>
-<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }">
+<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }" @parent-focus="$event => emit('parent-focus', $event)">
 	<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<XNotifications :include-types="column.includingTypes"/>
 </XColumn>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XColumn from './column.vue';
 import XNotifications from '@/components/notifications.vue';
 import * as os from '@/os';
 import { updateColumn } from './deck-store';
+import { Column } from './deck-store';
 
-export default defineComponent({
-	components: {
-		XColumn,
-		XNotifications
-	},
+const props = defineProps<{
+	column: Column;
+	isStacked: boolean;
+}>();
 
-	props: {
-		column: {
-			type: Object,
-			required: true
+const emit = defineEmits<{
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
+
+function func() {
+	os.popup(import('@/components/notification-setting-window.vue'), {
+		includingTypes: props.column.includingTypes,
+	}, {
+		done: async (res) => {
+			const { includingTypes } = res;
+			updateColumn(props.column.id, {
+				includingTypes: includingTypes
+			});
 		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-		}
-	},
-
-	methods: {
-		func() {
-			os.popup(import('@/components/notification-setting-window.vue'), {
-				includingTypes: this.column.includingTypes,
-			}, {
-				done: async (res) => {
-					const { includingTypes } = res;
-					updateColumn(this.column.id, {
-						includingTypes: includingTypes
-					});
-				},
-			}, 'closed');
-		}
-	}
-});
+	}, 'closed');
+}
 </script>
diff --git a/packages/client/src/ui/deck/tl-column.vue b/packages/client/src/ui/deck/tl-column.vue
index 8b22d7efb9..02b9ef83a1 100644
--- a/packages/client/src/ui/deck/tl-column.vue
+++ b/packages/client/src/ui/deck/tl-column.vue
@@ -1,5 +1,5 @@
 <template>
-<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
+<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
 	<template #header>
 		<i v-if="column.tl === 'home'" class="fas fa-home"></i>
 		<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
@@ -15,108 +15,103 @@
 		</p>
 		<p class="desc">{{ $t('disabled-timeline.description') }}</p>
 	</div>
-	<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote"/>
+	<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
 </XColumn>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
 import XColumn from './column.vue';
 import XTimeline from '@/components/timeline.vue';
 import * as os from '@/os';
-import { removeColumn, updateColumn } from './deck-store';
+import { removeColumn, updateColumn, Column } from './deck-store';
+import { $i } from '@/account';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XColumn,
-		XTimeline,
-	},
+const props = defineProps<{
+	column: Column;
+	isStacked: boolean;
+}>();
 
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
+const emit = defineEmits<{
+	(e: 'loaded'): void;
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
+
+let disabled = $ref(false);
+let indicated = $ref(false);
+let columnActive = $ref(true);
+
+onMounted(() => {
+	if (props.column.tl == null) {
+		setType();
+	} else if ($i) {
+		disabled = !$i.isModerator && !$i.isAdmin && (
+			instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
+			instance.disableGlobalTimeline && ['global'].includes(props.column.tl));
+	}
+});
+
+async function setType() {
+	const { canceled, result: src } = await os.select({
+		title: i18n.ts.timeline,
+		items: [{
+			value: 'home' as const, text: i18n.ts._timelines.home
+		}, {
+			value: 'local' as const, text: i18n.ts._timelines.local
+		}, {
+			value: 'social' as const, text: i18n.ts._timelines.social
+		}, {
+			value: 'global' as const, text: i18n.ts._timelines.global
+		}],
+	});
+	if (canceled) {
+		if (props.column.tl == null) {
+			removeColumn(props.column.id);
 		}
-	},
+		return;
+	}
+	updateColumn(props.column.id, {
+		tl: src
+	});
+}
 
-	data() {
-		return {
-			disabled: false,
-			indicated: false,
-			columnActive: true,
-		};
-	},
+function queueUpdated(q) {
+	if (columnActive) {
+		indicated = q !== 0;
+	}
+}
 
+function onNote() {
+	if (!columnActive) {
+		indicated = true;
+	}
+}
+
+function onChangeActiveState(state) {
+	columnActive = state;
+
+	if (columnActive) {
+		indicated = false;
+	}
+}
+
+/*
+export default defineComponent({
 	watch: {
 		mediaOnly() {
 			(this.$refs.timeline as any).reload();
 		}
 	},
 
-	mounted() {
-		if (this.column.tl == null) {
-			this.setType();
-		} else {
-			this.disabled = !this.$i.isModerator && !this.$i.isAdmin && (
-				this.$instance.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) ||
-				this.$instance.disableGlobalTimeline && ['global'].includes(this.column.tl));
-		}
-	},
-
 	methods: {
-		async setType() {
-			const { canceled, result: src } = await os.select({
-				title: this.$ts.timeline,
-				items: [{
-					value: 'home', text: this.$ts._timelines.home
-				}, {
-					value: 'local', text: this.$ts._timelines.local
-				}, {
-					value: 'social', text: this.$ts._timelines.social
-				}, {
-					value: 'global', text: this.$ts._timelines.global
-				}]
-			});
-			if (canceled) {
-				if (this.column.tl == null) {
-					removeColumn(this.column.id);
-				}
-				return;
-			}
-			updateColumn(this.column.id, {
-				tl: src
-			});
-		},
-
-		queueUpdated(q) {
-			if (this.columnActive) {
-				this.indicated = q !== 0;
-			}
-		},
-
-		onNote() {
-			if (!this.columnActive) {
-				this.indicated = true;
-			}
-		},
-
-		onChangeActiveState(state) {
-			this.columnActive = state;
-
-			if (this.columnActive) {
-				this.indicated = false;
-			}
-		},
-
 		focus() {
 			(this.$refs.timeline as any).focus();
 		}
 	}
 });
+*/
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/ui/deck/widgets-column.vue b/packages/client/src/ui/deck/widgets-column.vue
index 8c3a95ac2b..a2edc38357 100644
--- a/packages/client/src/ui/deck/widgets-column.vue
+++ b/packages/client/src/ui/deck/widgets-column.vue
@@ -1,64 +1,49 @@
 <template>
-<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked">
+<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
 	<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
 	<div class="wtdtxvec">
-		<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
+		<XWidgets v-if="column.widgets" :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';
+<script lang="ts" setup>
+import { } from 'vue';
 import XWidgets from '@/components/widgets.vue';
 import XColumn from './column.vue';
-import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
+import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
 
-export default defineComponent({
-	components: {
-		XColumn,
-		XWidgets,
-	},
+const props = defineProps<{
+	column: Column;
+	isStacked: boolean;
+}>();
 
-	props: {
-		column: {
-			type: Object,
-			required: true,
-		},
-		isStacked: {
-			type: Boolean,
-			required: true,
-		},
-	},
+const emit = defineEmits<{
+	(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
+}>();
 
-	data() {
-		return {
-			edit: false,
-		};
-	},
+let edit = $ref(false);
 
-	methods: {
-		addWidget(widget) {
-			addColumnWidget(this.column.id, widget);
-		},
+function addWidget(widget) {
+	addColumnWidget(props.column.id, widget);
+}
 
-		removeWidget(widget) {
-			removeColumnWidget(this.column.id, widget);
-		},
+function removeWidget(widget) {
+	removeColumnWidget(props.column.id, widget);
+}
 
-		updateWidget({ id, data }) {
-			updateColumnWidget(this.column.id, id, data);
-		},
+function updateWidget({ id, data }) {
+	updateColumnWidget(props.column.id, id, data);
+}
 
-		updateWidgets(widgets) {
-			setColumnWidgets(this.column.id, widgets);
-		},
+function updateWidgets(widgets) {
+	setColumnWidgets(props.column.id, widgets);
+}
 
-		func() {
-			this.edit = !this.edit;
-		}
-	}
-});
+function func() {
+	edit = !edit;
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/ui/universal.widgets.vue b/packages/client/src/ui/universal.widgets.vue
index fbfafd10ee..2660e80368 100644
--- a/packages/client/src/ui/universal.widgets.vue
+++ b/packages/client/src/ui/universal.widgets.vue
@@ -1,58 +1,50 @@
 <template>
 <div class="efzpzdvf">
-	<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
+	<XWidgets :edit="editMode" :widgets="defaultStore.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
 
-	<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
-	<button v-else class="_textButton" style="font-size: 0.9em;" @click="editMode = true"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
+	<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="fas fa-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
+	<button v-else class="_textButton" style="font-size: 0.9em;" @click="editMode = true"><i class="fas fa-pencil-alt"></i> {{ i18n.ts.editWidgets }}</button>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
 import XWidgets from '@/components/widgets.vue';
-import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	components: {
-		XWidgets
-	},
+const emit = defineEmits<{
+	(e: 'mounted', el: Element): void;
+}>();
 
-	emits: ['mounted'],
+let editMode = $ref(false);
+let rootEl = $ref<HTMLDivElement>();
 
-	data() {
-		return {
-			editMode: false,
-		};
-	},
-
-	mounted() {
-		this.$emit('mounted', this.$el);
-	},
-
-	methods: {
-		addWidget(widget) {
-			this.$store.set('widgets', [{
-				...widget,
-				place: null,
-			}, ...this.$store.state.widgets]);
-		},
-
-		removeWidget(widget) {
-			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
-		},
-
-		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);
-		}
-	}
+onMounted(() => {
+	emit('mounted', rootEl);
 });
+
+function addWidget(widget) {
+	defaultStore.set('widgets', [{
+		...widget,
+		place: null,
+	}, ...defaultStore.state.widgets]);
+}
+
+function removeWidget(widget) {
+	defaultStore.set('widgets', defaultStore.state.widgets.filter(w => w.id != widget.id));
+}
+
+function updateWidget({ id, data }) {
+	defaultStore.set('widgets', defaultStore.state.widgets.map(w => w.id === id ? {
+		...w,
+		data: data
+	} : w));
+}
+
+function updateWidgets(widgets) {
+	defaultStore.set('widgets', widgets);
+}
 </script>
 
 <style lang="scss" scoped>