From 4b8b413c5170d7bcf9b37eb9bef2511f0ceb2b32 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Mon, 6 Feb 2023 15:52:00 -0500
Subject: [PATCH 01/48] =?UTF-8?q?Improving=20keyboard=20support,=20part=20?=
 =?UTF-8?q?1=E2=84=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/client/src/components/MkButton.vue   |  3 +-
 packages/client/src/components/form/radio.vue |  3 ++
 .../client/src/components/form/switch.vue     |  3 ++
 packages/client/src/directives/tooltip.ts     | 33 ++++++++++++-------
 packages/client/src/pages/admin/_header_.vue  |  6 +---
 packages/client/src/style.scss                |  4 ---
 packages/client/src/ui/_common_/navbar.vue    | 13 ++++++--
 7 files changed, 39 insertions(+), 26 deletions(-)

diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue
index 9042500b4..5220e8c95 100644
--- a/packages/client/src/components/MkButton.vue
+++ b/packages/client/src/components/MkButton.vue
@@ -184,8 +184,7 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&:focus-visible {
-		outline: solid 2px var(--focus);
-		outline-offset: 2px;
+		outline: auto;
 	}
 
 	&.inline {
diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue
index b36f7e9fd..406038582 100644
--- a/packages/client/src/components/form/radio.vue
+++ b/packages/client/src/components/form/radio.vue
@@ -68,6 +68,9 @@ function toggle(): void {
 	&:hover {
 		border-color: var(--inputBorderHover) !important;
 	}
+	&:focus-within {
+		outline: auto;
+	}
 
 	&.checked {
 		background-color: var(--accentedBg) !important;
diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue
index 1ed00ae65..ef717ab6f 100644
--- a/packages/client/src/components/form/switch.vue
+++ b/packages/client/src/components/form/switch.vue
@@ -98,6 +98,9 @@ const toggle = () => {
 			border-color: var(--inputBorderHover) !important;
 		}
 	}
+	&:focus-within > .button {
+		outline: auto;
+	}
 
 	> .label {
 		margin-left: 12px;
diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
index 7738d14e8..91024a6e3 100644
--- a/packages/client/src/directives/tooltip.ts
+++ b/packages/client/src/directives/tooltip.ts
@@ -76,23 +76,32 @@ export default {
 			ev.preventDefault();
 		});
 
+		function showTooltip() {
+			window.clearTimeout(self.showTimer);
+			window.clearTimeout(self.hideTimer);
+			self.showTimer = window.setTimeout(self.show, delay);
+		}
+		function hideTooltip() {
+			window.clearTimeout(self.showTimer);
+			window.clearTimeout(self.hideTimer);
+			self.hideTimer = window.setTimeout(self.close, delay);
+		}
+
 		el.addEventListener(
-			start,
-			() => {
-				window.clearTimeout(self.showTimer);
-				window.clearTimeout(self.hideTimer);
-				self.showTimer = window.setTimeout(self.show, delay);
-			},
+			start, showTooltip,
+			{ passive: true },
+		);
+		el.addEventListener(
+			"focusin", showTooltip,
 			{ passive: true },
 		);
 
 		el.addEventListener(
-			end,
-			() => {
-				window.clearTimeout(self.showTimer);
-				window.clearTimeout(self.hideTimer);
-				self.hideTimer = window.setTimeout(self.close, delay);
-			},
+			end, hideTooltip,
+			{ passive: true },
+		);
+		el.addEventListener(
+			"focusout", hideTooltip,
 			{ passive: true },
 		);
 
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index bdb41b2d2..12702790e 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -265,11 +265,7 @@ onUnmounted(() => {
 			font-weight: normal;
 			opacity: 0.7;
 
-			&:hover {
-				opacity: 1;
-			}
-
-			&.active {
+			&:hover, &:focus-visible, &.active {
 				opacity: 1;
 			}
 
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index ca8bd8b43..01bff888a 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -178,10 +178,6 @@ hr {
 		pointer-events: none;
 	}
 
-	&:focus-visible {
-		outline: none;
-	}
-
 	&:disabled {
 		opacity: 0.5;
 		cursor: default;
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index c44c766b3..e99656ed6 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -341,8 +341,6 @@ function more(ev: MouseEvent) {
 					padding-left: 30px;
 					line-height: 2.85rem;
 					margin-bottom: 0.5rem;
-					text-overflow: ellipsis;
-					overflow: hidden;
 					white-space: nowrap;
 					width: 100%;
 					text-align: left;
@@ -368,6 +366,8 @@ function more(ev: MouseEvent) {
 					> .text {
 						position: relative;
 						font-size: 0.9em;
+						overflow: hidden;
+						text-overflow: ellipsis;
 					}
 
 					&:hover {
@@ -380,7 +380,7 @@ function more(ev: MouseEvent) {
 						color: var(--navActive);
 					}
 
-					&:hover, &.active {
+					&:hover, &:focus-within, &.active {
 						color: var(--accent);
 						transition: all 0.4s ease;
 
@@ -562,5 +562,12 @@ function more(ev: MouseEvent) {
 			}
 		}
 	}
+
+	.item {
+		outline: none;
+		&:focus-visible:before {
+			outline: auto;
+		}
+	}
 }
 </style>

From 2c4881dc94406b4696f8e5572cd15c3297bf112b Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Mon, 6 Feb 2023 23:23:56 -0500
Subject: [PATCH 02/48] Fix tabbing orders & allow closing modals w/ esc

---
 packages/client/src/components/MkMenu.vue      | 14 +++++++-------
 packages/client/src/components/MkModal.vue     |  8 ++++++--
 packages/client/src/components/MkPopupMenu.vue |  2 +-
 packages/client/src/components/MkSuperMenu.vue |  6 +++---
 4 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 78c1ff223..c9f8dbfd6 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -12,33 +12,33 @@
 			<span v-else-if="item.type === 'label'" class="label item">
 				<span>{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
+			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</a>
-			<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else-if="item.type === 'user'" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
+			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
 			</button>
-			<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index ed0e7f59d..62f67a380 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -1,8 +1,8 @@
 <template>
-<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
+<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
+		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1">
 			<slot :max-height="maxHeight" :type="type"></slot>
 		</div>
 	</div>
@@ -214,6 +214,7 @@ const align = () => {
 const onOpened = () => {
 	emit('opened');
 
+	content?.focus()
 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 	const el = content!.children[0];
 	el.addEventListener('mousedown', ev => {
@@ -328,6 +329,9 @@ defineExpose({
 }
 
 .qzhlnise {
+	> .content {
+		border-radius: 16px;
+	}
 	> .bg {
 		&.transparent {
 			background: transparent;
diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index f04c7f561..95234d7ce 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')" tabindex="-1">
 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
 </MkModal>
 </template>
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index 6e73b4959..b784d5239 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -5,15 +5,15 @@
 
 		<div class="items">
 			<template v-for="(item, i) in group.items">
-				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
 					<i v-if="item.icon" class="icon ph-fw ph-lg" :class="item.icon"></i>
 					<span class="text">{{ item.text }}</span>
 				</a>
-				<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
+				<button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
 					<i v-if="item.icon" class="icon ph-fw ph-lg" :class="item.icon"></i>
 					<span class="text">{{ item.text }}</span>
 				</button>
-				<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+				<MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
 					<i v-if="item.icon" class="icon ph-fw ph-lg" :class="item.icon"></i>
 					<span class="text">{{ item.text }}</span>
 				</MkA>

From b8517719af16078bc5003718d3d2a832cdb2b935 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 19:53:38 -0500
Subject: [PATCH 03/48] Use v-bind instead for focus, change tabindex's

---
 packages/client/src/components/MkMenu.vue  | 16 ++++++++--------
 packages/client/src/components/MkModal.vue |  6 +++---
 packages/client/src/directives/focus.ts    |  3 +++
 packages/client/src/directives/index.ts    |  2 ++
 4 files changed, 16 insertions(+), 11 deletions(-)
 create mode 100644 packages/client/src/directives/focus.ts

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index c9f8dbfd6..dd9939dcf 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<div>
+<div tabindex="-1" v-focus>
 	<div
 		ref="itemsEl" v-hotkey="keymap"
 		class="rrevdjwt _popup _shadow"
@@ -12,33 +12,33 @@
 			<span v-else-if="item.type === 'label'" class="label item">
 				<span>{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
+			<span v-else-if="item.type === 'pending'" :tabindex="-1" class="pending item">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="-1" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="-1" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</a>
-			<button v-else-if="item.type === 'user'" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else-if="item.type === 'user'" :tabindex="-1" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<span v-else-if="item.type === 'switch'" :tabindex="-1" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
+			<button v-else-if="item.type === 'parent'" :tabindex="-1" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
 			</button>
-			<button v-else :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="-1" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 62f67a380..6a0744487 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -2,7 +2,7 @@
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1">
+		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
 			<slot :max-height="maxHeight" :type="type"></slot>
 		</div>
 	</div>
@@ -214,7 +214,7 @@ const align = () => {
 const onOpened = () => {
 	emit('opened');
 
-	content?.focus()
+	// content?.focus()
 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 	const el = content!.children[0];
 	el.addEventListener('mousedown', ev => {
@@ -330,7 +330,7 @@ defineExpose({
 
 .qzhlnise {
 	> .content {
-		border-radius: 16px;
+		border-radius: var(--radius);
 	}
 	> .bg {
 		&.transparent {
diff --git a/packages/client/src/directives/focus.ts b/packages/client/src/directives/focus.ts
new file mode 100644
index 000000000..4d34fbf1f
--- /dev/null
+++ b/packages/client/src/directives/focus.ts
@@ -0,0 +1,3 @@
+export default {
+	mounted: (el) => el.focus()
+}
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
index 0a5c32326..77639e2f3 100644
--- a/packages/client/src/directives/index.ts
+++ b/packages/client/src/directives/index.ts
@@ -11,6 +11,7 @@ import anim from "./anim";
 import clickAnime from "./click-anime";
 import panel from "./panel";
 import adaptiveBorder from "./adaptive-border";
+import focus from "./focus";
 
 export default function (app: App) {
 	app.directive("userPreview", userPreview);
@@ -25,4 +26,5 @@ export default function (app: App) {
 	app.directive("click-anime", clickAnime);
 	app.directive("panel", panel);
 	app.directive("adaptive-border", adaptiveBorder);
+	app.directive("focus", focus);
 }

From 1a7a0fdf31d87ad3af17abb9ced9f14a7287c3a0 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 19:53:38 -0500
Subject: [PATCH 04/48] Use v-bind instead for focus, allow focusing submenus,
 change tabindex's

---
 packages/client/src/components/MkMenu.vue  | 16 ++++++++--------
 packages/client/src/components/MkModal.vue |  6 +++---
 packages/client/src/directives/focus.ts    |  3 +++
 packages/client/src/directives/index.ts    |  2 ++
 4 files changed, 16 insertions(+), 11 deletions(-)
 create mode 100644 packages/client/src/directives/focus.ts

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index c9f8dbfd6..dd9939dcf 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<div>
+<div tabindex="-1" v-focus>
 	<div
 		ref="itemsEl" v-hotkey="keymap"
 		class="rrevdjwt _popup _shadow"
@@ -12,33 +12,33 @@
 			<span v-else-if="item.type === 'label'" class="label item">
 				<span>{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
+			<span v-else-if="item.type === 'pending'" :tabindex="-1" class="pending item">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="-1" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="-1" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</a>
-			<button v-else-if="item.type === 'user'" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else-if="item.type === 'user'" :tabindex="-1" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<span v-else-if="item.type === 'switch'" :tabindex="-1" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
+			<button v-else-if="item.type === 'parent'" :tabindex="-1" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
 			</button>
-			<button v-else :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="-1" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 62f67a380..6a0744487 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -2,7 +2,7 @@
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1">
+		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
 			<slot :max-height="maxHeight" :type="type"></slot>
 		</div>
 	</div>
@@ -214,7 +214,7 @@ const align = () => {
 const onOpened = () => {
 	emit('opened');
 
-	content?.focus()
+	// content?.focus()
 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 	const el = content!.children[0];
 	el.addEventListener('mousedown', ev => {
@@ -330,7 +330,7 @@ defineExpose({
 
 .qzhlnise {
 	> .content {
-		border-radius: 16px;
+		border-radius: var(--radius);
 	}
 	> .bg {
 		&.transparent {
diff --git a/packages/client/src/directives/focus.ts b/packages/client/src/directives/focus.ts
new file mode 100644
index 000000000..4d34fbf1f
--- /dev/null
+++ b/packages/client/src/directives/focus.ts
@@ -0,0 +1,3 @@
+export default {
+	mounted: (el) => el.focus()
+}
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
index 0a5c32326..77639e2f3 100644
--- a/packages/client/src/directives/index.ts
+++ b/packages/client/src/directives/index.ts
@@ -11,6 +11,7 @@ import anim from "./anim";
 import clickAnime from "./click-anime";
 import panel from "./panel";
 import adaptiveBorder from "./adaptive-border";
+import focus from "./focus";
 
 export default function (app: App) {
 	app.directive("userPreview", userPreview);
@@ -25,4 +26,5 @@ export default function (app: App) {
 	app.directive("click-anime", clickAnime);
 	app.directive("panel", panel);
 	app.directive("adaptive-border", adaptiveBorder);
+	app.directive("focus", focus);
 }

From e30c159226d0654cdecbef6d11fb500bdd2ce7b6 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 19:56:35 -0500
Subject: [PATCH 05/48] forgot to remove comment

---
 packages/client/src/components/MkModal.vue | 1 -
 1 file changed, 1 deletion(-)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 6a0744487..029e03939 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -214,7 +214,6 @@ const align = () => {
 const onOpened = () => {
 	emit('opened');
 
-	// content?.focus()
 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 	const el = content!.children[0];
 	el.addEventListener('mousedown', ev => {

From 8b92fc8bb8834f30c6c3ffebb6ff86c66f7bed46 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 20:25:22 -0500
Subject: [PATCH 06/48] oops

---
 packages/client/src/components/MkModal.vue | 2 --
 1 file changed, 2 deletions(-)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 9bea5dfc0..029e03939 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -2,7 +2,6 @@
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
 		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
 			<slot :max-height="maxHeight" :type="type"></slot>
 		</div>
@@ -331,7 +330,6 @@ defineExpose({
 .qzhlnise {
 	> .content {
 		border-radius: var(--radius);
-		border-radius: var(--radius);
 	}
 	> .bg {
 		&.transparent {

From 9cc5073dd7ab754b2141c008f7289de25729e977 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 21:19:37 -0500
Subject: [PATCH 07/48] fixes

---
 packages/client/src/components/MkMenu.vue     | 22 ++++++++++---------
 .../client/src/components/MkPopupMenu.vue     |  2 +-
 2 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index dd9939dcf..2037f205d 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -12,33 +12,33 @@
 			<span v-else-if="item.type === 'label'" class="label item">
 				<span>{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="-1" class="pending item">
+			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="-1" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="-1" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</a>
-			<button v-else-if="item.type === 'user'" :tabindex="-1" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else-if="item.type === 'user'" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
 			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="-1" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="-1" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
+			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<span>{{ item.text }}</span>
 				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
 			</button>
-			<button v-else :tabindex="-1" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 				<span>{{ item.text }}</span>
@@ -207,8 +207,7 @@ onBeforeUnmount(() => {
 		font-size: 0.9em;
 		line-height: 20px;
 		text-align: left;
-		overflow: hidden;
-		text-overflow: ellipsis;
+		outline: none;
 
 		&:before {
 			content: "";
@@ -232,7 +231,7 @@ onBeforeUnmount(() => {
 			transform: translateY(0em);
 		}
 
-		&:not(:disabled):hover {
+		&:not(:disabled):hover, &:focus-visible {
 			color: var(--accent);
 			text-decoration: none;
 
@@ -240,6 +239,9 @@ onBeforeUnmount(() => {
 				background: var(--accentedBg);
 			}
 		}
+		&:focus-visible:before {
+			outline: auto;
+		}
 
 		&.danger {
 			color: #eb6f92;
diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index 95234d7ce..386330f15 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -1,5 +1,5 @@
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')" tabindex="-1">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')" tabindex="-1" v-focus>
 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
 </MkModal>
 </template>

From 841f8bee60e17dbe8ce398074a362ec6f8c01a04 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Wed, 8 Feb 2023 21:55:07 -0500
Subject: [PATCH 08/48] focus selected page

---
 packages/client/src/components/global/RouterView.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index e21a57471..31de1e337 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -1,7 +1,7 @@
 <template>
 <KeepAlive :max="defaultStore.state.numberOfPageCache">
 	<Suspense>
-		<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
+		<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)" tabindex="-1" v-focus/>
 
 		<template #fallback>
 			<MkLoading/>

From 6e8221a62a1c50641cd8f4bcd9ef121860de76be Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 10 Feb 2023 11:43:47 -0500
Subject: [PATCH 09/48] Change line-height to w3c recommend

https://www.w3.org/TR/WCAG21/#text-spacing
---
 packages/client/src/style.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index 01bff888a..2e53aadd7 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -32,7 +32,7 @@ html {
 	overflow-wrap: break-word;
 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
 	font-size: 14px;
-	line-height: 1.35;
+	line-height: 1.6;
 	text-size-adjust: 100%;
 	tab-size: 2;
 

From 14e3b3ac965a636ee8d9063f3d3dc3c0063782de Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sat, 11 Feb 2023 14:11:25 -0500
Subject: [PATCH 10/48] focus to more elements

---
 packages/client/src/components/MkLaunchPad.vue | 2 +-
 packages/client/src/components/MkSuperMenu.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index a88700131..ae3f3dcbe 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -96,7 +96,7 @@ function close() {
 			height: 100px;
 			border-radius: 10px;
 
-			&:hover {
+			&:hover, &:focus-visible {
 				color: var(--accent);
 				background: var(--accentedBg);
 				text-decoration: none;
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index b784d5239..b58906929 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -67,7 +67,7 @@ export default defineComponent({
 				font-size: 0.9em;
 				margin-bottom: 0.3rem;
 
-				&:hover {
+				&:hover, &:focus-visible {
 					text-decoration: none;
 					background: var(--panelHighlight);
 				}

From e266cc0b9928e368914b877834563bc0e253c8bb Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 20:49:41 -0500
Subject: [PATCH 11/48] Focus last element when exiting modal

---
 packages/client/src/components/MkModal.vue | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 029e03939..bc4478def 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -77,10 +77,12 @@ const type = $computed(() => {
 
 let contentClicking = false;
 
+const focusedElement = document.activeElement;
 const close = () => {
 	if (props.src) props.src.style.pointerEvents = 'auto';
 	showing = false;
 	emit('close');
+	focusedElement.focus();
 };
 
 const onBgClick = () => {

From ded8ab980a0de90d1d4d887b00970d4d2718cc32 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 20:59:11 -0500
Subject: [PATCH 12/48] Allow focusing of elements inside notes/posts

---
 packages/client/src/components/MkNote.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index ee6c5770f..fc33e649b 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -193,8 +193,8 @@ const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
 	'q': () => renoteButton.value.renote(true),
-	'up|k|shift+tab': focusBefore,
-	'down|j|tab': focusAfter,
+	'up|k': focusBefore,
+	'down|j': focusAfter,
 	'esc': blur,
 	'm|o': () => menu(true),
 	's': () => showContent.value !== showContent.value,

From 5e07df2ed065c92eb1134c7e1b323f884dae391a Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 21:56:01 -0500
Subject: [PATCH 13/48] FOCUS TRAPPING!!!!

---
 package.json                                  |  2 +
 .../client/src/components/MkMenu.child.vue    |  5 ++-
 packages/client/src/components/MkMenu.vue     |  8 +---
 packages/client/src/components/MkModal.vue    | 13 ++++---
 .../client/src/components/MkModalWindow.vue   | 29 +++++++-------
 pnpm-lock.yaml                                | 38 ++++++++++++++++++-
 6 files changed, 68 insertions(+), 27 deletions(-)

diff --git a/package.json b/package.json
index e8da6c5a7..34015128b 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
 		"@bull-board/ui": "^4.10.2",
 		"@tensorflow/tfjs": "^3.21.0",
 		"calckey-js": "^0.0.22",
+		"focus-trap": "^7.2.0",
+		"focus-trap-vue": "^4.0.1",
 		"js-yaml": "4.1.0",
 		"phosphor-icons": "^1.4.2",
 		"seedrandom": "^3.0.5"
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index 3ada4afbd..ee0514aab 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,6 +1,8 @@
 <template>
 <div ref="el" class="sfhdhdhr">
-	<MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
+	<FocusTrap v-bind:active="isActive">
+		<MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
+	</FocusTrap>
 </div>
 </template>
 
@@ -9,6 +11,7 @@ import { on } from 'events';
 import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue';
 import MkMenu from './MkMenu.vue';
 import { MenuItem } from '@/types/menu';
+import { FocusTrap } from 'focus-trap-vue';
 import * as os from '@/os';
 
 const props = defineProps<{
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index be7cd72bd..b7fd75478 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,7 +1,7 @@
 <template>
 <div tabindex="-1" v-focus>
 	<div
-		ref="itemsEl" v-hotkey="keymap"
+		ref="itemsEl"
 		class="rrevdjwt _popup _shadow"
 		:class="{ center: align === 'center', asDrawer }"
 		:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@@ -84,12 +84,6 @@ let items2: InnerMenuItem[] = $ref([]);
 
 let child = $ref<InstanceType<typeof XChild>>();
 
-let keymap = computed(() => ({
-	'up|k|shift+tab': focusUp,
-	'down|j|tab': focusDown,
-	'esc': close,
-}));
-
 let childShowingItem = $ref<MenuItem | null>();
 
 watch(() => props.items, () => {
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index bc4478def..e31818877 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -1,11 +1,13 @@
 <template>
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
-	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
-		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
-			<slot :max-height="maxHeight" :type="type"></slot>
+	<focus-trap v-model:active="isActive">
+		<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+			<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
+			<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
+				<slot :max-height="maxHeight" :type="type"></slot>
+			</div>
 		</div>
-	</div>
+	</focus-trap>
 </transition>
 </template>
 
@@ -15,6 +17,7 @@ import * as os from '@/os';
 import { isTouchUsing } from '@/scripts/touch';
 import { defaultStore } from '@/store';
 import { deviceKind } from '@/scripts/device-kind';
+import { FocusTrap } from 'focus-trap-vue';
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === 'BODY') return null;
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 666382466..77ae1bd8c 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -1,23 +1,26 @@
 <template>
-<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
-	<div ref="rootEl" class="ebkgoccj _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
-		<div ref="headerEl" class="header">
-			<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
-			<span class="title">
-				<slot name="header"></slot>
-			</span>
-			<button v-if="!withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
-			<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check-bold ph-lg"></i></button>
+<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @keyup.esc="$emit('close')" @closed="$emit('closed')">
+	<FocusTrap v-model:active="isActive">
+		<div ref="rootEl" class="ebkgoccj _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
+			<div ref="headerEl" class="header">
+				<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
+				<span class="title">
+					<slot name="header"></slot>
+				</span>
+				<button v-if="!withOkButton" class="_button" @click="$emit('close')"><i class="ph-x-bold ph-lg"></i></button>
+				<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check-bold ph-lg"></i></button>
+			</div>
+			<div class="body">
+				<slot :width="bodyWidth" :height="bodyHeight"></slot>
+			</div>
 		</div>
-		<div class="body">
-			<slot :width="bodyWidth" :height="bodyHeight"></slot>
-		</div>
-	</div>
+	</FocusTrap>
 </MkModal>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted } from 'vue';
+import { FocusTrap } from 'focus-trap-vue';
 import MkModal from './MkModal.vue';
 
 const props = withDefaults(defineProps<{
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ee8486978..f295e1c1e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,6 +16,8 @@ importers:
       cross-env: 7.0.3
       cypress: 10.11.0
       execa: 5.1.1
+      focus-trap: ^7.2.0
+      focus-trap-vue: ^4.0.1
       gulp: 4.0.2
       gulp-cssnano: 2.1.3
       gulp-rename: 2.0.0
@@ -33,6 +35,8 @@ importers:
       '@bull-board/ui': 4.10.2
       '@tensorflow/tfjs': 3.21.0_seedrandom@3.0.5
       calckey-js: 0.0.22
+      focus-trap: 7.2.0
+      focus-trap-vue: 4.0.1_focus-trap@7.2.0
       js-yaml: 4.1.0
       phosphor-icons: 1.4.2
       seedrandom: 3.0.5
@@ -3387,7 +3391,7 @@ packages:
   /axios/0.25.0_debug@4.3.4:
     resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==}
     dependencies:
-      follow-redirects: 1.15.2
+      follow-redirects: 1.15.2_debug@4.3.4
     transitivePeerDependencies:
       - debug
     dev: true
@@ -6364,6 +6368,21 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
+  /focus-trap-vue/4.0.1_focus-trap@7.2.0:
+    resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
+    peerDependencies:
+      focus-trap: ^7.0.0
+      vue: ^3.0.0
+    dependencies:
+      focus-trap: 7.2.0
+    dev: false
+
+  /focus-trap/7.2.0:
+    resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==}
+    dependencies:
+      tabbable: 6.0.1
+    dev: false
+
   /follow-redirects/1.15.2:
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     engines: {node: '>=4.0'}
@@ -6372,6 +6391,19 @@ packages:
     peerDependenciesMeta:
       debug:
         optional: true
+    dev: false
+
+  /follow-redirects/1.15.2_debug@4.3.4:
+    resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+    dependencies:
+      debug: 4.3.4
+    dev: true
 
   /for-each/0.3.3:
     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -11986,6 +12018,10 @@ packages:
     resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
     dev: true
 
+  /tabbable/6.0.1:
+    resolution: {integrity: sha512-SYJSIgeyXW7EuX1ytdneO5e8jip42oHWg9xl/o3oTYhmXusZVgiA+VlPvjIN+kHii9v90AmzTZEBcsEvuAY+TA==}
+    dev: false
+
   /tapable/2.2.1:
     resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
     engines: {node: '>=6'}

From afbdd3bbcc5f614dc1e29a582316915ac2c95585 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 22:16:15 -0500
Subject: [PATCH 14/48] foxes

---
 packages/client/src/components/MkMenu.vue  | 99 +++++++++++-----------
 packages/client/src/components/MkModal.vue |  4 +-
 2 files changed, 53 insertions(+), 50 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index b7fd75478..acfaf9459 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,54 +1,56 @@
 <template>
 <div tabindex="-1" v-focus>
-	<div
-		ref="itemsEl"
-		class="rrevdjwt _popup _shadow"
-		:class="{ center: align === 'center', asDrawer }"
-		:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-		@contextmenu.self="e => e.preventDefault()"
-	>
-		<template v-for="(item, i) in items2">
-			<div v-if="item === null" class="divider"></div>
-			<span v-else-if="item.type === 'label'" class="label item">
-				<span>{{ item.text }}</span>
+	<FocusTrap v-bind:active="isActive">
+		<div
+			ref="itemsEl"
+			class="rrevdjwt _popup _shadow"
+			:class="{ center: align === 'center', asDrawer }"
+			:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
+			@contextmenu.self="e => e.preventDefault()"
+		>
+			<template v-for="(item, i) in items2">
+				<div v-if="item === null" class="divider"></div>
+				<span v-else-if="item.type === 'label'" class="label item">
+					<span>{{ item.text }}</span>
+				</span>
+				<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
+					<span><MkEllipsis/></span>
+				</span>
+				<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
+					<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+					<span>{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
+				</MkA>
+				<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
+					<span>{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
+				</a>
+				<button v-else-if="item.type === 'user' && !items.hidden" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
+					<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
+				</button>
+				<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
+				</span>
+				<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
+					<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
+					<span>{{ item.text }}</span>
+					<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
+				</button>
+				<button v-else-if="!item.hidden" :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+					<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
+					<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+					<span>{{ item.text }}</span>
+					<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
+				</button>
+			</template>
+			<span v-if="items2.length === 0" class="none item">
+				<span>{{ i18n.ts.none }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" :tabindex="0" class="pending item">
-				<span><MkEllipsis/></span>
-			</span>
-			<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="0" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
-				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
-			</MkA>
-			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="0" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
-			</a>
-			<button v-else-if="item.type === 'user' && !items.hidden" :tabindex="0" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
-				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
-			</button>
-			<span v-else-if="item.type === 'switch'" :tabindex="0" class="item" @click.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
-			</span>
-			<button v-else-if="item.type === 'parent'" :tabindex="0" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)" @click="showChildren(item, $event)">
-				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
-				<span>{{ item.text }}</span>
-				<span class="caret"><i class="ph-caret-right-bold ph-lg ph-fw ph-lg"></i></span>
-			</button>
-			<button v-else-if="!item.hidden" :tabindex="0" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
-				<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
-				<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
-				<span>{{ item.text }}</span>
-				<span v-if="item.indicate" class="indicator"><i class="ph-circle-fill"></i></span>
-			</button>
-		</template>
-		<span v-if="items2.length === 0" class="none item">
-			<span>{{ i18n.ts.none }}</span>
-		</span>
-	</div>
+		</div>
+	</FocusTrap>
 	<div v-if="childMenu" class="child">
 		<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
 	</div>
@@ -62,6 +64,7 @@ import FormSwitch from '@/components/form/switch.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
+import { FocusTrap } from 'focus-trap-vue';
 
 const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
 
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index e31818877..32bffa038 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -1,9 +1,9 @@
 <template>
 <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @keyup.esc="emit('click')" @after-enter="onOpened">
 	<focus-trap v-model:active="isActive">
-		<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+		<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }" tabindex="-1" v-focus>
 			<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-			<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick" tabindex="-1" v-focus>
+			<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
 				<slot :max-height="maxHeight" :type="type"></slot>
 			</div>
 		</div>

From 3d68efff4c6b3991a7cba1f139c315f38514490a Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 22:19:40 -0500
Subject: [PATCH 15/48] focus trap emoji picker (also fixes closing w/ esc)

---
 .../client/src/components/MkEmojiPicker.vue   | 131 +++++++++---------
 1 file changed, 67 insertions(+), 64 deletions(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index b26eb1206..578407d76 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -1,80 +1,82 @@
 <template>
-<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
-	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
-	<div ref="emojis" class="emojis">
-		<section class="result">
-			<div v-if="searchResultCustom.length > 0" class="body">
-				<button
-					v-for="emoji in searchResultCustom"
-					:key="emoji.id"
-					class="_button item"
-					:title="emoji.name"
-					tabindex="0"
-					@click="chosen(emoji, $event)"
-				>
-					<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
-					<img class="emoji" :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
-				</button>
-			</div>
-			<div v-if="searchResultUnicode.length > 0" class="body">
-				<button
-					v-for="emoji in searchResultUnicode"
-					:key="emoji.name"
-					class="_button item"
-					:title="emoji.name"
-					tabindex="0"
-					@click="chosen(emoji, $event)"
-				>
-					<MkEmoji class="emoji" :emoji="emoji.char"/>
-				</button>
-			</div>
-		</section>
-
-		<div v-if="tab === 'index'" class="group index">
-			<section v-if="showPinned">
-				<div class="body">
+<FocusTrap v-bind:active="isActive">
+	<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
+		<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
+		<div ref="emojis" class="emojis">
+			<section class="result">
+				<div v-if="searchResultCustom.length > 0" class="body">
 					<button
-						v-for="emoji in pinned"
-						:key="emoji"
+						v-for="emoji in searchResultCustom"
+						:key="emoji.id"
 						class="_button item"
+						:title="emoji.name"
 						tabindex="0"
 						@click="chosen(emoji, $event)"
 					>
-						<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
+						<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+						<img class="emoji" :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+					</button>
+				</div>
+				<div v-if="searchResultUnicode.length > 0" class="body">
+					<button
+						v-for="emoji in searchResultUnicode"
+						:key="emoji.name"
+						class="_button item"
+						:title="emoji.name"
+						tabindex="0"
+						@click="chosen(emoji, $event)"
+					>
+						<MkEmoji class="emoji" :emoji="emoji.char"/>
 					</button>
 				</div>
 			</section>
 
-			<section>
-				<header><i class="ph-alarm-bold ph-fw ph-lg"></i> {{ i18n.ts.recentUsed }}</header>
-				<div class="body">
-					<button
-						v-for="emoji in recentlyUsedEmojis"
-						:key="emoji"
-						class="_button item"
-						@click="chosen(emoji, $event)"
-					>
-						<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
-					</button>
-				</div>
-			</section>
+			<div v-if="tab === 'index'" class="group index">
+				<section v-if="showPinned">
+					<div class="body">
+						<button
+							v-for="emoji in pinned"
+							:key="emoji"
+							class="_button item"
+							tabindex="0"
+							@click="chosen(emoji, $event)"
+						>
+							<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
+						</button>
+					</div>
+				</section>
+
+				<section>
+					<header><i class="ph-alarm-bold ph-fw ph-lg"></i> {{ i18n.ts.recentUsed }}</header>
+					<div class="body">
+						<button
+							v-for="emoji in recentlyUsedEmojis"
+							:key="emoji"
+							class="_button item"
+							@click="chosen(emoji, $event)"
+						>
+							<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
+						</button>
+					</div>
+				</section>
+			</div>
+			<div v-once class="group">
+				<header>{{ i18n.ts.customEmojis }}</header>
+				<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
+			</div>
+			<div v-once class="group">
+				<header>{{ i18n.ts.emoji }}</header>
+				<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
+			</div>
 		</div>
-		<div v-once class="group">
-			<header>{{ i18n.ts.customEmojis }}</header>
-			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
-		</div>
-		<div v-once class="group">
-			<header>{{ i18n.ts.emoji }}</header>
-			<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
+		<div class="tabs">
+			<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk-bold ph-lg ph-fw ph-lg"></i></button>
+			<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley-bold ph-lg ph-fw ph-lg"></i></button>
+			<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf-bold ph-lg ph-fw ph-lg"></i></button>
+			<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash-bold ph-lg ph-fw ph-lg"></i></button>
 		</div>
 	</div>
-	<div class="tabs">
-		<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk-bold ph-lg ph-fw ph-lg"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley-bold ph-lg ph-fw ph-lg"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf-bold ph-lg ph-fw ph-lg"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash-bold ph-lg ph-fw ph-lg"></i></button>
-	</div>
-</div>
+</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -90,6 +92,7 @@ import { deviceKind } from '@/scripts/device-kind';
 import { emojiCategories, instance } from '@/instance';
 import { i18n } from '@/i18n';
 import { defaultStore } from '@/store';
+import { FocusTrap } from 'focus-trap-vue';
 
 const props = withDefaults(defineProps<{
 	showPinned?: boolean;

From aab0377a1e350376e45c8738615db12043b565ef Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 22:24:12 -0500
Subject: [PATCH 16/48] fix submenu positioning

---
 packages/client/src/components/MkMenu.vue | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index acfaf9459..fe7ebbe1a 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,13 +1,13 @@
 <template>
 <div tabindex="-1" v-focus>
-	<FocusTrap v-bind:active="isActive">
-		<div
-			ref="itemsEl"
-			class="rrevdjwt _popup _shadow"
-			:class="{ center: align === 'center', asDrawer }"
-			:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-			@contextmenu.self="e => e.preventDefault()"
-		>
+	<div
+	ref="itemsEl"
+	class="rrevdjwt _popup _shadow"
+	:class="{ center: align === 'center', asDrawer }"
+	:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
+	@contextmenu.self="e => e.preventDefault()"
+	>
+		<FocusTrap v-bind:active="isActive">
 			<template v-for="(item, i) in items2">
 				<div v-if="item === null" class="divider"></div>
 				<span v-else-if="item.type === 'label'" class="label item">
@@ -49,8 +49,8 @@
 			<span v-if="items2.length === 0" class="none item">
 				<span>{{ i18n.ts.none }}</span>
 			</span>
-		</div>
-	</FocusTrap>
+		</FocusTrap>
+	</div>
 	<div v-if="childMenu" class="child">
 		<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
 	</div>

From a52281d4e018b33d53911b9376cf742556877157 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 12 Feb 2023 22:31:55 -0500
Subject: [PATCH 17/48] actually fix submenu pos

---
 packages/client/src/components/MkMenu.vue | 26 +++++++++++------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index fe7ebbe1a..2e25843cd 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,13 +1,13 @@
 <template>
 <div tabindex="-1" v-focus>
-	<div
-	ref="itemsEl"
-	class="rrevdjwt _popup _shadow"
-	:class="{ center: align === 'center', asDrawer }"
-	:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-	@contextmenu.self="e => e.preventDefault()"
-	>
-		<FocusTrap v-bind:active="isActive">
+	<FocusTrap v-bind:active="isActive">
+		<div
+			ref="itemsEl"
+			class="rrevdjwt _popup _shadow"
+			:class="{ center: align === 'center', asDrawer }"
+			:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
+			@contextmenu.self="e => e.preventDefault()"
+		>
 			<template v-for="(item, i) in items2">
 				<div v-if="item === null" class="divider"></div>
 				<span v-else-if="item.type === 'label'" class="label item">
@@ -49,11 +49,11 @@
 			<span v-if="items2.length === 0" class="none item">
 				<span>{{ i18n.ts.none }}</span>
 			</span>
-		</FocusTrap>
-	</div>
-	<div v-if="childMenu" class="child">
-		<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
-	</div>
+		</div>
+		<div v-if="childMenu" class="child">
+			<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
+		</div>
+	</FocusTrap>
 </div>
 </template>
 

From d61a314f40d493201e6aebd8b4480acfce2c5bfe Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 19:06:29 -0400
Subject: [PATCH 18/48] forgot to add this

---
 pnpm-lock.yaml | 41 ++++++++++++++++++++++++++---------------
 1 file changed, 26 insertions(+), 15 deletions(-)

diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cf4c29c06..9c5079f98 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,6 +19,12 @@ importers:
       '@tensorflow/tfjs':
         specifier: ^3.21.0
         version: 3.21.0(seedrandom@3.0.5)
+      focus-trap:
+        specifier: ^7.2.0
+        version: 7.2.0
+      focus-trap-vue:
+        specifier: ^4.0.1
+        version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
@@ -3803,14 +3809,12 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       source-map: 0.6.1
-    dev: true
 
   /@vue/compiler-dom@3.2.45:
     resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
     dependencies:
       '@vue/compiler-core': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/compiler-sfc@2.7.14:
     resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@@ -3833,14 +3837,12 @@ packages:
       magic-string: 0.25.9
       postcss: 8.4.21
       source-map: 0.6.1
-    dev: true
 
   /@vue/compiler-ssr@3.2.45:
     resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
     dependencies:
       '@vue/compiler-dom': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/reactivity-transform@3.2.45:
     resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@@ -3850,20 +3852,17 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       magic-string: 0.25.9
-    dev: true
 
   /@vue/reactivity@3.2.45:
     resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
     dependencies:
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/runtime-core@3.2.45:
     resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
     dependencies:
       '@vue/reactivity': 3.2.45
       '@vue/shared': 3.2.45
-    dev: true
 
   /@vue/runtime-dom@3.2.45:
     resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@@ -3871,7 +3870,6 @@ packages:
       '@vue/runtime-core': 3.2.45
       '@vue/shared': 3.2.45
       csstype: 2.6.21
-    dev: true
 
   /@vue/server-renderer@3.2.45(vue@3.2.45):
     resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@@ -3881,11 +3879,9 @@ packages:
       '@vue/compiler-ssr': 3.2.45
       '@vue/shared': 3.2.45
       vue: 3.2.45
-    dev: true
 
   /@vue/shared@3.2.45:
     resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
-    dev: true
 
   /@webassemblyjs/ast@1.11.1:
     resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@@ -6074,7 +6070,6 @@ packages:
 
   /csstype@2.6.21:
     resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
-    dev: true
 
   /csstype@3.1.1:
     resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
@@ -6979,7 +6974,6 @@ packages:
 
   /estree-walker@2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
-    dev: true
 
   /esutils@2.0.3:
     resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@@ -7445,6 +7439,22 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
+  /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
+    resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
+    peerDependencies:
+      focus-trap: ^7.0.0
+      vue: ^3.0.0
+    dependencies:
+      focus-trap: 7.2.0
+      vue: 3.2.45
+    dev: false
+
+  /focus-trap@7.2.0:
+    resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==}
+    dependencies:
+      tabbable: 6.1.1
+    dev: false
+
   /follow-redirects@1.15.2(debug@4.3.4):
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     engines: {node: '>=4.0'}
@@ -10350,7 +10360,6 @@ packages:
     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
     dependencies:
       sourcemap-codec: 1.4.8
-    dev: true
 
   /mailcheck@1.1.1:
     resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
@@ -13267,7 +13276,6 @@ packages:
   /sourcemap-codec@1.4.8:
     resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
     deprecated: Please use @jridgewell/sourcemap-codec instead
-    dev: true
 
   /sparkles@1.0.1:
     resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==}
@@ -13686,6 +13694,10 @@ packages:
     resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
     dev: true
 
+  /tabbable@6.1.1:
+    resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==}
+    dev: false
+
   /tapable@2.2.1:
     resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
     engines: {node: '>=6'}
@@ -14776,7 +14788,6 @@ packages:
       '@vue/runtime-dom': 3.2.45
       '@vue/server-renderer': 3.2.45(vue@3.2.45)
       '@vue/shared': 3.2.45
-    dev: true
 
   /vuedraggable@4.1.0(vue@3.2.45):
     resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}

From 10b3079658c7a9fdb9ed45b9d9b46bc83a0b6a76 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 19:28:25 -0400
Subject: [PATCH 19/48] Fix focusing inside of CW's

---
 packages/client/src/components/MkCwButton.vue | 15 ++++++++--
 packages/client/src/components/MkNote.vue     |  4 ++-
 .../src/components/MkSubNoteContent.vue       | 30 ++++++++++++++++---
 3 files changed, 42 insertions(+), 7 deletions(-)

diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 35af48874..01ab11351 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -1,5 +1,6 @@
 <template>
 	<button
+		ref="el"
 		class="_button"
 		:class="{ showLess: modelValue, fade: !modelValue }"
 		@click.stop="toggle"
@@ -12,7 +13,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from "vue";
+import { computed, ref } from "vue";
 import { length } from "stringz";
 import * as misskey from "calckey-js";
 import { concat } from "@/scripts/array";
@@ -27,6 +28,8 @@ const emit = defineEmits<{
 	(ev: "update:modelValue", v: boolean): void;
 }>();
 
+const el = ref<HTMLElement>(); 
+
 const label = computed(() => {
 	return concat([
 		props.note.text
@@ -43,6 +46,14 @@ const label = computed(() => {
 const toggle = () => {
 	emit("update:modelValue", !props.modelValue);
 };
+
+function focus() {
+	el.value.focus();
+}
+
+defineExpose({
+	focus
+});
 </script>
 
 <style lang="scss" scoped>
@@ -62,7 +73,7 @@ const toggle = () => {
 			}
 		}
 	}
-	&:hover > span {
+	&:hover > span, &:focus > span {
 		background: var(--cwFg) !important;
 		color: var(--cwBg) !important;
 	}
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index f49105e81..5d9c40d38 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -84,6 +84,7 @@
 						:detailedView="detailedView"
 						:parentId="appearNote.parentId"
 						@push="(e) => router.push(notePage(e))"
+						@focusfooter="footerEl.focus()"
 					></MkSubNoteContent>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -117,7 +118,7 @@
 						<MkTime :time="appearNote.createdAt" mode="absolute" />
 					</MkA>
 				</div>
-				<footer ref="el" class="footer" @click.stop>
+				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -278,6 +279,7 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
+const footerEl = ref<HTMLElement>(); 
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index a345b23f5..a20afc39b 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -35,7 +35,11 @@
 			class="content"
 			:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
 		>
-			<div class="body">
+			<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" />
+			<div 
+				class="body"
+				v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
+			>
 				<span v-if="note.deletedAt" style="opacity: 0.5"
 					>({{ i18n.ts.deleted }})</span
 				>
@@ -96,6 +100,11 @@
 						<XNoteSimple :note="note.renote" />
 					</div>
 				</template>
+				<div
+					v-if="!showContent"
+					tabindex="0"
+					v-on:focus="cwButton?.focus()"
+				></div>
 			</div>
 			<button
 				v-if="isLong && collapsed"
@@ -111,13 +120,13 @@
 			>
 				<span>{{ i18n.ts.showLess }}</span>
 			</button>
-			<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
+			<XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" />
 		</div>
 	</div>
 </template>
 
 <script lang="ts" setup>
-import {} from "vue";
+import { ref } from "vue"; 
 import * as misskey from "calckey-js";
 import * as mfm from "mfm-js";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
@@ -138,8 +147,10 @@ const props = defineProps<{
 
 const emit = defineEmits<{
 	(ev: "push", v): void;
+	(ev: "focusfooter"): void;
 }>();
 
+const cwButton = ref<HTMLElement>(); 
 const isLong =
 	!props.detailedView &&
 	props.note.cw == null &&
@@ -151,6 +162,13 @@ const urls = props.note.text
 	: null;
 
 let showContent = $ref(false);
+
+
+function focusFooter(ev) {
+	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
+		emit("focusfooter");
+	}
+}
 </script>
 
 <style lang="scss" scoped>
@@ -242,6 +260,9 @@ let showContent = $ref(false);
 				margin-top: -50px;
 				padding-top: 50px;
 				overflow: hidden;
+				user-select: none;
+				-webkit-user-select: none;
+				-moz-user-select: none;
 			}
 			&.collapsed > .body {
 				box-sizing: border-box;
@@ -271,6 +292,7 @@ let showContent = $ref(false);
 				bottom: 0;
 				left: 0;
 				width: 100%;
+				z-index: 2;
 				> span {
 					display: inline-block;
 					background: var(--panel);
@@ -279,7 +301,7 @@ let showContent = $ref(false);
 					border-radius: 999px;
 					box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 				}
-				&:hover {
+				&:hover, &:focus {
 					> span {
 						background: var(--panelHighlight);
 					}

From 14ec973c7088c5a7ff58fcb51270c00d4d4dbd84 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 19:57:19 -0400
Subject: [PATCH 20/48] add the focus trap thingies again

---
 .../client/src/components/MkEmojiPicker.vue   | 277 ++++++-------
 packages/client/src/components/MkMenu.vue     | 366 +++++++++---------
 packages/client/src/components/MkModal.vue    |  26 +-
 .../client/src/components/MkModalWindow.vue   |  93 ++---
 4 files changed, 380 insertions(+), 382 deletions(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index a22006951..6c18e92f0 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -1,157 +1,159 @@
 <template>
-	<div
-		class="omfetrab"
-		:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
-		:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
-	>
-		<input
-			ref="search"
-			v-model.trim="q"
-			class="search"
-			data-prevent-emoji-insert
-			:class="{ filled: q != null && q != '' }"
-			:placeholder="i18n.ts.search"
-			type="search"
-			@paste.stop="paste"
-			@keyup.enter="done()"
-		/>
-		<div ref="emojis" class="emojis">
-			<section class="result">
-				<div v-if="searchResultCustom.length > 0" class="body">
-					<button
-						v-for="emoji in searchResultCustom"
-						:key="emoji.id"
-						class="_button item"
-						:title="emoji.name"
-						tabindex="0"
-						@click="chosen(emoji, $event)"
-					>
-						<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
-						<img
-							class="emoji"
-							:src="
-								disableShowingAnimatedImages
-									? getStaticImageUrl(emoji.url)
-									: emoji.url
-							"
-						/>
-					</button>
-				</div>
-				<div v-if="searchResultUnicode.length > 0" class="body">
-					<button
-						v-for="emoji in searchResultUnicode"
-						:key="emoji.name"
-						class="_button item"
-						:title="emoji.name"
-						tabindex="0"
-						@click="chosen(emoji, $event)"
-					>
-						<MkEmoji class="emoji" :emoji="emoji.char" />
-					</button>
-				</div>
-			</section>
-
-			<div v-if="tab === 'index'" class="group index">
-				<section v-if="showPinned">
-					<div class="body">
+	<FocusTrap v-bind:active="isActive">
+		<div
+			class="omfetrab"
+			:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
+			:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
+		>
+			<input
+				ref="search"
+				v-model.trim="q"
+				class="search"
+				data-prevent-emoji-insert
+				:class="{ filled: q != null && q != '' }"
+				:placeholder="i18n.ts.search"
+				type="search"
+				@paste.stop="paste"
+				@keyup.enter="done()"
+			/>
+			<div ref="emojis" class="emojis">
+				<section class="result">
+					<div v-if="searchResultCustom.length > 0" class="body">
 						<button
-							v-for="emoji in pinned"
-							:key="emoji"
+							v-for="emoji in searchResultCustom"
+							:key="emoji.id"
 							class="_button item"
+							:title="emoji.name"
 							tabindex="0"
 							@click="chosen(emoji, $event)"
 						>
-							<MkEmoji
+							<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+							<img
 								class="emoji"
-								:emoji="emoji"
-								:normal="true"
+								:src="
+									disableShowingAnimatedImages
+										? getStaticImageUrl(emoji.url)
+										: emoji.url
+								"
 							/>
 						</button>
 					</div>
+					<div v-if="searchResultUnicode.length > 0" class="body">
+						<button
+							v-for="emoji in searchResultUnicode"
+							:key="emoji.name"
+							class="_button item"
+							:title="emoji.name"
+							tabindex="0"
+							@click="chosen(emoji, $event)"
+						>
+							<MkEmoji class="emoji" :emoji="emoji.char" />
+						</button>
+					</div>
 				</section>
 
-				<section>
-					<header class="_acrylic">
-						<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
-						{{ i18n.ts.recentUsed }}
-					</header>
-					<div class="body">
-						<button
-							v-for="emoji in recentlyUsedEmojis"
-							:key="emoji"
-							class="_button item"
-							@click="chosen(emoji, $event)"
-						>
-							<MkEmoji
-								class="emoji"
-								:emoji="emoji"
-								:normal="true"
-							/>
-						</button>
-					</div>
-				</section>
+				<div v-if="tab === 'index'" class="group index">
+					<section v-if="showPinned">
+						<div class="body">
+							<button
+								v-for="emoji in pinned"
+								:key="emoji"
+								class="_button item"
+								tabindex="0"
+								@click="chosen(emoji, $event)"
+							>
+								<MkEmoji
+									class="emoji"
+									:emoji="emoji"
+									:normal="true"
+								/>
+							</button>
+						</div>
+					</section>
+
+					<section>
+						<header class="_acrylic">
+							<i class="ph-alarm ph-bold ph-fw ph-lg"></i>
+							{{ i18n.ts.recentUsed }}
+						</header>
+						<div class="body">
+							<button
+								v-for="emoji in recentlyUsedEmojis"
+								:key="emoji"
+								class="_button item"
+								@click="chosen(emoji, $event)"
+							>
+								<MkEmoji
+									class="emoji"
+									:emoji="emoji"
+									:normal="true"
+								/>
+							</button>
+						</div>
+					</section>
+				</div>
+				<div v-once class="group">
+					<header>{{ i18n.ts.customEmojis }}</header>
+					<XSection
+						v-for="category in customEmojiCategories"
+						:key="'custom:' + category"
+						:initial-shown="false"
+						:emojis="
+							customEmojis
+								.filter((e) => e.category === category)
+								.map((e) => ':' + e.name + ':')
+						"
+						@chosen="chosen"
+						>{{ category || i18n.ts.other }}</XSection
+					>
+				</div>
+				<div v-once class="group">
+					<header>{{ i18n.ts.emoji }}</header>
+					<XSection
+						v-for="category in categories"
+						:key="category"
+						:emojis="
+							emojilist
+								.filter((e) => e.category === category)
+								.map((e) => e.char)
+						"
+						@chosen="chosen"
+						>{{ category }}</XSection
+					>
+				</div>
 			</div>
-			<div v-once class="group">
-				<header>{{ i18n.ts.customEmojis }}</header>
-				<XSection
-					v-for="category in customEmojiCategories"
-					:key="'custom:' + category"
-					:initial-shown="false"
-					:emojis="
-						customEmojis
-							.filter((e) => e.category === category)
-							.map((e) => ':' + e.name + ':')
-					"
-					@chosen="chosen"
-					>{{ category || i18n.ts.other }}</XSection
+			<div class="tabs">
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'index' }"
+					@click="tab = 'index'"
 				>
-			</div>
-			<div v-once class="group">
-				<header>{{ i18n.ts.emoji }}</header>
-				<XSection
-					v-for="category in categories"
-					:key="category"
-					:emojis="
-						emojilist
-							.filter((e) => e.category === category)
-							.map((e) => e.char)
-					"
-					@chosen="chosen"
-					>{{ category }}</XSection
+					<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'custom' }"
+					@click="tab = 'custom'"
 				>
+					<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'unicode' }"
+					@click="tab = 'unicode'"
+				>
+					<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
+				<button
+					class="_button tab"
+					:class="{ active: tab === 'tags' }"
+					@click="tab = 'tags'"
+				>
+					<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
+				</button>
 			</div>
 		</div>
-		<div class="tabs">
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'index' }"
-				@click="tab = 'index'"
-			>
-				<i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'custom' }"
-				@click="tab = 'custom'"
-			>
-				<i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'unicode' }"
-				@click="tab = 'unicode'"
-			>
-				<i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-			<button
-				class="_button tab"
-				:class="{ active: tab === 'tags' }"
-				@click="tab = 'tags'"
-			>
-				<i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i>
-			</button>
-		</div>
-	</div>
+	</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -171,6 +173,7 @@ import { deviceKind } from "@/scripts/device-kind";
 import { emojiCategories, instance } from "@/instance";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
+import { FocusTrap } from 'focus-trap-vue';
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 45b69a23f..af2613b8c 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,191 +1,185 @@
 <template>
-	<div>
-		<div
-			ref="itemsEl"
-			v-hotkey="keymap"
-			class="rrevdjwt _popup _shadow"
-			:class="{ center: align === 'center', asDrawer }"
-			:style="{
-				width: width && !asDrawer ? width + 'px' : '',
-				maxHeight: maxHeight ? maxHeight + 'px' : '',
-			}"
-			@contextmenu.self="(e) => e.preventDefault()"
-		>
-			<template v-for="(item, i) in items2">
-				<div v-if="item === null" class="divider"></div>
-				<span v-else-if="item.type === 'label'" class="label item">
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-				</span>
-				<span
-					v-else-if="item.type === 'pending'"
-					:tabindex="i"
-					class="pending item"
-				>
-					<span><MkEllipsis /></span>
-				</span>
-				<MkA
-					v-else-if="item.type === 'link'"
-					:to="item.to"
-					:tabindex="i"
-					class="_button item"
-					@click.passive="close(true)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
+	<FocusTrap v-bind:active="isActive">
+	<div tabindex="-1" v-focus>
+			<div
+				ref="itemsEl"
+				class="rrevdjwt _popup _shadow"
+				:class="{ center: align === 'center', asDrawer }"
+				:style="{
+					width: width && !asDrawer ? width + 'px' : '',
+					maxHeight: maxHeight ? maxHeight + 'px' : '',
+				}"
+				@contextmenu.self="(e) => e.preventDefault()"
+			>
+				<template v-for="(item, i) in items2">
+					<div v-if="item === null" class="divider"></div>
+					<span v-else-if="item.type === 'label'" class="label item">
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
 					</span>
-					<MkAvatar
-						v-if="item.avatar"
-						:user="item.avatar"
-						class="avatar"
-					/>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</MkA>
-				<a
-					v-else-if="item.type === 'a'"
-					:href="item.href"
-					:target="item.target"
-					:download="item.download"
-					:tabindex="i"
-					class="_button item"
-					@click="close(true)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</a>
-				<button
-					v-else-if="item.type === 'user' && !items.hidden"
-					:tabindex="i"
-					class="_button item"
-					:class="{ active: item.active }"
-					:disabled="item.active"
-					@click="clicked(item.action, $event)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<MkAvatar :user="item.user" class="avatar" /><MkUserName
-						:user="item.user"
-					/>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</button>
-				<span
-					v-else-if="item.type === 'switch'"
-					:tabindex="i"
-					class="item"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<FormSwitch
-						v-model="item.ref"
-						:disabled="item.disabled"
-						class="form-switch"
-						:style="item.textStyle || ''"
-						>{{ item.text }}</FormSwitch
+					<span
+						v-else-if="item.type === 'pending'"
+						class="pending item"
 					>
+						<span><MkEllipsis /></span>
+					</span>
+					<MkA
+						v-else-if="item.type === 'link'"
+						:to="item.to"
+						class="_button item"
+						@click.passive="close(true)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<MkAvatar
+							v-if="item.avatar"
+							:user="item.avatar"
+							class="avatar"
+						/>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</MkA>
+					<a
+						v-else-if="item.type === 'a'"
+						:href="item.href"
+						:target="item.target"
+						:download="item.download"
+						class="_button item"
+						@click="close(true)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</a>
+					<button
+						v-else-if="item.type === 'user' && !items.hidden"
+						class="_button item"
+						:class="{ active: item.active }"
+						:disabled="item.active"
+						@click="clicked(item.action, $event)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<MkAvatar :user="item.user" class="avatar" /><MkUserName
+							:user="item.user"
+						/>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</button>
+					<span
+						v-else-if="item.type === 'switch'"
+						class="item"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<FormSwitch
+							v-model="item.ref"
+							:disabled="item.disabled"
+							class="form-switch"
+							:style="item.textStyle || ''"
+							>{{ item.text }}</FormSwitch
+						>
+					</span>
+					<button
+						v-else-if="item.type === 'parent'"
+						class="_button item parent"
+						:class="{ childShowing: childShowingItem === item }"
+						@mouseenter="showChildren(item, $event)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span class="caret"
+							><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
+						></span>
+					</button>
+					<button
+						v-else-if="!item.hidden"
+						class="_button item"
+						:class="{ danger: item.danger, active: item.active }"
+						:disabled="item.active"
+						@click="clicked(item.action, $event)"
+						@mouseenter.passive="onItemMouseEnter(item)"
+						@mouseleave.passive="onItemMouseLeave(item)"
+					>
+						<i
+							v-if="item.icon"
+							class="ph-fw ph-lg"
+							:class="item.icon"
+						></i>
+						<span v-else-if="item.icons">
+							<i
+								v-for="icon in item.icons"
+								class="ph-fw ph-lg"
+								:class="icon"
+							></i>
+						</span>
+						<MkAvatar
+							v-if="item.avatar"
+							:user="item.avatar"
+							class="avatar"
+						/>
+						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span v-if="item.indicate" class="indicator"
+							><i class="ph-circle ph-fill"></i
+						></span>
+					</button>
+				</template>
+				<span v-if="items2.length === 0" class="none item">
+					<span>{{ i18n.ts.none }}</span>
 				</span>
-				<button
-					v-else-if="item.type === 'parent'"
-					:tabindex="i"
-					class="_button item parent"
-					:class="{ childShowing: childShowingItem === item }"
-					@mouseenter="showChildren(item, $event)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span class="caret"
-						><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
-					></span>
-				</button>
-				<button
-					v-else-if="!item.hidden"
-					:tabindex="i"
-					class="_button item"
-					:class="{ danger: item.danger, active: item.active }"
-					:disabled="item.active"
-					@click="clicked(item.action, $event)"
-					@mouseenter.passive="onItemMouseEnter(item)"
-					@mouseleave.passive="onItemMouseLeave(item)"
-				>
-					<i
-						v-if="item.icon"
-						class="ph-fw ph-lg"
-						:class="item.icon"
-					></i>
-					<span v-else-if="item.icons">
-						<i
-							v-for="icon in item.icons"
-							class="ph-fw ph-lg"
-							:class="icon"
-						></i>
-					</span>
-					<MkAvatar
-						v-if="item.avatar"
-						:user="item.avatar"
-						class="avatar"
-					/>
-					<span :style="item.textStyle || ''">{{ item.text }}</span>
-					<span v-if="item.indicate" class="indicator"
-						><i class="ph-circle ph-fill"></i
-					></span>
-				</button>
-			</template>
-			<span v-if="items2.length === 0" class="none item">
-				<span>{{ i18n.ts.none }}</span>
-			</span>
+			</div>
+			<div v-if="childMenu" class="child">
+				<XChild
+					ref="child"
+					:items="childMenu"
+					:target-element="childTarget"
+					:root-element="itemsEl"
+					showing
+					@actioned="childActioned"
+				/>
+			</div>
 		</div>
-		<div v-if="childMenu" class="child">
-			<XChild
-				ref="child"
-				:items="childMenu"
-				:target-element="childTarget"
-				:root-element="itemsEl"
-				showing
-				@actioned="childActioned"
-			/>
-		</div>
-	</div>
+	</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -229,12 +223,6 @@ let items2: InnerMenuItem[] = $ref([]);
 
 let child = $ref<InstanceType<typeof XChild>>();
 
-let keymap = computed(() => ({
-	"up|k|shift+tab": focusUp,
-	"down|j|tab": focusDown,
-	esc: close,
-}));
-
 let childShowingItem = $ref<MenuItem | null>();
 
 watch(
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 2c5498583..ff001f869 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -35,6 +35,8 @@
 					: 'none',
 				'--transformOrigin': transformOrigin,
 			}"
+			tabindex="-1"
+			v-focus
 		>
 			<div
 				class="_modalBg data-cy-bg"
@@ -50,17 +52,19 @@
 				@mousedown="onBgClick"
 				@contextmenu.prevent.stop="() => {}"
 			></div>
-			<div
-				ref="content"
-				:class="[
-					$style.content,
-					{ [$style.fixed]: fixed, top: type === 'dialog:top' },
-				]"
-				:style="{ zIndex }"
-				@click.self="onBgClick"
-			>
-				<slot :max-height="maxHeight" :type="type"></slot>
-			</div>
+			<focus-trap v-model:active="isActive">
+				<div
+					ref="content"
+					:class="[
+						$style.content,
+						{ [$style.fixed]: fixed, top: type === 'dialog:top' },
+					]"
+					:style="{ zIndex }"
+					@click.self="onBgClick"
+				>
+					<slot :max-height="maxHeight" :type="type"></slot>
+				</div>
+			</focus-trap>
 		</div>
 	</Transition>
 </template>
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index b425c4625..48028422e 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -3,54 +3,57 @@
 		ref="modal"
 		:prefer-type="'dialog'"
 		@click="onBgClick"
+		@keyup.esc="$emit('close')"
 		@closed="$emit('closed')"
 	>
-		<div
-			ref="rootEl"
-			class="ebkgoccj"
-			:style="{
-				width: `${width}px`,
-				height: scroll
-					? height
-						? `${height}px`
-						: null
-					: height
-					? `min(${height}px, 100%)`
-					: '100%',
-			}"
-			@keydown="onKeydown"
-		>
-			<div ref="headerEl" class="header">
-				<button
-					v-if="withOkButton"
-					class="_button"
-					@click="$emit('close')"
-				>
-					<i class="ph-x ph-bold ph-lg"></i>
-				</button>
-				<span class="title">
-					<slot name="header"></slot>
-				</span>
-				<button
-					v-if="!withOkButton"
-					class="_button"
-					@click="$emit('close')"
-				>
-					<i class="ph-x ph-bold ph-lg"></i>
-				</button>
-				<button
-					v-if="withOkButton"
-					class="_button"
-					:disabled="okButtonDisabled"
-					@click="$emit('ok')"
-				>
-					<i class="ph-check ph-bold ph-lg"></i>
-				</button>
+		<focus-trap v-model:active="isActive">
+			<div
+				ref="rootEl"
+				class="ebkgoccj"
+				:style="{
+					width: `${width}px`,
+					height: scroll
+						? height
+							? `${height}px`
+							: null
+						: height
+						? `min(${height}px, 100%)`
+						: '100%',
+				}"
+				@keydown="onKeydown"
+			>
+				<div ref="headerEl" class="header">
+					<button
+						v-if="withOkButton"
+						class="_button"
+						@click="$emit('close')"
+					>
+						<i class="ph-x ph-bold ph-lg"></i>
+					</button>
+					<span class="title">
+						<slot name="header"></slot>
+					</span>
+					<button
+						v-if="!withOkButton"
+						class="_button"
+						@click="$emit('close')"
+					>
+						<i class="ph-x ph-bold ph-lg"></i>
+					</button>
+					<button
+						v-if="withOkButton"
+						class="_button"
+						:disabled="okButtonDisabled"
+						@click="$emit('ok')"
+					>
+						<i class="ph-check ph-bold ph-lg"></i>
+					</button>
+				</div>
+				<div class="body">
+					<slot :width="bodyWidth" :height="bodyHeight"></slot>
+				</div>
 			</div>
-			<div class="body">
-				<slot :width="bodyWidth" :height="bodyHeight"></slot>
-			</div>
-		</div>
+		</focus-trap>
 	</MkModal>
 </template>
 

From ec372782119f82f13785bcd53a8310a41c6b7199 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 20:26:10 -0400
Subject: [PATCH 21/48] fixes

---
 packages/client/src/components/MkMenu.vue  |  2 +-
 packages/client/src/components/MkModal.vue | 67 +++++++++++-----------
 2 files changed, 35 insertions(+), 34 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index af2613b8c..f22f0f9ca 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,6 +1,6 @@
 <template>
 	<FocusTrap v-bind:active="isActive">
-	<div tabindex="-1" v-focus>
+		<div tabindex="-1" v-focus>
 			<div
 				ref="itemsEl"
 				class="rrevdjwt _popup _shadow"
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index ff001f869..c47cd47b3 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -14,45 +14,46 @@
 		:duration="transitionDuration"
 		appear
 		@after-leave="emit('closed')"
+		@keyup.esc="emit('click')"
 		@enter="emit('opening')"
 		@after-enter="onOpened"
 	>
-		<div
-			v-show="manualShowing != null ? manualShowing : showing"
-			v-hotkey.global="keymap"
-			:class="[
-				$style.root,
-				{
-					[$style.drawer]: type === 'drawer',
-					[$style.dialog]: type === 'dialog' || type === 'dialog:top',
-					[$style.popup]: type === 'popup',
-				},
-			]"
-			:style="{
-				zIndex,
-				pointerEvents: (manualShowing != null ? manualShowing : showing)
-					? 'auto'
-					: 'none',
-				'--transformOrigin': transformOrigin,
-			}"
-			tabindex="-1"
-			v-focus
-		>
+		<focus-trap v-model:active="isActive">
 			<div
-				class="_modalBg data-cy-bg"
+				v-show="manualShowing != null ? manualShowing : showing"
+				v-hotkey.global="keymap"
 				:class="[
-					$style.bg,
+					$style.root,
 					{
-						[$style.bgTransparent]: isEnableBgTransparent,
-						'data-cy-transparent': isEnableBgTransparent,
+						[$style.drawer]: type === 'drawer',
+						[$style.dialog]: type === 'dialog' || type === 'dialog:top',
+						[$style.popup]: type === 'popup',
 					},
 				]"
-				:style="{ zIndex }"
-				@click="onBgClick"
-				@mousedown="onBgClick"
-				@contextmenu.prevent.stop="() => {}"
-			></div>
-			<focus-trap v-model:active="isActive">
+				:style="{
+					zIndex,
+					pointerEvents: (manualShowing != null ? manualShowing : showing)
+						? 'auto'
+						: 'none',
+					'--transformOrigin': transformOrigin,
+				}"
+				tabindex="-1"
+				v-focus
+			>
+				<div
+					class="_modalBg data-cy-bg"
+					:class="[
+						$style.bg,
+						{
+							[$style.bgTransparent]: isEnableBgTransparent,
+							'data-cy-transparent': isEnableBgTransparent,
+						},
+					]"
+					:style="{ zIndex }"
+					@click="onBgClick"
+					@mousedown="onBgClick"
+					@contextmenu.prevent.stop="() => {}"
+				></div>
 				<div
 					ref="content"
 					:class="[
@@ -64,8 +65,8 @@
 				>
 					<slot :max-height="maxHeight" :type="type"></slot>
 				</div>
-			</focus-trap>
-		</div>
+			</div>
+		</focus-trap>
 	</Transition>
 </template>
 

From b8ca00c593ce2efdaf0d4b94407d93a1d5df7f15 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 20:47:57 -0400
Subject: [PATCH 22/48] fixes?

---
 packages/client/src/components/MkEmojiPicker.vue | 1 +
 packages/client/src/components/MkMenu.child.vue  | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index 6c18e92f0..88d207bab 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -4,6 +4,7 @@
 			class="omfetrab"
 			:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
 			:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
+			tabindex="-1"
 		>
 			<input
 				ref="search"
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index 52131c5ff..d5e9d774f 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,6 +1,6 @@
 <template>
 	<FocusTrap v-bind:active="isActive">
-		<div ref="el" class="sfhdhdhr">
+		<div ref="el" class="sfhdhdhr" tabindex="-1">
 			<MkMenu
 				ref="menu"
 				:items="items"

From efcd0dd71bbddbf79943fedfd81b54d116336130 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 20:52:09 -0400
Subject: [PATCH 23/48] focus last element on bg click

---
 packages/client/src/components/MkModal.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index c47cd47b3..d1a96b4b7 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -188,6 +188,7 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
 function onBgClick() {
 	if (contentClicking) return;
 	emit("click");
+	focusedElement.focus();
 }
 
 if (type === "drawer") {

From 7c698d1697c9edf542773af1b40076184c8c448b Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 21:18:00 -0400
Subject: [PATCH 24/48] consistency + fix

---
 packages/client/src/components/MkModal.vue          | 6 +++---
 packages/client/src/components/MkModalWindow.vue    | 5 +++--
 packages/client/src/components/MkSubNoteContent.vue | 2 +-
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index d1a96b4b7..c41fb50c0 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -18,7 +18,7 @@
 		@enter="emit('opening')"
 		@after-enter="onOpened"
 	>
-		<focus-trap v-model:active="isActive">
+		<FocusTrap v-model:active="isActive">
 			<div
 				v-show="manualShowing != null ? manualShowing : showing"
 				v-hotkey.global="keymap"
@@ -66,7 +66,7 @@
 					<slot :max-height="maxHeight" :type="type"></slot>
 				</div>
 			</div>
-		</focus-trap>
+		</FocusTrap>
 	</Transition>
 </template>
 
@@ -187,8 +187,8 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
 
 function onBgClick() {
 	if (contentClicking) return;
-	emit("click");
 	focusedElement.focus();
+	emit("click");
 }
 
 if (type === "drawer") {
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 48028422e..017bfae8c 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -6,7 +6,7 @@
 		@keyup.esc="$emit('close')"
 		@closed="$emit('closed')"
 	>
-		<focus-trap v-model:active="isActive">
+		<FocusTrap v-model:active="isActive">
 			<div
 				ref="rootEl"
 				class="ebkgoccj"
@@ -21,6 +21,7 @@
 						: '100%',
 				}"
 				@keydown="onKeydown"
+				tabindex="-1"
 			>
 				<div ref="headerEl" class="header">
 					<button
@@ -53,7 +54,7 @@
 					<slot :width="bodyWidth" :height="bodyHeight"></slot>
 				</div>
 			</div>
-		</focus-trap>
+		</FocusTrap>
 	</MkModal>
 </template>
 
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index a20afc39b..761334e1d 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -101,7 +101,7 @@
 					</div>
 				</template>
 				<div
-					v-if="!showContent"
+					v-if="note.cw && !showContent"
 					tabindex="0"
 					v-on:focus="cwButton?.focus()"
 				></div>

From 9e99df6e82bbd7ce5c9e22966d2b32eb245e04f1 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 21:22:29 -0400
Subject: [PATCH 25/48] fix subnote

---
 packages/client/src/components/MkNoteSub.vue | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index f5e70891f..a0b70ff1f 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -26,6 +26,7 @@
 						:note="note"
 						:parentId="appearNote.parentId"
 						:conversation="conversation"
+						@focusfooter="footerEl.focus()"
 					/>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -46,7 +47,7 @@
 						</div>
 					</div>
 				</div>
-				<footer class="footer" @click.stop>
+				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -212,6 +213,7 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
+const footerEl = ref<HTMLElement>();
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();

From d8a6ce64a36426961481293a56cb2e1daf339478 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 21:39:48 -0400
Subject: [PATCH 26/48] Fix focusing avatars in weird places

---
 packages/client/src/components/MkMenu.vue               | 4 +++-
 packages/client/src/components/MkUserSelectDialog.vue   | 2 ++
 packages/client/src/components/MkUsersTooltip.vue       | 2 +-
 packages/client/src/components/global/MkPageHeader.vue  | 2 ++
 packages/client/src/pages/admin/overview.moderators.vue | 2 +-
 packages/client/src/pages/follow-requests.vue           | 1 +
 packages/client/src/ui/_common_/navbar-for-mobile.vue   | 1 +
 packages/client/src/ui/_common_/navbar.vue              | 1 +
 packages/client/src/ui/classic.header.vue               | 1 +
 packages/client/src/ui/classic.sidebar.vue              | 1 +
 10 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index f22f0f9ca..0b5fb7455 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -46,6 +46,7 @@
 							v-if="item.avatar"
 							:user="item.avatar"
 							class="avatar"
+							disableLink
 						/>
 						<span :style="item.textStyle || ''">{{ item.text }}</span>
 						<span v-if="item.indicate" class="indicator"
@@ -88,7 +89,7 @@
 						@mouseenter.passive="onItemMouseEnter(item)"
 						@mouseleave.passive="onItemMouseLeave(item)"
 					>
-						<MkAvatar :user="item.user" class="avatar" /><MkUserName
+						<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
 							:user="item.user"
 						/>
 						<span v-if="item.indicate" class="indicator"
@@ -157,6 +158,7 @@
 							v-if="item.avatar"
 							:user="item.avatar"
 							class="avatar"
+							disableLink
 						/>
 						<span :style="item.textStyle || ''">{{ item.text }}</span>
 						<span v-if="item.indicate" class="indicator"
diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue
index 506f48bd4..14553ca46 100644
--- a/packages/client/src/components/MkUserSelectDialog.vue
+++ b/packages/client/src/components/MkUserSelectDialog.vue
@@ -46,6 +46,7 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
+							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
@@ -73,6 +74,7 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
+							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue
index 972864d1f..78a4f90f2 100644
--- a/packages/client/src/components/MkUsersTooltip.vue
+++ b/packages/client/src/components/MkUsersTooltip.vue
@@ -7,7 +7,7 @@
 	>
 		<div class="beaffaef">
 			<div v-for="u in users" :key="u.id" class="user">
-				<MkAvatar class="avatar" :user="u" />
+				<MkAvatar class="avatar" :user="u" disableLink />
 				<MkUserName class="name" :user="u" :nowrap="true" />
 			</div>
 			<div v-if="users.length < count" class="omitted">
diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue
index ad1d80ca6..c78ef0c10 100644
--- a/packages/client/src/components/global/MkPageHeader.vue
+++ b/packages/client/src/components/global/MkPageHeader.vue
@@ -19,6 +19,7 @@
 				class="avatar"
 				:user="$i"
 				:disable-preview="true"
+				disableLink
 			/>
 		</div>
 		<template v-if="metadata">
@@ -33,6 +34,7 @@
 					:user="metadata.avatar"
 					:disable-preview="true"
 					:show-indicator="true"
+					disableLink
 				/>
 				<i
 					v-else-if="metadata.icon && !narrow"
diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue
index 6184cfb10..db953b890 100644
--- a/packages/client/src/pages/admin/overview.moderators.vue
+++ b/packages/client/src/pages/admin/overview.moderators.vue
@@ -12,7 +12,7 @@
 					class="user"
 					:to="`/user-info/${user.id}`"
 				>
-					<MkAvatar :user="user" class="avatar" indicator />
+					<MkAvatar :user="user" class="avatar" indicator disableLink />
 				</MkA>
 			</div>
 		</Transition>
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index 2aac52163..35279495b 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -23,6 +23,7 @@
 								class="avatar"
 								:user="req.follower"
 								:show-indicator="true"
+								disableLink
 							/>
 							<div class="body">
 								<div class="name">
diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue
index 43c91d147..39abb7c26 100644
--- a/packages/client/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue
@@ -18,6 +18,7 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
+						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 1c69067e1..4fb27a071 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -18,6 +18,7 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
+						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
index 5c3e6b702..99a0ab098 100644
--- a/packages/client/src/ui/classic.header.vue
+++ b/packages/client/src/ui/classic.header.vue
@@ -83,6 +83,7 @@
 					<MkAvatar :user="$i" class="avatar" /><MkAcct
 						class="acct"
 						:user="$i"
+						disableLink
 					/>
 				</button>
 				<div class="post" @click="post">
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index b70a3c984..33aa62ed7 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -8,6 +8,7 @@
 			<MkAvatar :user="$i" class="avatar" /><MkAcct
 				class="text"
 				:user="$i"
+				disableLink
 			/>
 		</button>
 		<div class="post" data-cy-open-post-form @click="post">

From e52b3cef77fcbe21ed531b8aa343a5e150cd4894 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 21:49:41 -0400
Subject: [PATCH 27/48] Make accounts in account management page focusable

---
 packages/client/src/pages/settings/accounts.vue | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index ec2cd2477..3010354b6 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -6,14 +6,14 @@
 				{{ i18n.ts.addAccount }}</FormButton
 			>
 
-			<div
+			<button
 				v-for="account in accounts"
 				:key="account.id"
 				class="_panel _button lcjjdxlm"
 				@click="menu(account, $event)"
 			>
 				<div class="avatar">
-					<MkAvatar :user="account" class="avatar" />
+					<MkAvatar :user="account" class="avatar" disableLink />
 				</div>
 				<div class="body">
 					<div class="name">
@@ -23,7 +23,7 @@
 						<MkAcct :user="account" />
 					</div>
 				</div>
-			</div>
+			</button>
 		</FormSuspense>
 	</div>
 </template>
@@ -158,6 +158,8 @@ definePageMetadata({
 .lcjjdxlm {
 	display: flex;
 	padding: 16px;
+	width: 100%;
+	text-align: unset;
 
 	> .avatar {
 		display: block;

From 739e0331a84a2f14e881d3c881567e2c009144fd Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:04:35 -0400
Subject: [PATCH 28/48] make folder dropdown focusable

---
 packages/client/src/components/form/folder.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue
index 8868f5784..a2fde5341 100644
--- a/packages/client/src/components/form/folder.vue
+++ b/packages/client/src/components/form/folder.vue
@@ -1,6 +1,6 @@
 <template>
 	<div class="dwzlatin" :class="{ opened }">
-		<div class="header _button" @click="toggle">
+		<button class="header _button" @click="toggle">
 			<span class="icon"><slot name="icon"></slot></span>
 			<span class="text"><slot name="label"></slot></span>
 			<span class="right">
@@ -8,7 +8,7 @@
 				<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
 				<i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
 			</span>
-		</div>
+		</button>
 		<KeepAlive>
 			<div v-if="openedAtLeastOnce" v-show="opened" class="body">
 				<MkSpacer :margin-min="14" :margin-max="22">

From d096d7ea9a48d434e1ffc249d759d14db8a4b01c Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:17:39 -0400
Subject: [PATCH 29/48] focus to media

---
 packages/client/src/components/MkDriveFileThumbnail.vue | 7 +++++--
 packages/client/src/components/MkMediaImage.vue         | 4 ++++
 packages/client/src/components/MkPostFormAttaches.vue   | 1 -
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/MkDriveFileThumbnail.vue b/packages/client/src/components/MkDriveFileThumbnail.vue
index 39150c10c..48b542817 100644
--- a/packages/client/src/components/MkDriveFileThumbnail.vue
+++ b/packages/client/src/components/MkDriveFileThumbnail.vue
@@ -1,5 +1,5 @@
 <template>
-	<div ref="thumbnail" class="zdjebgpv">
+	<button ref="thumbnail" class="zdjebgpv">
 		<ImgWithBlurhash
 			v-if="isThumbnailAvailable"
 			:hash="file.blurhash"
@@ -36,7 +36,7 @@
 			v-if="isThumbnailAvailable && is === 'video'"
 			class="ph-file-video ph-bold ph-lg icon-sub"
 		></i>
-	</div>
+	</button>
 </template>
 
 <script lang="ts" setup>
@@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => {
 	background: var(--panel);
 	border-radius: 8px;
 	overflow: clip;
+	border: 0;
+	padding: 0;
+	cursor: pointer;
 
 	> .icon-sub {
 		position: absolute;
diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue
index 882908040..055a9e9b1 100644
--- a/packages/client/src/components/MkMediaImage.vue
+++ b/packages/client/src/components/MkMediaImage.vue
@@ -138,6 +138,10 @@ watch(
 		background-position: center;
 		background-size: contain;
 		background-repeat: no-repeat;
+		&:focus {
+			border: 2px solid var(--accent);
+			box-sizing: border-box;
+		}
 
 		> .gif {
 			background-color: var(--fg);
diff --git a/packages/client/src/components/MkPostFormAttaches.vue b/packages/client/src/components/MkPostFormAttaches.vue
index ad2155155..29d5dc79c 100644
--- a/packages/client/src/components/MkPostFormAttaches.vue
+++ b/packages/client/src/components/MkPostFormAttaches.vue
@@ -198,7 +198,6 @@ export default defineComponent({
 			height: 64px;
 			margin-right: 4px;
 			border-radius: 4px;
-			overflow: hidden;
 			cursor: move;
 
 			&:hover > .remove {

From d21de526ed51347e86aa9e5cae967d0219c4c87a Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:19:02 -0400
Subject: [PATCH 30/48] a

---
 packages/client/src/components/MkMediaImage.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue
index 055a9e9b1..3cfb0f465 100644
--- a/packages/client/src/components/MkMediaImage.vue
+++ b/packages/client/src/components/MkMediaImage.vue
@@ -138,9 +138,9 @@ watch(
 		background-position: center;
 		background-size: contain;
 		background-repeat: no-repeat;
-		&:focus {
+		box-sizing: border-box;
+		&:focus-visible {
 			border: 2px solid var(--accent);
-			box-sizing: border-box;
 		}
 
 		> .gif {

From bd31d5d0af88a9d973e3edb103a4f3de51aca12a Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:24:40 -0400
Subject: [PATCH 31/48] fix

---
 packages/client/src/components/MkMenu.child.vue | 6 +-----
 packages/client/src/components/MkMenu.vue       | 1 +
 2 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index d5e9d774f..e5ca9e4ee 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,6 +1,5 @@
 <template>
-	<FocusTrap v-bind:active="isActive">
-		<div ref="el" class="sfhdhdhr" tabindex="-1">
+	<div ref="el" class="sfhdhdhr" tabindex="-1">
 			<MkMenu
 				ref="menu"
 				:items="items"
@@ -10,7 +9,6 @@
 				@close="onChildClosed"
 			/>
 		</div>
-	</FocusTrap>
 </template>
 
 <script lang="ts" setup>
@@ -25,8 +23,6 @@ import {
 } from "vue";
 import MkMenu from "./MkMenu.vue";
 import { MenuItem } from "@/types/menu";
-import { FocusTrap } from 'focus-trap-vue';
-import * as os from "@/os";
 
 const props = defineProps<{
 	items: MenuItem[];
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 0b5fb7455..c71e3ac58 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -115,6 +115,7 @@
 						class="_button item parent"
 						:class="{ childShowing: childShowingItem === item }"
 						@mouseenter="showChildren(item, $event)"
+						@click="showChildren(item, $event)"
 					>
 						<i
 							v-if="item.icon"

From 3647f78747c94fc56e9a8c81c5fe5449edcd6d82 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:40:23 -0400
Subject: [PATCH 32/48] sidebar focus

---
 packages/client/src/ui/_common_/navbar.vue | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 4fb27a071..20c177f37 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -335,6 +335,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -428,7 +429,8 @@ function more(ev: MouseEvent) {
 						text-overflow: ellipsis;
 					}
 
-					&:hover {
+					&:hover,
+					&:focus-within {
 						text-decoration: none;
 						color: var(--navHoverFg);
 						transition: all 0.4s ease;
@@ -530,6 +532,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -615,6 +618,7 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
+					&:focus-within,
 					&.active {
 						text-decoration: none;
 						color: var(--accent);

From a66b334c3f055b64f5801f7ebdd5960c7444c6d6 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 22:50:32 -0400
Subject: [PATCH 33/48] widgets

---
 packages/client/src/components/MkWidgets.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue
index 07e845032..d48fc5383 100644
--- a/packages/client/src/components/MkWidgets.vue
+++ b/packages/client/src/components/MkWidgets.vue
@@ -1,7 +1,7 @@
 <template>
 	<div class="vjoppmmu">
 		<template v-if="edit">
-			<header>
+			<header tabindex="-1" v-focus>
 				<MkSelect
 					v-model="widgetAdderSelected"
 					style="margin-bottom: var(--margin)"

From 205c634d41f23deb2f57a18090d5f9ebe10706b9 Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 23:18:01 -0400
Subject: [PATCH 34/48] classic view fixes

---
 packages/client/src/components/MkModalPageWindow.vue | 1 +
 packages/client/src/ui/classic.sidebar.vue           | 5 +++--
 packages/client/src/ui/classic.vue                   | 2 ++
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue
index 361128464..bf4d8d0bc 100644
--- a/packages/client/src/components/MkModalPageWindow.vue
+++ b/packages/client/src/components/MkModalPageWindow.vue
@@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) {
 	flex-direction: column;
 	contain: content;
 	border-radius: var(--radius);
+	margin: auto;
 
 	--root-margin: 24px;
 
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index 33aa62ed7..fa72c5765 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -5,10 +5,9 @@
 			class="item _button account"
 			@click="openAccountMenu"
 		>
-			<MkAvatar :user="$i" class="avatar" /><MkAcct
+			<MkAvatar :user="$i" class="avatar" disableLink /><MkAcct
 				class="text"
 				:user="$i"
-				disableLink
 			/>
 		</button>
 		<div class="post" data-cy-open-post-form @click="post">
@@ -300,6 +299,7 @@ function openInstanceMenu(ev: MouseEvent) {
 				width: 46px;
 				height: 46px;
 				padding: 0;
+				margin-inline: 0 !important;
 			}
 		}
 
@@ -373,6 +373,7 @@ function openInstanceMenu(ev: MouseEvent) {
 
 		> i {
 			width: 32px;
+			justify-content: center;
 		}
 
 		> i,
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index a721ffd0b..266effd9a 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -227,6 +227,8 @@ onMounted(() => {
 }
 
 .gbhvwtnk {
+	display: flex;
+	justify-content: center;
 	$ui-font-size: 1em;
 	$widgets-hide-threshold: 1200px;
 

From ba21f5446956279367aa22daabb37e5e5c2da00c Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 23:26:19 -0400
Subject: [PATCH 35/48] outline fixes

---
 packages/client/src/components/MkModal.vue           | 1 +
 packages/client/src/components/global/RouterView.vue | 1 +
 2 files changed, 2 insertions(+)

diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index c41fb50c0..12e79f428 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -490,6 +490,7 @@ defineExpose({
 }
 
 .root {
+	outline: none;
 	&.dialog {
 		> .content {
 			position: fixed;
diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index edb3fedaa..437b7c53e 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -7,6 +7,7 @@
 				v-bind="Object.fromEntries(currentPageProps)"
 				tabindex="-1"
 				v-focus
+				style="outline: none;"
 			/>
 
 			<template #fallback>

From cd4130e22b39cbdf927c27093166eba11d85fc7b Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Fri, 28 Apr 2023 23:47:13 -0400
Subject: [PATCH 36/48] a

---
 packages/client/src/components/MkNotePreview.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue
index 9d388e71b..6fdd79dc6 100644
--- a/packages/client/src/components/MkNotePreview.vue
+++ b/packages/client/src/components/MkNotePreview.vue
@@ -1,6 +1,6 @@
 <template>
 	<div v-size="{ min: [350, 500] }" class="fefdfafb">
-		<MkAvatar class="avatar" :user="$i" />
+		<MkAvatar class="avatar" :user="$i" disableLink />
 		<div class="main">
 			<div class="header">
 				<MkUserName :user="$i" />

From d1cdd58bc1ee5e1a31103f73f8597a61e48828fa Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sat, 29 Apr 2023 09:56:28 -0400
Subject: [PATCH 37/48] a

---
 packages/client/src/components/MkCwButton.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 3038f97f7..1f6340510 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -84,6 +84,7 @@ defineExpose({
 		bottom: 0;
 		left: 0;
 		width: 100%;
+		z-index: 2;
 		> span {
 			display: inline-block;
 			background: var(--panel);
@@ -92,7 +93,7 @@ defineExpose({
 			border-radius: 999px;
 			box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 		}
-		&:hover {
+		&:hover, &:focus {
 			> span {
 				background: var(--panelHighlight);
 			}

From 426f47fdad70b24b00c35eaeeb6b455ce0084283 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 30 Apr 2023 23:18:39 +0900
Subject: [PATCH 38/48] add blockMath

---
 packages/client/src/pages/mfm-cheat-sheet.vue | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index 1946d17ce..a52cacdd4 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -137,6 +137,18 @@
 						</div>
 					</div>
 				</div>
+				<div class="section _block">
+					<div class="title">{{ i18n.ts._mfm.blockMath }}</div>
+					<div class="content">
+						<p>{{ i18n.ts._mfm.blockMathDescription }}</p>
+						<div class="preview">
+							<Mfm :text="preview_blockMath" />
+							<MkTextarea v-model="preview_blockMath"
+								><template #label>MFM</template></MkTextarea
+							>
+						</div>
+					</div>
+				</div>
 				<!-- deprecated
 		<div class="section _block">
 			<div class="title">{{ i18n.ts._mfm.search }}</div>
@@ -427,6 +439,7 @@ let preview_blockCode = $ref(
 	'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```'
 );
 let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
+let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
 let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
 let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`);
 let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");

From 0db337b977c234006ff6ed8e4919a70932baa8e0 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 30 Apr 2023 23:24:19 +0900
Subject: [PATCH 39/48] more search syntax (although it doesn't appear since
 the description of the search syntax is commented out)

---
 packages/client/src/pages/mfm-cheat-sheet.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index a52cacdd4..f531a1911 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -441,7 +441,7 @@ let preview_blockCode = $ref(
 let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
 let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
 let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
-let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`);
+let preview_search = $ref(`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`);
 let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");
 let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
 let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");

From 1d37e78ca2b09e38a48de686a06101d1825aa4a9 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 30 Apr 2023 23:28:17 +0900
Subject: [PATCH 40/48] use new styling method

---
 packages/client/src/pages/mfm-cheat-sheet.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index f531a1911..71eba67c5 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -2,7 +2,7 @@
 	<MkStickyContainer>
 		<template #header><MkPageHeader /></template>
 		<MkSpacer :content-max="800">
-			<div class="mwysmxbg">
+			<div :class="$style.root">
 				<div>{{ i18n.ts._mfm.intro }}</div>
 				<br />
 				<div class="section _block">
@@ -478,8 +478,8 @@ definePageMetadata({
 });
 </script>
 
-<style lang="scss" scoped>
-.mwysmxbg {
+<style lang="scss" module>
+.root {
 	background: var(--bg);
 
 	> .section {

From 9d193340350baf99ff9cf4507816699c045aa255 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Sun, 30 Apr 2023 23:49:08 +0900
Subject: [PATCH 41/48] blockMath is not necessarily multi-line (is this
 copy-pasted from blockCode?)

---
 locales/en-US.yml | 2 +-
 locales/ja-JP.yml | 2 +-
 locales/zh-CN.yml | 2 +-
 locales/zh-TW.yml | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index f9d4d23f0..e5377dc54 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1197,7 +1197,7 @@ _mfm:
   inlineMath: "Math (Inline)"
   inlineMathDescription: "Display math formulas (KaTeX) in-line"
   blockMath: "Math (Block)"
-  blockMathDescription: "Display multi-line math formulas (KaTeX) in a block"
+  blockMathDescription: "Display math formulas (KaTeX) in a block"
   quote: "Quote"
   quoteDescription: "Displays content as a quote."
   emoji: "Custom Emoji"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 466212ba2..90a775714 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1079,7 +1079,7 @@ _mfm:
   inlineMath: "数式(インライン)"
   inlineMathDescription: "数式(KaTeX)をインラインで表示します。"
   blockMath: "数式(ブロック)"
-  blockMathDescription: "複数行の数式(KaTeX)をブロックで表示します。"
+  blockMathDescription: "数式(KaTeX)をブロックで表示します。"
   quote: "引用"
   quoteDescription: "内容が引用であることを示せます。"
   emoji: "カスタム絵文字"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index c652b52b7..3e90e4fb7 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1011,7 +1011,7 @@ _mfm:
   inlineMath: "数学公式(内嵌)"
   inlineMathDescription: "显示内嵌的KaTex公式。"
   blockMath: "数学公式(块)"
-  blockMathDescription: "显示整块的多行KaTex数学公式。"
+  blockMathDescription: "显示整块的KaTex数学公式。"
   quote: "引用"
   quoteDescription: "可以用来表示引用的内容。"
   emoji: "自定义表情符号"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index eb640b7dd..d1826f772 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -1014,7 +1014,7 @@ _mfm:
   inlineMath: "數學公式(內嵌)"
   inlineMathDescription: "顯示內嵌的KaTex數學公式。"
   blockMath: "數學公式(方塊)"
-  blockMathDescription: "以區塊顯示複數行的KaTex數學式。"
+  blockMathDescription: "以區塊顯示KaTex數學式。"
   quote: "引用"
   quoteDescription: "可以用來表示引用的内容。"
   emoji: "自訂表情符號"

From 44f6c141242ef7ad1bf8f32bdfdc0d4382391adf Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 1 May 2023 00:02:16 +0900
Subject: [PATCH 42/48] KaTex -> KaTeX

---
 locales/zh-CN.yml | 4 ++--
 locales/zh-TW.yml | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 3e90e4fb7..645f11f56 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1009,9 +1009,9 @@ _mfm:
   blockCode: "代码(块)"
   blockCodeDescription: "语法高亮显示整块程序代码。"
   inlineMath: "数学公式(内嵌)"
-  inlineMathDescription: "显示内嵌的KaTex公式。"
+  inlineMathDescription: "显示内嵌的KaTeX公式。"
   blockMath: "数学公式(块)"
-  blockMathDescription: "显示整块的KaTex数学公式。"
+  blockMathDescription: "显示整块的KaTeX数学公式。"
   quote: "引用"
   quoteDescription: "可以用来表示引用的内容。"
   emoji: "自定义表情符号"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index d1826f772..c2dfd1ce0 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -1012,9 +1012,9 @@ _mfm:
   blockCode: "程式碼(區塊)"
   blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。"
   inlineMath: "數學公式(內嵌)"
-  inlineMathDescription: "顯示內嵌的KaTex數學公式。"
+  inlineMathDescription: "顯示內嵌的KaTeX數學公式。"
   blockMath: "數學公式(方塊)"
-  blockMathDescription: "以區塊顯示KaTex數學式。"
+  blockMathDescription: "以區塊顯示KaTeX數學式。"
   quote: "引用"
   quoteDescription: "可以用來表示引用的内容。"
   emoji: "自訂表情符號"

From affeb55c0116104b7b6b9b8a2c3827825ad6929f Mon Sep 17 00:00:00 2001
From: Freeplay <Freeplay@duck.com>
Date: Sun, 30 Apr 2023 11:58:36 -0400
Subject: [PATCH 43/48] Fix jumping to top of page when opening menu

I also thought this would maybe fix one of the focustrap errors in the console, but no.
---
 packages/client/src/components/MkMenu.vue | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index c71e3ac58..41611bf0b 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,6 +1,6 @@
 <template>
-	<FocusTrap v-bind:active="isActive">
-		<div tabindex="-1" v-focus>
+	<FocusTrap :active="false" ref="focusTrap">
+		<div tabindex="-1">
 			<div
 				ref="itemsEl"
 				class="rrevdjwt _popup _shadow"
@@ -206,6 +206,7 @@ import { i18n } from "@/i18n";
 import { FocusTrap } from 'focus-trap-vue';
 
 const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
+const focusTrap = ref();
 
 const props = defineProps<{
 	items: MenuItem[];
@@ -316,6 +317,8 @@ function focusDown() {
 }
 
 onMounted(() => {
+	focusTrap.value.activate();
+
 	if (props.viaKeyboard) {
 		nextTick(() => {
 			focusNext(itemsEl.children[0], true, false);

From 0cf2e71b2e361b050b2d16224ce18a5db99abc64 Mon Sep 17 00:00:00 2001
From: fruye <fruye@unix.dog>
Date: Sun, 30 Apr 2023 19:34:52 +0000
Subject: [PATCH 44/48] Use numeric ids everywhere in mastodon API (#9970)

Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9970
Co-authored-by: fruye <fruye@unix.dog>
Co-committed-by: fruye <fruye@unix.dog>
---
 packages/backend/src/server/api/index.ts      |   5 +-
 .../mastodon/ApiMastodonCompatibleService.ts  |  10 +-
 .../src/server/api/mastodon/converters.ts     |  44 +++++
 .../server/api/mastodon/endpoints/account.ts  | 163 ++++--------------
 .../server/api/mastodon/endpoints/filter.ts   |  20 ++-
 .../api/mastodon/endpoints/notifications.ts   |  17 +-
 .../server/api/mastodon/endpoints/search.ts   |  21 ++-
 .../server/api/mastodon/endpoints/status.ts   |  97 +++++++----
 .../server/api/mastodon/endpoints/timeline.ts |  73 +++++---
 9 files changed, 238 insertions(+), 212 deletions(-)
 create mode 100644 packages/backend/src/server/api/mastodon/converters.ts

diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts
index 06b3ea4ef..3568a27b2 100644
--- a/packages/backend/src/server/api/index.ts
+++ b/packages/backend/src/server/api/index.ts
@@ -29,6 +29,7 @@ import {
 	convertId,
 	IdConvertType as IdType,
 } from "../../../native-utils/built/index.js";
+import { convertAttachment } from "./mastodon/converters.js";
 
 // re-export native rust id conversion (function and enum)
 export { IdType, convertId };
@@ -93,7 +94,7 @@ mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
 			return;
 		}
 		const data = await client.uploadMedia(multipartData);
-		ctx.body = data.data;
+		ctx.body = convertAttachment(data.data as Entity.Attachment);
 	} catch (e: any) {
 		console.error(e);
 		ctx.status = 401;
@@ -112,7 +113,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
 			return;
 		}
 		const data = await client.uploadMedia(multipartData);
-		ctx.body = data.data;
+		ctx.body = convertAttachment(data.data as Entity.Attachment);
 	} catch (e: any) {
 		console.error(e);
 		ctx.status = 401;
diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts
index e8dfe5281..0c59b38f4 100644
--- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts
+++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts
@@ -8,6 +8,8 @@ import { apiTimelineMastodon } from "./endpoints/timeline.js";
 import { apiNotificationsMastodon } from "./endpoints/notifications.js";
 import { apiSearchMastodon } from "./endpoints/search.js";
 import { getInstance } from "./endpoints/meta.js";
+import { convertAnnouncement, convertFilter } from "./converters.js";
+import { convertId, IdType } from "../index.js";
 
 export function getClient(
 	BASE_URL: string,
@@ -68,7 +70,7 @@ export function apiMastodonCompatible(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = await client.getInstanceAnnouncements();
-			ctx.body = data.data;
+			ctx.body = data.data.map(announcement => convertAnnouncement(announcement));
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -83,7 +85,9 @@ export function apiMastodonCompatible(router: Router): void {
 			const accessTokens = ctx.request.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.dismissInstanceAnnouncement(ctx.params.id);
+				const data = await client.dismissInstanceAnnouncement(
+					convertId(ctx.params.id, IdType.CalckeyId),
+				);
 				ctx.body = data.data;
 			} catch (e: any) {
 				console.error(e);
@@ -100,7 +104,7 @@ export function apiMastodonCompatible(router: Router): void {
 		// displayed without being logged in
 		try {
 			const data = await client.getFilters();
-			ctx.body = data.data;
+			ctx.body = data.data.map(filter => convertFilter(filter));
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
new file mode 100644
index 000000000..d9a4f90ef
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -0,0 +1,44 @@
+import { Entity } from "@calckey/megalodon";
+import { convertId, IdType } from "../index.js";
+
+function simpleConvert(data: any) {
+	data.id = convertId(data.id, IdType.MastodonId);
+	return data;
+}
+
+export function convertAccount(account: Entity.Account) { return simpleConvert(account); }
+export function convertAnnouncement(announcement: Entity.Announcement) { return simpleConvert(announcement); }
+export function convertAttachment(attachment: Entity.Attachment) { return simpleConvert(attachment); }
+export function convertFilter(filter: Entity.Filter) { return simpleConvert(filter); }
+export function convertList(list: Entity.List) { return simpleConvert(list); }
+
+export function convertNotification(notification: Entity.Notification) {
+	notification.account = convertAccount(notification.account);
+	notification.id = convertId(notification.id, IdType.MastodonId);
+	if (notification.status)
+		notification.status = convertStatus(notification.status);
+	return notification;
+}
+
+export function convertPoll(poll: Entity.Poll) { return simpleConvert(poll); }
+export function convertRelationship(relationship: Entity.Relationship) { return simpleConvert(relationship); }
+
+export function convertStatus(status: Entity.Status) {
+	status.account = convertAccount(status.account);
+	status.id = convertId(status.id, IdType.MastodonId);
+	if (status.in_reply_to_account_id)
+		status.in_reply_to_account_id = convertId(status.in_reply_to_account_id, IdType.MastodonId);
+	if (status.in_reply_to_id)
+		status.in_reply_to_id = convertId(status.in_reply_to_id, IdType.MastodonId);
+	status.media_attachments = status.media_attachments.map(attachment => convertAttachment(attachment));
+	status.mentions = status.mentions.map(mention => ({
+		...mention,
+		id: convertId(mention.id, IdType.MastodonId),
+	}));
+	if (status.poll)
+		status.poll = convertPoll(status.poll);
+	if (status.reblog)
+		status.reblog = convertStatus(status.reblog);
+
+	return status;
+}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 749058193..2984c20e3 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -3,8 +3,9 @@ import { resolveUser } from "@/remote/resolve-user.js";
 import Router from "@koa/router";
 import { FindOptionsWhere, IsNull } from "typeorm";
 import { getClient } from "../ApiMastodonCompatibleService.js";
-import { argsToBools, limitToInt } from "./timeline.js";
+import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js";
 import { convertId, IdType } from "../../index.js";
+import { convertAccount, convertList, convertRelationship, convertStatus } from "../converters.js";
 
 const relationshipModel = {
 	id: "",
@@ -62,9 +63,7 @@ export function apiAccountMastodon(router: Router): void {
 			const data = await client.updateCredentials(
 				(ctx.request as any).body as any,
 			);
-			let resp = data.data;
-			resp.id = convertId(resp.id, IdType.MastodonId);
-			ctx.body = resp;
+			ctx.body = convertAccount(data.data);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -81,9 +80,7 @@ export function apiAccountMastodon(router: Router): void {
 				(ctx.request.query as any).acct,
 				"accounts",
 			);
-			let resp = data.data.accounts[0];
-			resp.id = convertId(resp.id, IdType.MastodonId);
-			ctx.body = resp;
+			ctx.body = convertAccount(data.data.accounts[0]);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -115,11 +112,7 @@ export function apiAccountMastodon(router: Router): void {
 			}
 
 			const data = await client.getRelationships(reqIds);
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
+			ctx.body = data.data.map(relationship => convertRelationship(relationship));
 		} catch (e: any) {
 			console.error(e);
 			let data = e.response.data;
@@ -136,9 +129,7 @@ export function apiAccountMastodon(router: Router): void {
 		try {
 			const calcId = convertId(ctx.params.id, IdType.CalckeyId);
 			const data = await client.getAccount(calcId);
-			let resp = data.data;
-			resp.id = convertId(resp.id, IdType.MastodonId);
-			ctx.body = resp;
+			ctx.body = convertAccount(data.data);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -155,27 +146,9 @@ export function apiAccountMastodon(router: Router): void {
 			try {
 				const data = await client.getAccountStatuses(
 					convertId(ctx.params.id, IdType.CalckeyId),
-					argsToBools(limitToInt(ctx.query as any)),
+					convertTimelinesArgsId(argsToBools(limitToInt(ctx.query as any))),
 				);
-				let resp = data.data;
-				for (let statIdx = 0; statIdx < resp.length; statIdx++) {
-					resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
-					resp[statIdx].in_reply_to_account_id = resp[statIdx]
-						.in_reply_to_account_id
-						? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
-						: null;
-					resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
-						? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
-						: null;
-					let mentions = resp[statIdx].mentions;
-					for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
-						resp[statIdx].mentions[mtnIdx].id = convertId(
-							mentions[mtnIdx].id,
-							IdType.MastodonId,
-						);
-					}
-				}
-				ctx.body = resp;
+				ctx.body = data.data.map(status => convertStatus(status));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -193,13 +166,9 @@ export function apiAccountMastodon(router: Router): void {
 			try {
 				const data = await client.getAccountFollowers(
 					convertId(ctx.params.id, IdType.CalckeyId),
-					limitToInt(ctx.query as any),
+					convertTimelinesArgsId(limitToInt(ctx.query as any)),
 				);
-				let resp = data.data;
-				for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-					resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-				}
-				ctx.body = resp;
+				ctx.body = data.data.map(account => convertAccount(account));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -217,13 +186,9 @@ export function apiAccountMastodon(router: Router): void {
 			try {
 				const data = await client.getAccountFollowing(
 					convertId(ctx.params.id, IdType.CalckeyId),
-					limitToInt(ctx.query as any),
+					convertTimelinesArgsId(limitToInt(ctx.query as any)),
 				);
-				let resp = data.data;
-				for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-					resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-				}
-				ctx.body = resp;
+				ctx.body = data.data.map(account => convertAccount(account));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -239,8 +204,10 @@ export function apiAccountMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.getAccountLists(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.getAccountLists(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = data.data.map(list => convertList(list));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -259,9 +226,8 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.followAccount(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let acct = data.data;
+				let acct = convertRelationship(data.data);
 				acct.following = true;
-				acct.id = convertId(acct.id, IdType.MastodonId);
 				ctx.body = acct;
 			} catch (e: any) {
 				console.error(e);
@@ -281,8 +247,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.unfollowAccount(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let acct = data.data;
-				acct.id = convertId(acct.id, IdType.MastodonId);
+				let acct = convertRelationship(data.data);
 				acct.following = false;
 				ctx.body = acct;
 			} catch (e: any) {
@@ -303,9 +268,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.blockAccount(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -324,9 +287,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.unblockAccount(
 					convertId(ctx.params.id, IdType.MastodonId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -346,9 +307,7 @@ export function apiAccountMastodon(router: Router): void {
 					convertId(ctx.params.id, IdType.CalckeyId),
 					(ctx.request as any).body as any,
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -367,9 +326,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.unmuteAccount(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -384,27 +341,9 @@ export function apiAccountMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = (await client.getBookmarks(
-				limitToInt(ctx.query as any),
-			)) as any;
-			let resp = data.data;
-			for (let statIdx = 0; statIdx < resp.length; statIdx++) {
-				resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
-				resp[statIdx].in_reply_to_account_id = resp[statIdx]
-					.in_reply_to_account_id
-					? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
-					: null;
-				resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
-					? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
-					: null;
-				let mentions = resp[statIdx].mentions;
-				for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
-					resp[statIdx].mentions[mtnIdx].id = convertId(
-						mentions[mtnIdx].id,
-						IdType.MastodonId,
-					);
-				}
-			}
-			ctx.body = resp;
+				convertTimelinesArgsId(limitToInt(ctx.query as any)),
+			));
+			ctx.body = data.data.map(status => convertStatus(status));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -417,26 +356,8 @@ export function apiAccountMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getFavourites(limitToInt(ctx.query as any));
-			let resp = data.data;
-			for (let statIdx = 0; statIdx < resp.length; statIdx++) {
-				resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
-				resp[statIdx].in_reply_to_account_id = resp[statIdx]
-					.in_reply_to_account_id
-					? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
-					: null;
-				resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
-					? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
-					: null;
-				let mentions = resp[statIdx].mentions;
-				for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
-					resp[statIdx].mentions[mtnIdx].id = convertId(
-						mentions[mtnIdx].id,
-						IdType.MastodonId,
-					);
-				}
-			}
-			ctx.body = resp;
+			const data = await client.getFavourites(convertTimelinesArgsId(limitToInt(ctx.query as any)));
+			ctx.body = data.data.map(status => convertStatus(status));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -449,12 +370,8 @@ export function apiAccountMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getMutes(limitToInt(ctx.query as any));
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
+			const data = await client.getMutes(convertTimelinesArgsId(limitToInt(ctx.query as any)));
+			ctx.body = data.data.map(account => convertAccount(account));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -467,12 +384,8 @@ export function apiAccountMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getBlocks(limitToInt(ctx.query as any));
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
+			const data = await client.getBlocks(convertTimelinesArgsId(limitToInt(ctx.query as any)));
+			ctx.body = data.data.map(account => convertAccount(account));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -488,11 +401,7 @@ export function apiAccountMastodon(router: Router): void {
 			const data = await client.getFollowRequests(
 				((ctx.query as any) || { limit: 20 }).limit,
 			);
-			let resp = data.data;
-			for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
-				resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
-			}
-			ctx.body = resp;
+			ctx.body = data.data.map(account => convertAccount(account));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -510,9 +419,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.acceptFollowRequest(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -531,9 +438,7 @@ export function apiAccountMastodon(router: Router): void {
 				const data = await client.rejectFollowRequest(
 					convertId(ctx.params.id, IdType.CalckeyId),
 				);
-				let resp = data.data;
-				resp.id = convertId(resp.id, IdType.MastodonId);
-				ctx.body = resp;
+				ctx.body = convertRelationship(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
index d21bc1d33..7343fc337 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
@@ -1,6 +1,8 @@
 import megalodon, { MegalodonInterface } from "@calckey/megalodon";
 import Router from "@koa/router";
 import { getClient } from "../ApiMastodonCompatibleService.js";
+import { IdType, convertId } from "../../index.js";
+import { convertFilter } from "../converters.js";
 
 export function apiFilterMastodon(router: Router): void {
 	router.get("/v1/filters", async (ctx) => {
@@ -10,7 +12,7 @@ export function apiFilterMastodon(router: Router): void {
 		const body: any = ctx.request.body;
 		try {
 			const data = await client.getFilters();
-			ctx.body = data.data;
+			ctx.body = data.data.map(filter => convertFilter(filter));
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -24,8 +26,10 @@ export function apiFilterMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const data = await client.getFilter(ctx.params.id);
-			ctx.body = data.data;
+			const data = await client.getFilter(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
+			ctx.body = convertFilter(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -40,7 +44,7 @@ export function apiFilterMastodon(router: Router): void {
 		const body: any = ctx.request.body;
 		try {
 			const data = await client.createFilter(body.phrase, body.context, body);
-			ctx.body = data.data;
+			ctx.body = convertFilter(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -55,11 +59,11 @@ export function apiFilterMastodon(router: Router): void {
 		const body: any = ctx.request.body;
 		try {
 			const data = await client.updateFilter(
-				ctx.params.id,
+				convertId(ctx.params.id, IdType.CalckeyId),
 				body.phrase,
 				body.context,
 			);
-			ctx.body = data.data;
+			ctx.body = convertFilter(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -73,7 +77,9 @@ export function apiFilterMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const data = await client.deleteFilter(ctx.params.id);
+			const data = await client.deleteFilter(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
 			ctx.body = data.data;
 		} catch (e: any) {
 			console.error(e);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
index 8508f1d48..868377b78 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
@@ -1,8 +1,10 @@
 import megalodon, { MegalodonInterface } from "@calckey/megalodon";
 import Router from "@koa/router";
 import { koaBody } from "koa-body";
+import { convertId, IdType } from "../../index.js";
 import { getClient } from "../ApiMastodonCompatibleService.js";
-import { toTextWithReaction } from "./timeline.js";
+import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js";
+import { convertNotification } from "../converters.js";
 function toLimitToInt(q: any) {
 	if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
 	return q;
@@ -15,9 +17,10 @@ export function apiNotificationsMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const data = await client.getNotifications(toLimitToInt(ctx.query));
+			const data = await client.getNotifications(convertTimelinesArgsId(toLimitToInt(ctx.query)));
 			const notfs = data.data;
 			const ret = notfs.map((n) => {
+				n = convertNotification(n);
 				if (n.type !== "follow" && n.type !== "follow_request") {
 					if (n.type === "reaction") n.type = "favourite";
 					n.status = toTextWithReaction(
@@ -43,8 +46,10 @@ export function apiNotificationsMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const dataRaw = await client.getNotification(ctx.params.id);
-			const data = dataRaw.data;
+			const dataRaw = await client.getNotification(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
+			const data = convertNotification(dataRaw.data);
 			if (data.type !== "follow" && data.type !== "follow_request") {
 				if (data.type === "reaction") data.type = "favourite";
 				ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0];
@@ -79,7 +84,9 @@ export function apiNotificationsMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const data = await client.dismissNotification(ctx.params.id);
+			const data = await client.dismissNotification(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
 			ctx.body = data.data;
 		} catch (e: any) {
 			console.error(e);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts
index e4990811a..98349cfd2 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/search.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts
@@ -3,7 +3,8 @@ import Router from "@koa/router";
 import { getClient } from "../ApiMastodonCompatibleService.js";
 import axios from "axios";
 import { Converter } from "@calckey/megalodon";
-import { limitToInt } from "./timeline.js";
+import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
+import { convertAccount, convertStatus } from "../converters.js";
 
 export function apiSearchMastodon(router: Router): void {
 	router.get("/v1/search", async (ctx) => {
@@ -12,7 +13,7 @@ export function apiSearchMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		const body: any = ctx.request.body;
 		try {
-			const query: any = limitToInt(ctx.query);
+			const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
 			const type = query.type || "";
 			const data = await client.search(query.q, type, query);
 			ctx.body = data.data;
@@ -27,18 +28,18 @@ export function apiSearchMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const query: any = limitToInt(ctx.query);
+			const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
 			const type = query.type;
 			if (type) {
 				const data = await client.search(query.q, type, query);
-				ctx.body = data.data;
+				ctx.body = data.data.accounts.map(account => convertAccount(account));
 			} else {
 				const acct = await client.search(query.q, "accounts", query);
 				const stat = await client.search(query.q, "statuses", query);
 				const tags = await client.search(query.q, "hashtags", query);
 				ctx.body = {
-					accounts: acct.data.accounts,
-					statuses: stat.data.statuses,
+					accounts: acct.data.accounts.map(account => convertAccount(account)),
+					statuses: stat.data.statuses.map(status => convertStatus(status)),
 					hashtags: tags.data.hashtags,
 				};
 			}
@@ -57,7 +58,7 @@ export function apiSearchMastodon(router: Router): void {
 				ctx.request.hostname,
 				accessTokens,
 			);
-			ctx.body = data;
+			ctx.body = data.map(status => convertStatus(status));
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -69,12 +70,16 @@ export function apiSearchMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		try {
 			const query: any = ctx.query;
-			const data = await getFeaturedUser(
+			let data = await getFeaturedUser(
 				BASE_URL,
 				ctx.request.hostname,
 				accessTokens,
 				query.limit || 20,
 			);
+			data = data.map(suggestion => {
+				suggestion.account = convertAccount(suggestion.account);
+				return suggestion;
+			});
 			console.log(data);
 			ctx.body = data;
 		} catch (e: any) {
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index f7589569c..27b30d113 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -4,7 +4,9 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
 import axios from "axios";
 import querystring from "node:querystring";
 import qs from "qs";
-import { limitToInt } from "./timeline.js";
+import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
+import { convertId, IdType } from "../../index.js";
+import { convertAccount, convertAttachment, convertPoll, convertStatus } from "../converters.js";
 
 function normalizeQuery(data: any) {
 	const str = querystring.stringify(data);
@@ -18,6 +20,8 @@ export function apiStatusMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			let body: any = ctx.request.body;
+			if (body.in_reply_to_id)
+				body.in_reply_to_id = convertId(body.in_reply_to_id, IdType.CalckeyId);
 			if (
 				(!body.poll && body["poll[options][]"]) ||
 				(!body.media_ids && body["media_ids[]"])
@@ -54,7 +58,7 @@ export function apiStatusMastodon(router: Router): void {
 			body.sensitive =
 				typeof sensitive === "string" ? sensitive === "true" : sensitive;
 			const data = await client.postStatus(text, body);
-			ctx.body = data.data;
+			ctx.body = convertStatus(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -66,8 +70,10 @@ export function apiStatusMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getStatus(ctx.params.id);
-			ctx.body = data.data;
+			const data = await client.getStatus(
+				convertId(ctx.params.id, IdType.CalckeyId),
+			);
+			ctx.body = convertStatus(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -79,7 +85,9 @@ export function apiStatusMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.deleteStatus(ctx.params.id);
+			const data = await client.deleteStatus(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
 			ctx.body = data.data;
 		} catch (e: any) {
 			console.error(e.response.data, request.params.id);
@@ -100,10 +108,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const id = ctx.params.id;
+				const id = convertId(ctx.params.id, IdType.CalckeyId);
 				const data = await client.getStatusContext(
 					id,
-					limitToInt(ctx.query as any),
+					convertTimelinesArgsId(limitToInt(ctx.query as any)),
 				);
 				const status = await client.getStatus(id);
 				let reqInstance = axios.create({
@@ -126,6 +134,8 @@ export function apiStatusMastodon(router: Router): void {
 						text,
 					),
 				);
+				data.data.ancestors = data.data.ancestors.map(status => convertStatus(status));
+				data.data.descendants = data.data.descendants.map(status => convertStatus(status));
 				ctx.body = data.data;
 			} catch (e: any) {
 				console.error(e);
@@ -141,8 +151,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.getStatusRebloggedBy(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.getStatusRebloggedBy(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = data.data.map(account => convertAccount(account));
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -165,11 +177,11 @@ export function apiStatusMastodon(router: Router): void {
 			const react = await getFirstReaction(BASE_URL, accessTokens);
 			try {
 				const a = (await client.createEmojiReaction(
-					ctx.params.id,
+					convertId(ctx.params.id, IdType.CalckeyId),
 					react,
 				)) as any;
 				//const data = await client.favouriteStatus(ctx.params.id) as any;
-				ctx.body = a.data;
+				ctx.body = convertStatus(a.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -186,8 +198,11 @@ export function apiStatusMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			const react = await getFirstReaction(BASE_URL, accessTokens);
 			try {
-				const data = await client.deleteEmojiReaction(ctx.params.id, react);
-				ctx.body = data.data;
+				const data = await client.deleteEmojiReaction(
+					convertId(ctx.params.id, IdType.CalckeyId),
+					react
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -203,8 +218,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.reblogStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.reblogStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -220,8 +237,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.unreblogStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.unreblogStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -237,8 +256,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.bookmarkStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.bookmarkStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -254,8 +275,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = (await client.unbookmarkStatus(ctx.params.id)) as any;
-				ctx.body = data.data;
+				const data = await client.unbookmarkStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -271,8 +294,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.pinStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.pinStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -288,8 +313,10 @@ export function apiStatusMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.unpinStatus(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.unpinStatus(
+					convertId(ctx.params.id, IdType.CalckeyId)
+				);
+				ctx.body = convertStatus(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
@@ -302,8 +329,10 @@ export function apiStatusMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getMedia(ctx.params.id);
-			ctx.body = data.data;
+			const data = await client.getMedia(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
+			ctx.body = convertAttachment(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -316,10 +345,10 @@ export function apiStatusMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = await client.updateMedia(
-				ctx.params.id,
+				convertId(ctx.params.id, IdType.CalckeyId),
 				ctx.request.body as any,
 			);
-			ctx.body = data.data;
+			ctx.body = convertAttachment(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -331,8 +360,10 @@ export function apiStatusMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getPoll(ctx.params.id);
-			ctx.body = data.data;
+			const data = await client.getPoll(
+				convertId(ctx.params.id, IdType.CalckeyId)
+			);
+			ctx.body = convertPoll(data.data);
 		} catch (e: any) {
 			console.error(e);
 			ctx.status = 401;
@@ -347,10 +378,10 @@ export function apiStatusMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.votePoll(
-					ctx.params.id,
+					convertId(ctx.params.id, IdType.CalckeyId),
 					(ctx.request.body as any).choices,
 				);
-				ctx.body = data.data;
+				ctx.body = convertPoll(data.data);
 			} catch (e: any) {
 				console.error(e);
 				ctx.status = 401;
diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
index 57e5d9bb0..268c6a161 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
@@ -4,6 +4,8 @@ import { getClient } from "../ApiMastodonCompatibleService.js";
 import { statusModel } from "./status.js";
 import Autolinker from "autolinker";
 import { ParsedUrlQuery } from "querystring";
+import { convertAccount, convertList, convertStatus } from "../converters.js";
+import { convertId, IdType } from "../../index.js";
 
 export function limitToInt(q: ParsedUrlQuery) {
 	let object: any = q;
@@ -29,6 +31,16 @@ export function argsToBools(q: ParsedUrlQuery) {
 	return q;
 }
 
+export function convertTimelinesArgsId(q: ParsedUrlQuery) {
+	if (typeof q.min_id === "string")
+		q.min_id = convertId(q.min_id, IdType.CalckeyId);
+	if (typeof q.max_id === "string")
+		q.max_id = convertId(q.max_id, IdType.CalckeyId);
+	if (typeof q.since_id === "string")
+		q.since_id = convertId(q.since_id, IdType.CalckeyId);
+	return q;
+}
+
 export function toTextWithReaction(status: Entity.Status[], host: string) {
 	return status.map((t) => {
 		if (!t) return statusModel(null, null, [], "no content");
@@ -97,9 +109,10 @@ export function apiTimelineMastodon(router: Router): void {
 		try {
 			const query: any = ctx.query;
 			const data = query.local
-				? await client.getLocalTimeline(argsToBools(limitToInt(query)))
-				: await client.getPublicTimeline(argsToBools(limitToInt(query)));
-			ctx.body = toTextWithReaction(data.data, ctx.hostname);
+				? await client.getLocalTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))))
+				: await client.getPublicTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))));
+			let resp = data.data.map(status => convertStatus(status));
+			ctx.body = toTextWithReaction(resp, ctx.hostname);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -116,9 +129,10 @@ export function apiTimelineMastodon(router: Router): void {
 			try {
 				const data = await client.getTagTimeline(
 					ctx.params.hashtag,
-					argsToBools(limitToInt(ctx.query)),
+					convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))),
 				);
-				ctx.body = toTextWithReaction(data.data, ctx.hostname);
+				let resp = data.data.map(status => convertStatus(status));
+				ctx.body = toTextWithReaction(resp, ctx.hostname);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -132,8 +146,9 @@ export function apiTimelineMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getHomeTimeline(limitToInt(ctx.query));
-			ctx.body = toTextWithReaction(data.data, ctx.hostname);
+			const data = await client.getHomeTimeline(convertTimelinesArgsId(limitToInt(ctx.query)));
+			let resp = data.data.map(status => convertStatus(status));
+			ctx.body = toTextWithReaction(resp, ctx.hostname);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -149,10 +164,11 @@ export function apiTimelineMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.getListTimeline(
-					ctx.params.listId,
-					limitToInt(ctx.query),
+					convertId(ctx.params.listId, IdType.CalckeyId),
+					convertTimelinesArgsId(limitToInt(ctx.query)),
 				);
-				ctx.body = toTextWithReaction(data.data, ctx.hostname);
+				let resp = data.data.map(status => convertStatus(status));
+				ctx.body = toTextWithReaction(resp, ctx.hostname);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -166,7 +182,7 @@ export function apiTimelineMastodon(router: Router): void {
 		const accessTokens = ctx.headers.authorization;
 		const client = getClient(BASE_URL, accessTokens);
 		try {
-			const data = await client.getConversationTimeline(limitToInt(ctx.query));
+			const data = await client.getConversationTimeline(convertTimelinesArgsId(limitToInt(ctx.query)));
 			ctx.body = data.data;
 		} catch (e: any) {
 			console.error(e);
@@ -181,7 +197,7 @@ export function apiTimelineMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = await client.getLists();
-			ctx.body = data.data;
+			ctx.body = data.data.map(list => convertList(list));
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -196,8 +212,10 @@ export function apiTimelineMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.getList(ctx.params.id);
-				ctx.body = data.data;
+				const data = await client.getList(
+					convertId(ctx.params.id, IdType.CalckeyId),
+				);
+				ctx.body = convertList(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -212,7 +230,7 @@ export function apiTimelineMastodon(router: Router): void {
 		const client = getClient(BASE_URL, accessTokens);
 		try {
 			const data = await client.createList((ctx.request.body as any).title);
-			ctx.body = data.data;
+			ctx.body = convertList(data.data);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -227,8 +245,11 @@ export function apiTimelineMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.updateList(ctx.params.id, (ctx.request.body as any).title);
-				ctx.body = data.data;
+				const data = await client.updateList(
+					convertId(ctx.params.id, IdType.CalckeyId),
+					(ctx.request.body as any).title
+				);
+				ctx.body = convertList(data.data);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -244,7 +265,9 @@ export function apiTimelineMastodon(router: Router): void {
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
 			try {
-				const data = await client.deleteList(ctx.params.id);
+				const data = await client.deleteList(
+					convertId(ctx.params.id, IdType.CalckeyId),
+				);
 				ctx.body = data.data;
 			} catch (e: any) {
 				console.error(e);
@@ -262,10 +285,10 @@ export function apiTimelineMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.getAccountsInList(
-					ctx.params.id,
-					ctx.query as any,
+					convertId(ctx.params.id, IdType.CalckeyId),
+					convertTimelinesArgsId(ctx.query as any),
 				);
-				ctx.body = data.data;
+				ctx.body = data.data.map(account => convertAccount(account));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -282,8 +305,8 @@ export function apiTimelineMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.addAccountsToList(
-					ctx.params.id,
-					(ctx.query as any).account_ids,
+					convertId(ctx.params.id, IdType.CalckeyId),
+					(ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)),
 				);
 				ctx.body = data.data;
 			} catch (e: any) {
@@ -302,8 +325,8 @@ export function apiTimelineMastodon(router: Router): void {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.deleteAccountsFromList(
-					ctx.params.id,
-					(ctx.query as any).account_ids,
+					convertId(ctx.params.id, IdType.CalckeyId),
+					(ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)),
 				);
 				ctx.body = data.data;
 			} catch (e: any) {

From 6f9a9dbfa13ca5aba0a2aacd6f8ef1b449919667 Mon Sep 17 00:00:00 2001
From: waon <waonme@gmail.com>
Date: Sun, 30 Apr 2023 07:55:50 +0000
Subject: [PATCH 45/48] chore: Translated using Weblate (Japanese)

Currently translated at 99.3% (1724 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/ja/
---
 locales/ja-JP.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 466212ba2..c0bb851a8 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1120,6 +1120,7 @@ _mfm:
   rotateDescription: "指定した角度で回転させます。"
   plain: "プレーン"
   plainDescription: "内側の構文を全て無効にします。"
+  position: 位置
 _instanceTicker:
   none: "表示しない"
   remote: "リモートユーザーに表示"
@@ -1128,7 +1129,7 @@ _serverDisconnectedBehavior:
   reload: "自動でリロード"
   dialog: "ダイアログで警告"
   quiet: "控えめに警告"
-  nothing: "何も起こらない"
+  nothing: "何もしない"
 _channel:
   create: "チャンネルを作成"
   edit: "チャンネルを編集"

From 3191853c32af7fcb7e44ceda260caff65e45c398 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 13:13:43 -0700
Subject: [PATCH 46/48] chore: up browser-image-resizer

---
 packages/client/package.json |   2 +-
 pnpm-lock.yaml               | 165 ++++++++++++++++++++++++++++++++---
 2 files changed, 154 insertions(+), 13 deletions(-)

diff --git a/packages/client/package.json b/packages/client/package.json
index 49c175b15..173585503 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -32,7 +32,7 @@
 		"autosize": "5.0.2",
 		"blurhash": "1.1.5",
 		"broadcast-channel": "4.19.1",
-		"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git",
+		"browser-image-resizer": "github:misskey-dev/browser-image-resizer",
 		"calckey-js": "workspace:*",
 		"chart.js": "4.1.1",
 		"chartjs-adapter-date-fns": "2.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 60e42e11a..77b0e0d4c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,7 +24,7 @@ importers:
         version: 7.2.0
       focus-trap-vue:
         specifier: ^4.0.1
-        version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
+        version: 4.0.1(focus-trap@7.2.0)(vue@3.2.47)
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
@@ -726,8 +726,8 @@ importers:
         specifier: 4.19.1
         version: 4.19.1
       browser-image-resizer:
-        specifier: https://github.com/misskey-dev/browser-image-resizer.git
-        version: github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c
+        specifier: github:misskey-dev/browser-image-resizer
+        version: github.com/misskey-dev/browser-image-resizer/56f504427ad7f6500e141a6d9f3aee42023d7f3e
       calckey-js:
         specifier: workspace:*
         version: link:../calckey-js
@@ -1071,6 +1071,11 @@ packages:
     resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
     engines: {node: '>=6.9.0'}
 
+  /@babel/helper-string-parser@7.21.5:
+    resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
+    engines: {node: '>=6.9.0'}
+    dev: false
+
   /@babel/helper-validator-identifier@7.19.1:
     resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
     engines: {node: '>=6.9.0'}
@@ -1114,6 +1119,14 @@ packages:
       '@babel/types': 7.21.4
     dev: true
 
+  /@babel/parser@7.21.5:
+    resolution: {integrity: sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+    dependencies:
+      '@babel/types': 7.21.5
+    dev: false
+
   /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.4):
     resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
     peerDependencies:
@@ -1300,6 +1313,15 @@ packages:
       to-fast-properties: 2.0.0
     dev: true
 
+  /@babel/types@7.21.5:
+    resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-string-parser': 7.21.5
+      '@babel/helper-validator-identifier': 7.19.1
+      to-fast-properties: 2.0.0
+    dev: false
+
   /@bcoe/v8-coverage@0.2.3:
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
     dev: true
@@ -3809,12 +3831,30 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       source-map: 0.6.1
+    dev: true
+
+  /@vue/compiler-core@3.2.47:
+    resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==}
+    dependencies:
+      '@babel/parser': 7.21.5
+      '@vue/shared': 3.2.47
+      estree-walker: 2.0.2
+      source-map: 0.6.1
+    dev: false
 
   /@vue/compiler-dom@3.2.45:
     resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
     dependencies:
       '@vue/compiler-core': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
+
+  /@vue/compiler-dom@3.2.47:
+    resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
+    dependencies:
+      '@vue/compiler-core': 3.2.47
+      '@vue/shared': 3.2.47
+    dev: false
 
   /@vue/compiler-sfc@2.7.14:
     resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@@ -3837,12 +3877,36 @@ packages:
       magic-string: 0.25.9
       postcss: 8.4.21
       source-map: 0.6.1
+    dev: true
+
+  /@vue/compiler-sfc@3.2.47:
+    resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
+    dependencies:
+      '@babel/parser': 7.21.5
+      '@vue/compiler-core': 3.2.47
+      '@vue/compiler-dom': 3.2.47
+      '@vue/compiler-ssr': 3.2.47
+      '@vue/reactivity-transform': 3.2.47
+      '@vue/shared': 3.2.47
+      estree-walker: 2.0.2
+      magic-string: 0.25.9
+      postcss: 8.4.23
+      source-map: 0.6.1
+    dev: false
 
   /@vue/compiler-ssr@3.2.45:
     resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
     dependencies:
       '@vue/compiler-dom': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
+
+  /@vue/compiler-ssr@3.2.47:
+    resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
+    dependencies:
+      '@vue/compiler-dom': 3.2.47
+      '@vue/shared': 3.2.47
+    dev: false
 
   /@vue/reactivity-transform@3.2.45:
     resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@@ -3852,17 +3916,43 @@ packages:
       '@vue/shared': 3.2.45
       estree-walker: 2.0.2
       magic-string: 0.25.9
+    dev: true
+
+  /@vue/reactivity-transform@3.2.47:
+    resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==}
+    dependencies:
+      '@babel/parser': 7.21.5
+      '@vue/compiler-core': 3.2.47
+      '@vue/shared': 3.2.47
+      estree-walker: 2.0.2
+      magic-string: 0.25.9
+    dev: false
 
   /@vue/reactivity@3.2.45:
     resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
     dependencies:
       '@vue/shared': 3.2.45
+    dev: true
+
+  /@vue/reactivity@3.2.47:
+    resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==}
+    dependencies:
+      '@vue/shared': 3.2.47
+    dev: false
 
   /@vue/runtime-core@3.2.45:
     resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
     dependencies:
       '@vue/reactivity': 3.2.45
       '@vue/shared': 3.2.45
+    dev: true
+
+  /@vue/runtime-core@3.2.47:
+    resolution: {integrity: sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==}
+    dependencies:
+      '@vue/reactivity': 3.2.47
+      '@vue/shared': 3.2.47
+    dev: false
 
   /@vue/runtime-dom@3.2.45:
     resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@@ -3870,6 +3960,15 @@ packages:
       '@vue/runtime-core': 3.2.45
       '@vue/shared': 3.2.45
       csstype: 2.6.21
+    dev: true
+
+  /@vue/runtime-dom@3.2.47:
+    resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==}
+    dependencies:
+      '@vue/runtime-core': 3.2.47
+      '@vue/shared': 3.2.47
+      csstype: 2.6.21
+    dev: false
 
   /@vue/server-renderer@3.2.45(vue@3.2.45):
     resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@@ -3879,9 +3978,25 @@ packages:
       '@vue/compiler-ssr': 3.2.45
       '@vue/shared': 3.2.45
       vue: 3.2.45
+    dev: true
+
+  /@vue/server-renderer@3.2.47(vue@3.2.47):
+    resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==}
+    peerDependencies:
+      vue: 3.2.47
+    dependencies:
+      '@vue/compiler-ssr': 3.2.47
+      '@vue/shared': 3.2.47
+      vue: 3.2.47
+    dev: false
 
   /@vue/shared@3.2.45:
     resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
+    dev: true
+
+  /@vue/shared@3.2.47:
+    resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==}
+    dev: false
 
   /@webassemblyjs/ast@1.11.1:
     resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@@ -7439,14 +7554,14 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
-  /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
+  /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.47):
     resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
     peerDependencies:
       focus-trap: ^7.0.0
       vue: ^3.0.0
     dependencies:
       focus-trap: 7.2.0
-      vue: 3.2.45
+      vue: 3.2.47
     dev: false
 
   /focus-trap@7.2.0:
@@ -7606,7 +7721,7 @@ packages:
     resolution: {integrity: sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==}
     engines: {node: '>= 0.10'}
     dependencies:
-      graceful-fs: 4.2.11
+      graceful-fs: 4.2.10
       through2: 2.0.5
     dev: true
 
@@ -9695,7 +9810,7 @@ packages:
   /jsonfile@4.0.0:
     resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
     optionalDependencies:
-      graceful-fs: 4.2.11
+      graceful-fs: 4.2.10
 
   /jsonfile@5.0.0:
     resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==}
@@ -9709,7 +9824,7 @@ packages:
     dependencies:
       universalify: 2.0.0
     optionalDependencies:
-      graceful-fs: 4.2.11
+      graceful-fs: 4.2.10
     dev: true
 
   /jsonld@6.0.0:
@@ -10832,6 +10947,12 @@ packages:
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
+  /nanoid@3.3.6:
+    resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+    dev: false
+
   /nanomatch@1.2.13:
     resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
     engines: {node: '>=0.10.0'}
@@ -11953,6 +12074,15 @@ packages:
       picocolors: 1.0.0
       source-map-js: 1.0.2
 
+  /postcss@8.4.23:
+    resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==}
+    engines: {node: ^10 || ^12 || >=14}
+    dependencies:
+      nanoid: 3.3.6
+      picocolors: 1.0.0
+      source-map-js: 1.0.2
+    dev: false
+
   /postgres-array@2.0.0:
     resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
     engines: {node: '>=4'}
@@ -14675,7 +14805,7 @@ packages:
     dependencies:
       append-buffer: 1.0.2
       convert-source-map: 1.9.0
-      graceful-fs: 4.2.11
+      graceful-fs: 4.2.10
       normalize-path: 2.1.1
       now-and-later: 2.0.1
       remove-bom-buffer: 3.0.0
@@ -14788,6 +14918,17 @@ packages:
       '@vue/runtime-dom': 3.2.45
       '@vue/server-renderer': 3.2.45(vue@3.2.45)
       '@vue/shared': 3.2.45
+    dev: true
+
+  /vue@3.2.47:
+    resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==}
+    dependencies:
+      '@vue/compiler-dom': 3.2.47
+      '@vue/compiler-sfc': 3.2.47
+      '@vue/runtime-dom': 3.2.47
+      '@vue/server-renderer': 3.2.47(vue@3.2.47)
+      '@vue/shared': 3.2.47
+    dev: false
 
   /vuedraggable@4.1.0(vue@3.2.45):
     resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
@@ -15402,10 +15543,10 @@ packages:
     resolution: {integrity: sha512-+MLeeUcLTlnzVo5xDn9+LVN9oX4esvgZ7qfZczBN+YVUvZBafIrPPVyG2WdjMWU2Qkb2ZAh2M8lpqf1wIoGqJQ==}
     dev: false
 
-  github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c:
-    resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/0380d12c8e736788ea7f4e6e985175521ea7b23c}
+  github.com/misskey-dev/browser-image-resizer/56f504427ad7f6500e141a6d9f3aee42023d7f3e:
+    resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/56f504427ad7f6500e141a6d9f3aee42023d7f3e}
     name: browser-image-resizer
-    version: 2.2.1-misskey.3
+    version: 2.2.1-misskey.4
     dev: true
 
   github.com/sampotts/plyr/d434c9af16e641400aaee93188594208d88f2658:

From 1eb19b698847cf7e88fe15291bec982e997089d1 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 13:26:51 -0700
Subject: [PATCH 47/48] fix(ap): Use unique identifier for each follow request

Closes #9677

Co-authored-by: GitHub <hutchisr>
---
 packages/backend/src/server/activitypub.ts    | 47 ++++++++++++++++++-
 .../src/services/following/requests/create.ts |  3 +-
 2 files changed, 47 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index 29ac726ef..d47e6f300 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -10,7 +10,7 @@ import { renderPerson } from "@/remote/activitypub/renderer/person.js";
 import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
 import { inbox as processInbox } from "@/queue/index.js";
 import { isSelfHost, toPuny } from "@/misc/convert-host.js";
-import { Notes, Users, Emojis, NoteReactions } from "@/models/index.js";
+import { Notes, Users, Emojis, NoteReactions, FollowRequests } from "@/models/index.js";
 import type { ILocalUser, User } from "@/models/entities/user.js";
 import { renderLike } from "@/remote/activitypub/renderer/like.js";
 import { getUserKeypair } from "@/misc/keypair-store.js";
@@ -330,7 +330,7 @@ router.get("/likes/:like", async (ctx) => {
 });
 
 // follow
-router.get("/follows/:follower/:followee", async (ctx) => {
+router.get("/follows/:follower/:followee", async (ctx: Router.RouterContext) => {
 	const verify = await checkFetch(ctx.req);
 	if (verify !== 200) {
 		ctx.status = verify;
@@ -365,4 +365,47 @@ router.get("/follows/:follower/:followee", async (ctx) => {
 	setResponseType(ctx);
 });
 
+// follow request
+router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
+	const verify = await checkFetch(ctx.req);
+	if (verify !== 200) {
+		ctx.status = verify;
+		return;
+	}
+
+	const followRequest = await FollowRequests.findOneBy({
+		id: ctx.params.followRequestId,
+	});
+
+	if (followRequest == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const [follower, followee] = await Promise.all([
+		Users.findOneBy({
+			id: followRequest.followerId,
+			host: IsNull(),
+		}),
+		Users.findOneBy({
+			id: followRequest.followeeId,
+			host: Not(IsNull()),
+		}),
+	]);
+
+	if (follower == null || followee == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
+	} else {
+		ctx.set("Cache-Control", "public, max-age=180");
+	}
+	ctx.body = renderActivity(renderFollow(follower, followee));
+	setResponseType(ctx);
+});
+
 export default router;
diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts
index 8b2e86ab5..27f9144d0 100644
--- a/packages/backend/src/services/following/requests/create.ts
+++ b/packages/backend/src/services/following/requests/create.ts
@@ -6,6 +6,7 @@ import type { User } from "@/models/entities/user.js";
 import { Blockings, FollowRequests, Users } from "@/models/index.js";
 import { genId } from "@/misc/gen-id.js";
 import { createNotification } from "../../create-notification.js";
+import config from "@/config/index.js";
 
 export default async function (
 	follower: {
@@ -79,7 +80,7 @@ export default async function (
 	}
 
 	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		const content = renderActivity(renderFollow(follower, followee));
+		const content = renderActivity(renderFollow(follower, followee, requestId ?? `${config.url}/follows/${followRequest.id}`));
 		deliver(follower, content, followee.inbox);
 	}
 }

From 1fb5635a4d299da01d2b44df2f5193c637e26e99 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sun, 30 Apr 2023 13:27:27 -0700
Subject: [PATCH 48/48] chore: format

---
 packages/client/src/components/MkCwButton.vue | 10 +++---
 .../client/src/components/MkEmojiPicker.vue   |  2 +-
 .../client/src/components/MkLaunchPad.vue     |  3 +-
 .../client/src/components/MkMenu.child.vue    | 18 +++++-----
 packages/client/src/components/MkMenu.vue     | 35 +++++++++++++------
 packages/client/src/components/MkModal.vue    |  9 +++--
 .../client/src/components/MkModalWindow.vue   |  2 +-
 packages/client/src/components/MkNote.vue     |  2 +-
 .../src/components/MkSubNoteContent.vue       | 31 +++++++++++-----
 .../client/src/components/MkSuperMenu.vue     |  3 +-
 .../client/src/components/MkUserPreview.vue   | 17 +++++----
 .../src/components/global/RouterView.vue      |  2 +-
 packages/client/src/pages/admin/_header_.vue  |  4 ++-
 .../src/pages/admin/overview.moderators.vue   |  7 +++-
 packages/client/src/pages/mfm-cheat-sheet.vue | 16 ++++++---
 packages/client/src/ui/_common_/navbar.vue    |  2 +-
 16 files changed, 110 insertions(+), 53 deletions(-)

diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 1f6340510..5e59853b6 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -28,7 +28,7 @@ const emit = defineEmits<{
 	(ev: "update:modelValue", v: boolean): void;
 }>();
 
-const el = ref<HTMLElement>(); 
+const el = ref<HTMLElement>();
 
 const label = computed(() => {
 	return concat([
@@ -52,7 +52,7 @@ function focus() {
 }
 
 defineExpose({
-	focus
+	focus,
 });
 </script>
 
@@ -73,7 +73,8 @@ defineExpose({
 			}
 		}
 	}
-	&:hover > span, &:focus > span {
+	&:hover > span,
+	&:focus > span {
 		background: var(--cwFg) !important;
 		color: var(--cwBg) !important;
 	}
@@ -93,7 +94,8 @@ defineExpose({
 			border-radius: 999px;
 			box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 		}
-		&:hover, &:focus {
+		&:hover,
+		&:focus {
 			> span {
 				background: var(--panelHighlight);
 			}
diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index 88d207bab..3dd54ed84 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -174,7 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
 import { emojiCategories, instance } from "@/instance";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
-import { FocusTrap } from 'focus-trap-vue';
+import { FocusTrap } from "focus-trap-vue";
 
 const props = withDefaults(
 	defineProps<{
diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index 759c215f7..b1f42ec76 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -139,7 +139,8 @@ function close() {
 			height: 100px;
 			border-radius: 10px;
 
-			&:hover, &:focus-visible {
+			&:hover,
+			&:focus-visible {
 				color: var(--accent);
 				background: var(--accentedBg);
 				text-decoration: none;
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index e5ca9e4ee..83ae6b5a1 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -1,14 +1,14 @@
 <template>
 	<div ref="el" class="sfhdhdhr" tabindex="-1">
-			<MkMenu
-				ref="menu"
-				:items="items"
-				:align="align"
-				:width="width"
-				:as-drawer="false"
-				@close="onChildClosed"
-			/>
-		</div>
+		<MkMenu
+			ref="menu"
+			:items="items"
+			:align="align"
+			:width="width"
+			:as-drawer="false"
+			@close="onChildClosed"
+		/>
+	</div>
 </template>
 
 <script lang="ts" setup>
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 41611bf0b..e976eee4d 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -14,7 +14,9 @@
 				<template v-for="(item, i) in items2">
 					<div v-if="item === null" class="divider"></div>
 					<span v-else-if="item.type === 'label'" class="label item">
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 					</span>
 					<span
 						v-else-if="item.type === 'pending'"
@@ -48,7 +50,9 @@
 							class="avatar"
 							disableLink
 						/>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 						<span v-if="item.indicate" class="indicator"
 							><i class="ph-circle ph-fill"></i
 						></span>
@@ -75,7 +79,9 @@
 								:class="icon"
 							></i>
 						</span>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 						<span v-if="item.indicate" class="indicator"
 							><i class="ph-circle ph-fill"></i
 						></span>
@@ -89,9 +95,11 @@
 						@mouseenter.passive="onItemMouseEnter(item)"
 						@mouseleave.passive="onItemMouseLeave(item)"
 					>
-						<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
+						<MkAvatar
 							:user="item.user"
-						/>
+							class="avatar"
+							disableLink
+						/><MkUserName :user="item.user" />
 						<span v-if="item.indicate" class="indicator"
 							><i class="ph-circle ph-fill"></i
 						></span>
@@ -129,9 +137,13 @@
 								:class="icon"
 							></i>
 						</span>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 						<span class="caret"
-							><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i
+							><i
+								class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"
+							></i
 						></span>
 					</button>
 					<button
@@ -161,7 +173,9 @@
 							class="avatar"
 							disableLink
 						/>
-						<span :style="item.textStyle || ''">{{ item.text }}</span>
+						<span :style="item.textStyle || ''">{{
+							item.text
+						}}</span>
 						<span v-if="item.indicate" class="indicator"
 							><i class="ph-circle ph-fill"></i
 						></span>
@@ -203,7 +217,7 @@ import FormSwitch from "@/components/form/switch.vue";
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
-import { FocusTrap } from 'focus-trap-vue';
+import { FocusTrap } from "focus-trap-vue";
 
 const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
 const focusTrap = ref();
@@ -383,7 +397,8 @@ onBeforeUnmount(() => {
 			transform: translateY(0em);
 		}
 
-		&:not(:disabled):hover, &:focus-visible {
+		&:not(:disabled):hover,
+		&:focus-visible {
 			color: var(--accent);
 			text-decoration: none;
 
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 12e79f428..0a07f57e0 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -26,13 +26,16 @@
 					$style.root,
 					{
 						[$style.drawer]: type === 'drawer',
-						[$style.dialog]: type === 'dialog' || type === 'dialog:top',
+						[$style.dialog]:
+							type === 'dialog' || type === 'dialog:top',
 						[$style.popup]: type === 'popup',
 					},
 				]"
 				:style="{
 					zIndex,
-					pointerEvents: (manualShowing != null ? manualShowing : showing)
+					pointerEvents: (
+						manualShowing != null ? manualShowing : showing
+					)
 						? 'auto'
 						: 'none',
 					'--transformOrigin': transformOrigin,
@@ -76,7 +79,7 @@ import * as os from "@/os";
 import { isTouchUsing } from "@/scripts/touch";
 import { defaultStore } from "@/store";
 import { deviceKind } from "@/scripts/device-kind";
-import { FocusTrap } from 'focus-trap-vue';
+import { FocusTrap } from "focus-trap-vue";
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === "BODY") return null;
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 017bfae8c..ea5a9aca0 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -60,7 +60,7 @@
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted } from "vue";
-import { FocusTrap } from 'focus-trap-vue';
+import { FocusTrap } from "focus-trap-vue";
 import MkModal from "./MkModal.vue";
 
 const props = withDefaults(
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index 5d9c40d38..7e3fd6be5 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -279,7 +279,7 @@ const isRenote =
 	note.poll == null;
 
 const el = ref<HTMLElement>();
-const footerEl = ref<HTMLElement>(); 
+const footerEl = ref<HTMLElement>();
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 3d6f97e8b..fc587379c 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -35,10 +35,19 @@
 			class="content"
 			:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
 		>
-			<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" />
-			<div 
+			<XCwButton
+				ref="cwButton"
+				v-if="note.cw && !showContent"
+				v-model="showContent"
+				:note="note"
+				v-on:keydown="focusFooter"
+			/>
+			<div
 				class="body"
-				v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
+				v-bind="{
+					'aria-label': !showContent ? '' : null,
+					tabindex: !showContent ? '-1' : null,
+				}"
 			>
 				<span v-if="note.deletedAt" style="opacity: 0.5"
 					>({{ i18n.ts.deleted }})</span
@@ -106,14 +115,21 @@
 					v-on:focus="cwButton?.focus()"
 				></div>
 			</div>
-			<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton>
-			<XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" />
+			<XShowMoreButton
+				v-if="isLong"
+				v-model="collapsed"
+			></XShowMoreButton>
+			<XCwButton
+				v-if="note.cw && showContent"
+				v-model="showContent"
+				:note="note"
+			/>
 		</div>
 	</div>
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue"; 
+import { ref } from "vue";
 import * as misskey from "calckey-js";
 import * as mfm from "mfm-js";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
@@ -137,7 +153,7 @@ const emit = defineEmits<{
 	(ev: "focusfooter"): void;
 }>();
 
-const cwButton = ref<HTMLElement>(); 
+const cwButton = ref<HTMLElement>();
 const isLong =
 	!props.detailedView &&
 	props.note.cw == null &&
@@ -150,7 +166,6 @@ const urls = props.note.text
 
 let showContent = $ref(false);
 
-
 function focusFooter(ev) {
 	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
 		emit("focusfooter");
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index 83c667070..d66f838d1 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -96,7 +96,8 @@ export default defineComponent({
 				font-size: 0.9em;
 				margin-bottom: 0.3rem;
 
-				&:hover, &:focus-visible {
+				&:hover,
+				&:focus-visible {
 					text-decoration: none;
 					background: var(--panelHighlight);
 				}
diff --git a/packages/client/src/components/MkUserPreview.vue b/packages/client/src/components/MkUserPreview.vue
index 1e6db1442..a8a6bb90a 100644
--- a/packages/client/src/components/MkUserPreview.vue
+++ b/packages/client/src/components/MkUserPreview.vue
@@ -46,7 +46,10 @@
 					/></MkA>
 					<p class="username"><MkAcct :user="user" /></p>
 				</div>
-				<div class="description" :class="{ collapsed: isLong && collapsed }">
+				<div
+					class="description"
+					:class="{ collapsed: isLong && collapsed }"
+				>
 					<Mfm
 						v-if="user.description"
 						:text="user.description"
@@ -149,14 +152,15 @@ let user = $ref<misskey.entities.UserDetailed | null>(null);
 let top = $ref(0);
 let left = $ref(0);
 
-
 let isLong = $ref(false);
 let collapsed = $ref(!isLong);
 
 onMounted(() => {
 	if (typeof props.q === "object") {
 		user = props.q;
-		isLong = (user.description.split("\n").length > 9 || user.description.length > 400);
+		isLong =
+			user.description.split("\n").length > 9 ||
+			user.description.length > 400;
 	} else {
 		const query = props.q.startsWith("@")
 			? Acct.parse(props.q.substr(1))
@@ -165,10 +169,11 @@ onMounted(() => {
 		os.api("users/show", query).then((res) => {
 			if (!props.showing) return;
 			user = res;
-			isLong = (user.description.split("\n").length > 9 || user.description.length > 400);
+			isLong =
+				user.description.split("\n").length > 9 ||
+				user.description.length > 400;
 		});
 	}
-	
 
 	const rect = props.source.getBoundingClientRect();
 	const x =
@@ -313,7 +318,7 @@ onMounted(() => {
 
 		> .fields {
 			padding: 0 16px;
-			font-size: .8em;
+			font-size: 0.8em;
 			margin-top: 1em;
 
 			> .field {
diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index 437b7c53e..0fa244f8f 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -7,7 +7,7 @@
 				v-bind="Object.fromEntries(currentPageProps)"
 				tabindex="-1"
 				v-focus
-				style="outline: none;"
+				style="outline: none"
 			/>
 
 			<template #fallback>
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index bf070e269..a1a8c979b 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -313,7 +313,9 @@ onUnmounted(() => {
 			font-weight: normal;
 			opacity: 0.7;
 
-			&:hover, &:focus-visible, &.active {
+			&:hover,
+			&:focus-visible,
+			&.active {
 				opacity: 1;
 			}
 
diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue
index db953b890..a29cc20b9 100644
--- a/packages/client/src/pages/admin/overview.moderators.vue
+++ b/packages/client/src/pages/admin/overview.moderators.vue
@@ -12,7 +12,12 @@
 					class="user"
 					:to="`/user-info/${user.id}`"
 				>
-					<MkAvatar :user="user" class="avatar" indicator disableLink />
+					<MkAvatar
+						:user="user"
+						class="avatar"
+						indicator
+						disableLink
+					/>
 				</MkA>
 			</div>
 		</Transition>
diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index 71eba67c5..754f19e93 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -441,7 +441,9 @@ let preview_blockCode = $ref(
 let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
 let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
 let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
-let preview_search = $ref(`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`);
+let preview_search = $ref(
+	`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`
+);
 let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");
 let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
 let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");
@@ -463,9 +465,15 @@ let preview_x4 = $ref("$[x4 🍮]");
 let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
 let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
 let preview_sparkle = $ref("$[sparkle 🍮]");
-let preview_rotate = $ref("$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]");
-let preview_position = $ref("$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]");
-let preview_scale = $ref("$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]");
+let preview_rotate = $ref(
+	"$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]"
+);
+let preview_position = $ref(
+	"$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]"
+);
+let preview_scale = $ref(
+	"$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]"
+);
 let preview_fg = $ref("$[fg.color=ff0000 Text color]");
 let preview_bg = $ref("$[bg.color=ff0000 Background color]");
 let preview_plain = $ref(
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 20c177f37..14e1f1cd1 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -440,7 +440,7 @@ function more(ev: MouseEvent) {
 						color: var(--navActive);
 					}
 
-					&:hover, 
+					&:hover,
 					&:focus-within,
 					&.active {
 						color: var(--accent);