From f9a72e1ea6cd126a288368cf98f8c90829d47b97 Mon Sep 17 00:00:00 2001
From: Kaity A <kaity@theallans.com.au>
Date: Sun, 30 Apr 2023 11:09:51 +1000
Subject: [PATCH 1/8] Add Libre Translate support

---
 .../migration/1682777547198-LibreTranslate.js | 23 ++++++++++
 packages/backend/src/config/types.ts          |  5 +++
 packages/backend/src/models/entities/meta.ts  | 12 ++++++
 .../api/endpoints/admin/accounts/hosted.ts    | 11 +++++
 .../src/server/api/endpoints/admin/meta.ts    |  5 ++-
 .../server/api/endpoints/admin/update-meta.ts | 18 ++++++++
 .../backend/src/server/api/endpoints/meta.ts  |  3 +-
 .../server/api/endpoints/notes/translate.ts   | 43 ++++++++++++++++++-
 packages/client/src/pages/admin/settings.vue  | 34 +++++++++++++++
 9 files changed, 150 insertions(+), 4 deletions(-)
 create mode 100644 packages/backend/migration/1682777547198-LibreTranslate.js

diff --git a/packages/backend/migration/1682777547198-LibreTranslate.js b/packages/backend/migration/1682777547198-LibreTranslate.js
new file mode 100644
index 000000000..dbaf483e6
--- /dev/null
+++ b/packages/backend/migration/1682777547198-LibreTranslate.js
@@ -0,0 +1,23 @@
+export class LibreTranslate1682777547198 {
+	name = "LibreTranslate1682777547198";
+
+	async up(queryRunner) {
+		await queryRunner.query(`
+				ALTER TABLE "meta"
+				ADD "libreTranslateApiUrl" character varying(512)
+		`);
+		await queryRunner.query(`
+				ALTER TABLE "meta"
+				ADD "libreTranslateApiKey" character varying(128)
+		`);
+	}
+
+	async down(queryRunner) {
+		await queryRunner.query(`
+				ALTER TABLE "meta" DROP COLUMN "libreTranslateApiKey"
+		`);
+		await queryRunner.query(`
+				ALTER TABLE "meta" DROP COLUMN "libreTranslateApiUrl"
+		`);
+	}
+}
diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts
index 4f367debe..0cd8c02ad 100644
--- a/packages/backend/src/config/types.ts
+++ b/packages/backend/src/config/types.ts
@@ -89,6 +89,11 @@ export type Source = {
 		authKey?: string;
 		isPro?: boolean;
 	};
+	libreTranslate: {
+		managed?: boolean;
+		apiUrl?: string;
+		apiKey?: string;
+	};
 	email: {
 		managed?: boolean;
 		address?: string;
diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts
index 26a7c9c19..2f77796c4 100644
--- a/packages/backend/src/models/entities/meta.ts
+++ b/packages/backend/src/models/entities/meta.ts
@@ -386,6 +386,18 @@ export class Meta {
 	})
 	public deeplIsPro: boolean;
 
+	@Column('varchar', {
+		length: 512,
+		nullable: true,
+	})
+	public libreTranslateApiUrl: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true,
+	})
+	public libreTranslateApiKey: string | null;
+
 	@Column('varchar', {
 		length: 512,
 		nullable: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
index 15ad1f9a1..a7b6e95c2 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts
@@ -30,6 +30,17 @@ export default define(meta, paramDef, async (ps, me) => {
 				set.deeplIsPro = config.deepl.isPro;
 			}
 		}
+		if (
+			config.libreTranslate.managed != null &&
+			config.libreTranslate.managed === true
+		) {
+			if (typeof config.libreTranslate.apiUrl === "string") {
+				set.libreTranslateApiUrl = config.libreTranslate.apiUrl;
+			}
+			if (typeof config.libreTranslate.apiKey === "string") {
+				set.libreTranslateApiKey = config.libreTranslate.apiKey;
+			}
+		}
 		if (config.email.managed != null && config.email.managed === true) {
 			set.enableEmail = true;
 			if (typeof config.email.address === "string") {
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index c8c639f50..f0ac57892 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -512,7 +512,8 @@ export default define(meta, paramDef, async (ps, me) => {
 		enableGithubIntegration: instance.enableGithubIntegration,
 		enableDiscordIntegration: instance.enableDiscordIntegration,
 		enableServiceWorker: instance.enableServiceWorker,
-		translatorAvailable: instance.deeplAuthKey != null,
+		translatorAvailable:
+			instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
 		pinnedPages: instance.pinnedPages,
 		pinnedClipId: instance.pinnedClipId,
 		cacheRemoteFiles: instance.cacheRemoteFiles,
@@ -564,6 +565,8 @@ export default define(meta, paramDef, async (ps, me) => {
 		objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
 		deeplAuthKey: instance.deeplAuthKey,
 		deeplIsPro: instance.deeplIsPro,
+		libreTranslateApiUrl: instance.libreTranslateApiUrl,
+		libreTranslateApiKey: instance.libreTranslateApiKey,
 		enableIpLogging: instance.enableIpLogging,
 		enableActiveEmailValidation: instance.enableActiveEmailValidation,
 	};
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index f7e79b64b..a23000732 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -124,6 +124,8 @@ export const paramDef = {
 		summalyProxy: { type: "string", nullable: true },
 		deeplAuthKey: { type: "string", nullable: true },
 		deeplIsPro: { type: "boolean" },
+		libreTranslateApiUrl: { type: "string", nullable: true },
+		libreTranslateApiKey: { type: "string", nullable: true },
 		enableTwitterIntegration: { type: "boolean" },
 		twitterConsumerKey: { type: "string", nullable: true },
 		twitterConsumerSecret: { type: "string", nullable: true },
@@ -515,6 +517,22 @@ export default define(meta, paramDef, async (ps, me) => {
 		set.deeplIsPro = ps.deeplIsPro;
 	}
 
+	if (ps.libreTranslateApiUrl !== undefined) {
+		if (ps.libreTranslateApiUrl === "") {
+			set.libreTranslateApiUrl = null;
+		} else {
+			set.libreTranslateApiUrl = ps.libreTranslateApiUrl;
+		}
+	}
+
+	if (ps.libreTranslateApiKey !== undefined) {
+		if (ps.libreTranslateApiKey === "") {
+			set.libreTranslateApiKey = null;
+		} else {
+			set.libreTranslateApiKey = ps.libreTranslateApiKey;
+		}
+	}
+
 	if (ps.enableIpLogging !== undefined) {
 		set.enableIpLogging = ps.enableIpLogging;
 	}
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 4dc1c941e..23989750f 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -482,7 +482,8 @@ export default define(meta, paramDef, async (ps, me) => {
 
 		enableServiceWorker: instance.enableServiceWorker,
 
-		translatorAvailable: instance.deeplAuthKey != null,
+		translatorAvailable:
+			instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
 		defaultReaction: instance.defaultReaction,
 
 		...(ps.detail
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index c6415ceef..d86fc12a2 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -51,15 +51,54 @@ export default define(meta, paramDef, async (ps, user) => {
 
 	const instance = await fetchMeta();
 
-	if (instance.deeplAuthKey == null) {
+	if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
 		return 204; // TODO: 良い感じのエラー返す
 	}
 
 	let targetLang = ps.targetLang;
 	if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
 
+	if (instance.libreTranslateApiUrl != null) {
+		const jsonBody = {
+			q: note.text,
+			source: "auto",
+			target: targetLang,
+			format: "text",
+			api_key: instance.libreTranslateApiKey ?? "",
+		};
+
+		const url = new URL(instance.libreTranslateApiUrl);
+		if (url.pathname.endsWith("/")) {
+			url.pathname = url.pathname.slice(0, -1);
+		}
+		if (!url.pathname.endsWith("/translate")) {
+			url.pathname += "/translate";
+		}
+		const res = await fetch(url.toString(), {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/json",
+			},
+			body: JSON.stringify(jsonBody),
+			agent: getAgentByUrl,
+		});
+
+		const json = (await res.json()) as {
+			detectedLanguage?: {
+				confidence: number;
+				language: string;
+			};
+			translatedText: string;
+		};
+
+		return {
+			sourceLang: json.detectedLanguage?.language,
+			text: json.translatedText,
+		};
+	}
+
 	const params = new URLSearchParams();
-	params.append("auth_key", instance.deeplAuthKey);
+	params.append("auth_key", instance.deeplAuthKey ?? "");
 	params.append("text", note.text);
 	params.append("target_lang", targetLang);
 
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index 5349df805..feedaff6d 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -371,6 +371,34 @@
 								<template #label>Pro account</template>
 							</FormSwitch>
 						</FormSection>
+
+						<FormSection>
+							<template #label>Libre Translate</template>
+
+							<FormInput
+								v-model="libreTranslateApiUrl"
+								class="_formBlock"
+							>
+								<template #prefix
+									><i class="ph-link ph-bold ph-lg"></i
+								></template>
+								<template #label
+									>Libre Translate API URL</template
+								>
+							</FormInput>
+
+							<FormInput
+								v-model="libreTranslateApiKey"
+								class="_formBlock"
+							>
+								<template #prefix
+									><i class="ph-key ph-bold ph-lg"></i
+								></template>
+								<template #label
+									>Libre Translate API Key</template
+								>
+							</FormInput>
+						</FormSection>
 					</div>
 				</FormSuspense>
 			</MkSpacer>
@@ -422,6 +450,8 @@ let swPublicKey: any = $ref(null);
 let swPrivateKey: any = $ref(null);
 let deeplAuthKey: string = $ref("");
 let deeplIsPro: boolean = $ref(false);
+let libreTranslateApiUrl: string = $ref("");
+let libreTranslateApiKey: string = $ref("");
 let defaultReaction: string = $ref("");
 let defaultReactionCustom: string = $ref("");
 
@@ -456,6 +486,8 @@ async function init() {
 	swPrivateKey = meta.swPrivateKey;
 	deeplAuthKey = meta.deeplAuthKey;
 	deeplIsPro = meta.deeplIsPro;
+	libreTranslateApiUrl = meta.libreTranslateApiUrl;
+	libreTranslateApiKey = meta.libreTranslateApiKey;
 	defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
 		? meta.defaultReaction
 		: "custom";
@@ -498,6 +530,8 @@ function save() {
 		swPrivateKey,
 		deeplAuthKey,
 		deeplIsPro,
+		libreTranslateApiUrl,
+		libreTranslateApiKey,
 		defaultReaction,
 	}).then(() => {
 		fetchInstance();

From 0f4e88cf53ed8dd07da0526b772096ae4919f7d4 Mon Sep 17 00:00:00 2001
From: Kaitlyn Allan <kaitlyn.allan@enlabs.cloud>
Date: Sat, 29 Apr 2023 21:05:33 +1000
Subject: [PATCH 2/8] Remove eslint from calckey-js (use rome)

---
 packages/calckey-js/.eslintignore |  7 ----
 packages/calckey-js/.eslintrc.js  | 65 -------------------------------
 packages/calckey-js/package.json  |  3 +-
 3 files changed, 1 insertion(+), 74 deletions(-)
 delete mode 100644 packages/calckey-js/.eslintignore
 delete mode 100644 packages/calckey-js/.eslintrc.js

diff --git a/packages/calckey-js/.eslintignore b/packages/calckey-js/.eslintignore
deleted file mode 100644
index f22128f04..000000000
--- a/packages/calckey-js/.eslintignore
+++ /dev/null
@@ -1,7 +0,0 @@
-node_modules
-/built
-/coverage
-/.eslintrc.js
-/jest.config.ts
-/test
-/test-d
diff --git a/packages/calckey-js/.eslintrc.js b/packages/calckey-js/.eslintrc.js
deleted file mode 100644
index 164cf1fbe..000000000
--- a/packages/calckey-js/.eslintrc.js
+++ /dev/null
@@ -1,65 +0,0 @@
-module.exports = {
-	root: true,
-	parser: "@typescript-eslint/parser",
-	parserOptions: {
-		tsconfigRootDir: __dirname,
-		project: ["./tsconfig.json"],
-	},
-	plugins: ["@typescript-eslint"],
-	extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
-	rules: {
-		indent: [
-			"error",
-			"tab",
-			{
-				SwitchCase: 1,
-				MemberExpression: "off",
-				flatTernaryExpressions: true,
-				ArrayExpression: "first",
-				ObjectExpression: "first",
-			},
-		],
-		"eol-last": ["error", "always"],
-		semi: ["error", "always"],
-		quotes: ["error", "single"],
-		"comma-dangle": ["error", "always-multiline"],
-		"keyword-spacing": [
-			"error",
-			{
-				before: true,
-				after: true,
-			},
-		],
-		"key-spacing": [
-			"error",
-			{
-				beforeColon: false,
-				afterColon: true,
-			},
-		],
-		"space-infix-ops": ["error"],
-		"space-before-blocks": ["error", "always"],
-		"object-curly-spacing": ["error", "always"],
-		"nonblock-statement-body-position": ["error", "beside"],
-		eqeqeq: ["error", "always", { null: "ignore" }],
-		"no-multiple-empty-lines": ["error", { max: 1 }],
-		"no-multi-spaces": ["error"],
-		"no-var": ["error"],
-		"prefer-arrow-callback": ["error"],
-		"no-throw-literal": ["error"],
-		"no-param-reassign": ["warn"],
-		"no-constant-condition": ["warn"],
-		"no-empty-pattern": ["warn"],
-		"@typescript-eslint/no-unnecessary-condition": ["error"],
-		"@typescript-eslint/no-inferrable-types": ["warn"],
-		"@typescript-eslint/no-non-null-assertion": ["warn"],
-		"@typescript-eslint/explicit-function-return-type": ["warn"],
-		"@typescript-eslint/no-misused-promises": [
-			"error",
-			{
-				checksVoidReturn: false,
-			},
-		],
-		"@typescript-eslint/consistent-type-imports": "error",
-	},
-};
diff --git a/packages/calckey-js/package.json b/packages/calckey-js/package.json
index d68f24175..598dd1cdb 100644
--- a/packages/calckey-js/package.json
+++ b/packages/calckey-js/package.json
@@ -9,9 +9,8 @@
 		"tsd": "tsd",
 		"api": "pnpm api-extractor run --local --verbose",
 		"api-prod": "pnpm api-extractor run --verbose",
-		"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
 		"typecheck": "tsc --noEmit",
-		"lint": "pnpm typecheck && pnpm eslint",
+		"lint": "pnpm typecheck && pnpm rome check \"src/*.ts\"",
 		"jest": "jest --coverage --detectOpenHandles",
 		"test": "pnpm jest && pnpm tsd"
 	},

From 1051da8fbf84be7abdc9b468d0e762278e1c4d88 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 19:14:36 -0700
Subject: [PATCH 3/8] Revert "keyboard accessibility (#9725)"

This reverts commit c1d5922acbe7060c0ea779ccf314e9f0e6b91bb3.
---
 package.json                                  |   2 -
 packages/client/src/components/MkButton.vue   |   3 +-
 packages/client/src/components/MkCwButton.vue |  18 +-
 .../src/components/MkDriveFileThumbnail.vue   |   7 +-
 .../client/src/components/MkEmojiPicker.vue   | 278 +++++++------
 .../client/src/components/MkLaunchPad.vue     |   2 +-
 .../client/src/components/MkMediaImage.vue    |   4 -
 .../client/src/components/MkMenu.child.vue    |  21 +-
 packages/client/src/components/MkMenu.vue     | 382 +++++++++---------
 packages/client/src/components/MkModal.vue    |  86 ++--
 .../src/components/MkModalPageWindow.vue      |   1 -
 .../client/src/components/MkModalWindow.vue   |  95 +++--
 packages/client/src/components/MkNote.vue     |   8 +-
 .../client/src/components/MkNotePreview.vue   |   2 +-
 packages/client/src/components/MkNoteSub.vue  |   4 +-
 .../client/src/components/MkPopupMenu.vue     |   2 -
 .../src/components/MkPostFormAttaches.vue     |   1 +
 .../src/components/MkSubNoteContent.vue       |  27 +-
 .../client/src/components/MkSuperMenu.vue     |   5 +-
 .../src/components/MkUserSelectDialog.vue     |   2 -
 .../client/src/components/MkUsersTooltip.vue  |   2 +-
 packages/client/src/components/MkWidgets.vue  |   2 +-
 .../client/src/components/form/folder.vue     |   4 +-
 packages/client/src/components/form/radio.vue |   3 -
 .../client/src/components/form/switch.vue     |   3 -
 .../src/components/global/MkPageHeader.vue    |   2 -
 .../src/components/global/RouterView.vue      |   3 -
 packages/client/src/directives/focus.ts       |   3 -
 packages/client/src/directives/index.ts       |   2 -
 packages/client/src/directives/tooltip.ts     |  33 +-
 packages/client/src/pages/admin/_header_.vue  |   6 +-
 .../src/pages/admin/overview.moderators.vue   |   2 +-
 packages/client/src/pages/follow-requests.vue |   1 -
 .../client/src/pages/settings/accounts.vue    |   8 +-
 packages/client/src/style.scss                |   4 +
 .../src/ui/_common_/navbar-for-mobile.vue     |   1 -
 packages/client/src/ui/_common_/navbar.vue    |  21 +-
 packages/client/src/ui/classic.header.vue     |   1 -
 packages/client/src/ui/classic.sidebar.vue    |   4 +-
 packages/client/src/ui/classic.vue            |   2 -
 pnpm-lock.yaml                                |  41 +-
 41 files changed, 495 insertions(+), 603 deletions(-)
 delete mode 100644 packages/client/src/directives/focus.ts

diff --git a/package.json b/package.json
index 51556dea5..80942ff54 100644
--- a/package.json
+++ b/package.json
@@ -40,8 +40,6 @@
 		"@bull-board/ui": "^4.10.2",
 		"@napi-rs/cli": "^2.15.0",
 		"@tensorflow/tfjs": "^3.21.0",
-		"focus-trap": "^7.2.0",
-		"focus-trap-vue": "^4.0.1",
 		"js-yaml": "4.1.0",
 		"seedrandom": "^3.0.5"
 	},
diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue
index feac281d9..5f1a5bdb7 100644
--- a/packages/client/src/components/MkButton.vue
+++ b/packages/client/src/components/MkButton.vue
@@ -195,7 +195,8 @@ function onMousedown(evt: MouseEvent): void {
 	}
 
 	&:focus-visible {
-		outline: auto;
+		outline: solid 2px var(--focus);
+		outline-offset: 2px;
 	}
 
 	&.inline {
diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index 1f6340510..659cb1fbb 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -1,6 +1,5 @@
 <template>
 	<button
-		ref="el"
 		class="_button"
 		:class="{ showLess: modelValue, fade: !modelValue }"
 		@click.stop="toggle"
@@ -13,7 +12,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, ref } from "vue";
+import { computed } from "vue";
 import { length } from "stringz";
 import * as misskey from "calckey-js";
 import { concat } from "@/scripts/array";
@@ -28,8 +27,6 @@ const emit = defineEmits<{
 	(ev: "update:modelValue", v: boolean): void;
 }>();
 
-const el = ref<HTMLElement>(); 
-
 const label = computed(() => {
 	return concat([
 		props.note.text
@@ -46,14 +43,6 @@ const label = computed(() => {
 const toggle = () => {
 	emit("update:modelValue", !props.modelValue);
 };
-
-function focus() {
-	el.value.focus();
-}
-
-defineExpose({
-	focus
-});
 </script>
 
 <style lang="scss" scoped>
@@ -73,7 +62,7 @@ defineExpose({
 			}
 		}
 	}
-	&:hover > span, &:focus > span {
+	&:hover > span {
 		background: var(--cwFg) !important;
 		color: var(--cwBg) !important;
 	}
@@ -84,7 +73,6 @@ defineExpose({
 		bottom: 0;
 		left: 0;
 		width: 100%;
-		z-index: 2;
 		> span {
 			display: inline-block;
 			background: var(--panel);
@@ -93,7 +81,7 @@ defineExpose({
 			border-radius: 999px;
 			box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
 		}
-		&:hover, &:focus {
+		&:hover {
 			> span {
 				background: var(--panelHighlight);
 			}
diff --git a/packages/client/src/components/MkDriveFileThumbnail.vue b/packages/client/src/components/MkDriveFileThumbnail.vue
index 48b542817..39150c10c 100644
--- a/packages/client/src/components/MkDriveFileThumbnail.vue
+++ b/packages/client/src/components/MkDriveFileThumbnail.vue
@@ -1,5 +1,5 @@
 <template>
-	<button ref="thumbnail" class="zdjebgpv">
+	<div 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>
-	</button>
+	</div>
 </template>
 
 <script lang="ts" setup>
@@ -88,9 +88,6 @@ 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/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index 88d207bab..a22006951 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -1,160 +1,157 @@
 <template>
-	<FocusTrap v-bind:active="isActive">
-		<div
-			class="omfetrab"
-			:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
-			:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
-			tabindex="-1"
-		>
-			<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">
+	<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">
 						<button
-							v-for="emoji in searchResultCustom"
-							:key="emoji.id"
+							v-for="emoji in pinned"
+							:key="emoji"
 							class="_button item"
-							:title="emoji.name"
 							tabindex="0"
 							@click="chosen(emoji, $event)"
 						>
-							<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
-							<img
+							<MkEmoji
 								class="emoji"
-								:src="
-									disableShowingAnimatedImages
-										? getStaticImageUrl(emoji.url)
-										: emoji.url
-								"
+								:emoji="emoji"
+								:normal="true"
 							/>
 						</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">
-							<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>
+				<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 class="tabs">
-				<button
-					class="_button tab"
-					:class="{ active: tab === 'index' }"
-					@click="tab = 'index'"
+			<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
 				>
-					<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'"
+			</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-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>
+		<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>
 </template>
 
 <script lang="ts" setup>
@@ -174,7 +171,6 @@ 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/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index 759c215f7..f713b4c41 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -139,7 +139,7 @@ function close() {
 			height: 100px;
 			border-radius: 10px;
 
-			&:hover, &:focus-visible {
+			&:hover {
 				color: var(--accent);
 				background: var(--accentedBg);
 				text-decoration: none;
diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue
index 3cfb0f465..882908040 100644
--- a/packages/client/src/components/MkMediaImage.vue
+++ b/packages/client/src/components/MkMediaImage.vue
@@ -138,10 +138,6 @@ watch(
 		background-position: center;
 		background-size: contain;
 		background-repeat: no-repeat;
-		box-sizing: border-box;
-		&:focus-visible {
-			border: 2px solid var(--accent);
-		}
 
 		> .gif {
 			background-color: var(--fg);
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index e5ca9e4ee..6b05ab447 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>
+	<div ref="el" class="sfhdhdhr">
+		<MkMenu
+			ref="menu"
+			:items="items"
+			:align="align"
+			:width="width"
+			:as-drawer="false"
+			@close="onChildClosed"
+		/>
+	</div>
 </template>
 
 <script lang="ts" setup>
@@ -23,6 +23,7 @@ import {
 } from "vue";
 import MkMenu from "./MkMenu.vue";
 import { MenuItem } from "@/types/menu";
+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 c71e3ac58..88c8af1c5 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -1,188 +1,191 @@
 <template>
-	<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>
-					<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"
-							disableLink
-						/>
-						<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" disableLink /><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)"
-						@click="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"
-							disableLink
-						/>
-						<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>
+	<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>
-			</div>
-			<div v-if="childMenu" class="child">
-				<XChild
-					ref="child"
-					:items="childMenu"
-					:target-element="childTarget"
-					:root-element="itemsEl"
-					showing
-					@actioned="childActioned"
-				/>
-			</div>
+				<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>
+					</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>
+				<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>
-	</FocusTrap>
+		<div v-if="childMenu" class="child">
+			<XChild
+				ref="child"
+				:items="childMenu"
+				:target-element="childTarget"
+				:root-element="itemsEl"
+				showing
+				@actioned="childActioned"
+			/>
+		</div>
+	</div>
 </template>
 
 <script lang="ts" setup>
@@ -203,7 +206,6 @@ 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"));
 
@@ -226,6 +228,12 @@ 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(
@@ -356,7 +364,8 @@ onBeforeUnmount(() => {
 		font-size: 0.9em;
 		line-height: 20px;
 		text-align: left;
-		outline: none;
+		overflow: hidden;
+		text-overflow: ellipsis;
 
 		&:before {
 			content: "";
@@ -380,7 +389,7 @@ onBeforeUnmount(() => {
 			transform: translateY(0em);
 		}
 
-		&:not(:disabled):hover, &:focus-visible {
+		&:not(:disabled):hover {
 			color: var(--accent);
 			text-decoration: none;
 
@@ -388,9 +397,6 @@ onBeforeUnmount(() => {
 				background: var(--accentedBg);
 			}
 		}
-		&:focus-visible:before {
-			outline: auto;
-		}
 
 		&.danger {
 			color: #eb6f92;
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index 12e79f428..d9cd56f95 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -14,59 +14,54 @@
 		:duration="transitionDuration"
 		appear
 		@after-leave="emit('closed')"
-		@keyup.esc="emit('click')"
 		@enter="emit('opening')"
 		@after-enter="onOpened"
 	>
-		<FocusTrap v-model:active="isActive">
+		<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,
+			}"
+		>
 			<div
-				v-show="manualShowing != null ? manualShowing : showing"
-				v-hotkey.global="keymap"
+				class="_modalBg data-cy-bg"
 				:class="[
-					$style.root,
+					$style.bg,
 					{
-						[$style.drawer]: type === 'drawer',
-						[$style.dialog]: type === 'dialog' || type === 'dialog:top',
-						[$style.popup]: type === 'popup',
+						[$style.bgTransparent]: isEnableBgTransparent,
+						'data-cy-transparent': isEnableBgTransparent,
 					},
 				]"
-				:style="{
-					zIndex,
-					pointerEvents: (manualShowing != null ? manualShowing : showing)
-						? 'auto'
-						: 'none',
-					'--transformOrigin': transformOrigin,
-				}"
-				tabindex="-1"
-				v-focus
+				:style="{ zIndex }"
+				@click="onBgClick"
+				@mousedown="onBgClick"
+				@contextmenu.prevent.stop="() => {}"
+			></div>
+			<div
+				ref="content"
+				:class="[
+					$style.content,
+					{ [$style.fixed]: fixed, top: type === 'dialog:top' },
+				]"
+				:style="{ zIndex }"
+				@click.self="onBgClick"
 			>
-				<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="[
-						$style.content,
-						{ [$style.fixed]: fixed, top: type === 'dialog:top' },
-					]"
-					:style="{ zIndex }"
-					@click.self="onBgClick"
-				>
-					<slot :max-height="maxHeight" :type="type"></slot>
-				</div>
+				<slot :max-height="maxHeight" :type="type"></slot>
 			</div>
-		</FocusTrap>
+		</div>
 	</Transition>
 </template>
 
@@ -76,7 +71,6 @@ 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;
@@ -172,7 +166,6 @@ let transitionDuration = $computed(() =>
 
 let contentClicking = false;
 
-const focusedElement = document.activeElement;
 function close(opts: { useSendAnimation?: boolean } = {}) {
 	if (opts.useSendAnimation) {
 		useSendAnime = true;
@@ -182,12 +175,10 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
 	if (props.src) props.src.style.pointerEvents = "auto";
 	showing = false;
 	emit("close");
-	focusedElement.focus();
 }
 
 function onBgClick() {
 	if (contentClicking) return;
-	focusedElement.focus();
 	emit("click");
 }
 
@@ -490,7 +481,6 @@ defineExpose({
 }
 
 .root {
-	outline: none;
 	&.dialog {
 		> .content {
 			position: fixed;
diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue
index bf4d8d0bc..361128464 100644
--- a/packages/client/src/components/MkModalPageWindow.vue
+++ b/packages/client/src/components/MkModalPageWindow.vue
@@ -158,7 +158,6 @@ function onContextmenu(ev: MouseEvent) {
 	flex-direction: column;
 	contain: content;
 	border-radius: var(--radius);
-	margin: auto;
 
 	--root-margin: 24px;
 
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index 017bfae8c..3afcff6cb 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -3,64 +3,59 @@
 		ref="modal"
 		:prefer-type="'dialog'"
 		@click="onBgClick"
-		@keyup.esc="$emit('close')"
 		@closed="$emit('closed')"
 	>
-		<FocusTrap 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"
-				tabindex="-1"
-			>
-				<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
+			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>
-		</FocusTrap>
+			<div class="body">
+				<slot :width="bodyWidth" :height="bodyHeight"></slot>
+			</div>
+		</div>
 	</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(
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index 5d9c40d38..22a7ef93f 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -84,7 +84,6 @@
 						: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 />
@@ -118,7 +117,7 @@
 						<MkTime :time="appearNote.createdAt" mode="absolute" />
 					</MkA>
 				</div>
-				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
+				<footer ref="el" class="footer" @click.stop>
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -279,7 +278,6 @@ 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>>();
@@ -300,8 +298,8 @@ const keymap = {
 	r: () => reply(true),
 	"e|a|plus": () => react(true),
 	q: () => renoteButton.value.renote(true),
-	"up|k": focusBefore,
-	"down|j": focusAfter,
+	"up|k|shift+tab": focusBefore,
+	"down|j|tab": focusAfter,
 	esc: blur,
 	"m|o": () => menu(true),
 	s: () => showContent.value !== showContent.value,
diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue
index 6fdd79dc6..9d388e71b 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" disableLink />
+		<MkAvatar class="avatar" :user="$i" />
 		<div class="main">
 			<div class="header">
 				<MkUserName :user="$i" />
diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index a0b70ff1f..f5e70891f 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -26,7 +26,6 @@
 						:note="note"
 						:parentId="appearNote.parentId"
 						:conversation="conversation"
-						@focusfooter="footerEl.focus()"
 					/>
 					<div v-if="translating || translation" class="translation">
 						<MkLoading v-if="translating" mini />
@@ -47,7 +46,7 @@
 						</div>
 					</div>
 				</div>
-				<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
+				<footer class="footer" @click.stop>
 					<XReactionsViewer
 						v-if="enableEmojiReactions"
 						ref="reactionsViewer"
@@ -213,7 +212,6 @@ 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/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index 5f1ed037b..4d52616e1 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -7,8 +7,6 @@
 		:transparent-bg="true"
 		@click="modal.close()"
 		@closed="emit('closed')"
-		tabindex="-1"
-		v-focus
 	>
 		<MkMenu
 			:items="items"
diff --git a/packages/client/src/components/MkPostFormAttaches.vue b/packages/client/src/components/MkPostFormAttaches.vue
index 7cf397e55..7c7f240e8 100644
--- a/packages/client/src/components/MkPostFormAttaches.vue
+++ b/packages/client/src/components/MkPostFormAttaches.vue
@@ -198,6 +198,7 @@ export default defineComponent({
 			height: 64px;
 			margin-right: 4px;
 			border-radius: 4px;
+			overflow: hidden;
 			cursor: move;
 
 			&:hover > .remove {
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 68439527a..a1f7cc1b9 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -35,11 +35,7 @@
 			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 
-				class="body"
-				v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }"
-			>
+			<div class="body">
 				<span v-if="note.deletedAt" style="opacity: 0.5"
 					>({{ i18n.ts.deleted }})</span
 				>
@@ -100,20 +96,15 @@
 						<XNoteSimple :note="note.renote" />
 					</div>
 				</template>
-				<div
-					v-if="note.cw && !showContent"
-					tabindex="0"
-					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" />
+			<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
 		</div>
 	</div>
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue"; 
+import {} from "vue";
 import * as misskey from "calckey-js";
 import * as mfm from "mfm-js";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
@@ -135,10 +126,8 @@ 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,13 +140,6 @@ 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>
@@ -249,9 +231,6 @@ function focusFooter(ev) {
 				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;
diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue
index 83c667070..55d6fba50 100644
--- a/packages/client/src/components/MkSuperMenu.vue
+++ b/packages/client/src/components/MkSuperMenu.vue
@@ -9,6 +9,7 @@
 						v-if="item.type === 'a'"
 						:href="item.href"
 						:target="item.target"
+						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 					>
@@ -21,6 +22,7 @@
 					</a>
 					<button
 						v-else-if="item.type === 'button'"
+						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 						:disabled="item.active"
@@ -36,6 +38,7 @@
 					<MkA
 						v-else
 						:to="item.to"
+						:tabindex="i"
 						class="_button item"
 						:class="{ danger: item.danger, active: item.active }"
 					>
@@ -96,7 +99,7 @@ export default defineComponent({
 				font-size: 0.9em;
 				margin-bottom: 0.3rem;
 
-				&:hover, &:focus-visible {
+				&:hover {
 					text-decoration: none;
 					background: var(--panelHighlight);
 				}
diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue
index 14553ca46..506f48bd4 100644
--- a/packages/client/src/components/MkUserSelectDialog.vue
+++ b/packages/client/src/components/MkUserSelectDialog.vue
@@ -46,7 +46,6 @@
 							:user="user"
 							class="avatar"
 							:show-indicator="true"
-							disableLink
 						/>
 						<div class="body">
 							<MkUserName :user="user" class="name" />
@@ -74,7 +73,6 @@
 							: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 78a4f90f2..972864d1f 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" disableLink />
+				<MkAvatar class="avatar" :user="u" />
 				<MkUserName class="name" :user="u" :nowrap="true" />
 			</div>
 			<div v-if="users.length < count" class="omitted">
diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue
index d48fc5383..07e845032 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 tabindex="-1" v-focus>
+			<header>
 				<MkSelect
 					v-model="widgetAdderSelected"
 					style="margin-bottom: var(--margin)"
diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue
index a2fde5341..8868f5784 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 }">
-		<button class="header _button" @click="toggle">
+		<div 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>
-		</button>
+		</div>
 		<KeepAlive>
 			<div v-if="openedAtLeastOnce" v-show="opened" class="body">
 				<MkSpacer :margin-min="14" :margin-max="22">
diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue
index ef644b327..493b2d010 100644
--- a/packages/client/src/components/form/radio.vue
+++ b/packages/client/src/components/form/radio.vue
@@ -66,9 +66,6 @@ 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 b1c6df4e9..efaf488a9 100644
--- a/packages/client/src/components/form/switch.vue
+++ b/packages/client/src/components/form/switch.vue
@@ -99,9 +99,6 @@ const toggle = () => {
 			border-color: var(--inputBorderHover) !important;
 		}
 	}
-	&:focus-within > .button {
-		outline: auto;
-	}
 
 	> .label {
 		margin-left: 12px;
diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue
index c78ef0c10..ad1d80ca6 100644
--- a/packages/client/src/components/global/MkPageHeader.vue
+++ b/packages/client/src/components/global/MkPageHeader.vue
@@ -19,7 +19,6 @@
 				class="avatar"
 				:user="$i"
 				:disable-preview="true"
-				disableLink
 			/>
 		</div>
 		<template v-if="metadata">
@@ -34,7 +33,6 @@
 					:user="metadata.avatar"
 					:disable-preview="true"
 					:show-indicator="true"
-					disableLink
 				/>
 				<i
 					v-else-if="metadata.icon && !narrow"
diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue
index 437b7c53e..8423ce773 100644
--- a/packages/client/src/components/global/RouterView.vue
+++ b/packages/client/src/components/global/RouterView.vue
@@ -5,9 +5,6 @@
 				:is="currentPageComponent"
 				:key="key"
 				v-bind="Object.fromEntries(currentPageProps)"
-				tabindex="-1"
-				v-focus
-				style="outline: none;"
 			/>
 
 			<template #fallback>
diff --git a/packages/client/src/directives/focus.ts b/packages/client/src/directives/focus.ts
deleted file mode 100644
index 4d34fbf1f..000000000
--- a/packages/client/src/directives/focus.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
-	mounted: (el) => el.focus()
-}
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
index 77639e2f3..0a5c32326 100644
--- a/packages/client/src/directives/index.ts
+++ b/packages/client/src/directives/index.ts
@@ -11,7 +11,6 @@ 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);
@@ -26,5 +25,4 @@ export default function (app: App) {
 	app.directive("click-anime", clickAnime);
 	app.directive("panel", panel);
 	app.directive("adaptive-border", adaptiveBorder);
-	app.directive("focus", focus);
 }
diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
index 91024a6e3..7738d14e8 100644
--- a/packages/client/src/directives/tooltip.ts
+++ b/packages/client/src/directives/tooltip.ts
@@ -76,32 +76,23 @@ 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, showTooltip,
-			{ passive: true },
-		);
-		el.addEventListener(
-			"focusin", showTooltip,
+			start,
+			() => {
+				window.clearTimeout(self.showTimer);
+				window.clearTimeout(self.hideTimer);
+				self.showTimer = window.setTimeout(self.show, delay);
+			},
 			{ passive: true },
 		);
 
 		el.addEventListener(
-			end, hideTooltip,
-			{ passive: true },
-		);
-		el.addEventListener(
-			"focusout", hideTooltip,
+			end,
+			() => {
+				window.clearTimeout(self.showTimer);
+				window.clearTimeout(self.hideTimer);
+				self.hideTimer = window.setTimeout(self.close, delay);
+			},
 			{ passive: true },
 		);
 
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
index bf070e269..69fd1bc58 100644
--- a/packages/client/src/pages/admin/_header_.vue
+++ b/packages/client/src/pages/admin/_header_.vue
@@ -313,7 +313,11 @@ onUnmounted(() => {
 			font-weight: normal;
 			opacity: 0.7;
 
-			&:hover, &:focus-visible, &.active {
+			&:hover {
+				opacity: 1;
+			}
+
+			&.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..6184cfb10 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 disableLink />
+					<MkAvatar :user="user" class="avatar" indicator />
 				</MkA>
 			</div>
 		</Transition>
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index 35279495b..2aac52163 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -23,7 +23,6 @@
 								class="avatar"
 								:user="req.follower"
 								:show-indicator="true"
-								disableLink
 							/>
 							<div class="body">
 								<div class="name">
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index 3010354b6..ec2cd2477 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
 			>
 
-			<button
+			<div
 				v-for="account in accounts"
 				:key="account.id"
 				class="_panel _button lcjjdxlm"
 				@click="menu(account, $event)"
 			>
 				<div class="avatar">
-					<MkAvatar :user="account" class="avatar" disableLink />
+					<MkAvatar :user="account" class="avatar" />
 				</div>
 				<div class="body">
 					<div class="name">
@@ -23,7 +23,7 @@
 						<MkAcct :user="account" />
 					</div>
 				</div>
-			</button>
+			</div>
 		</FormSuspense>
 	</div>
 </template>
@@ -158,8 +158,6 @@ definePageMetadata({
 .lcjjdxlm {
 	display: flex;
 	padding: 16px;
-	width: 100%;
-	text-align: unset;
 
 	> .avatar {
 		display: block;
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index 52c7b62f4..051edf6e0 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -204,6 +204,10 @@ hr {
 		pointer-events: none;
 	}
 
+	&:focus-visible {
+		outline: none;
+	}
+
 	&:disabled {
 		opacity: 0.5;
 		cursor: default;
diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue
index 39abb7c26..43c91d147 100644
--- a/packages/client/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue
@@ -18,7 +18,6 @@
 					<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 20c177f37..380f77c3c 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -18,7 +18,6 @@
 					<MkAvatar
 						:user="$i"
 						class="icon"
-						disableLink
 					/><!-- <MkAcct class="text" :user="$i"/> -->
 				</button>
 			</div>
@@ -335,7 +334,6 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
-					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -400,6 +398,8 @@ 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;
@@ -425,12 +425,9 @@ function more(ev: MouseEvent) {
 					> .text {
 						position: relative;
 						font-size: 0.9em;
-						overflow: hidden;
-						text-overflow: ellipsis;
 					}
 
-					&:hover,
-					&:focus-within {
+					&:hover {
 						text-decoration: none;
 						color: var(--navHoverFg);
 						transition: all 0.4s ease;
@@ -440,8 +437,7 @@ function more(ev: MouseEvent) {
 						color: var(--navActive);
 					}
 
-					&:hover, 
-					&:focus-within,
+					&:hover,
 					&.active {
 						color: var(--accent);
 						transition: all 0.4s ease;
@@ -532,7 +528,6 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
-					&:focus-within,
 					&.active {
 						&:before {
 							background: var(--accentLighten);
@@ -618,7 +613,6 @@ function more(ev: MouseEvent) {
 					}
 
 					&:hover,
-					&:focus-within,
 					&.active {
 						text-decoration: none;
 						color: var(--accent);
@@ -648,12 +642,5 @@ function more(ev: MouseEvent) {
 			}
 		}
 	}
-
-	.item {
-		outline: none;
-		&:focus-visible:before {
-			outline: auto;
-		}
-	}
 }
 </style>
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
index 99a0ab098..5c3e6b702 100644
--- a/packages/client/src/ui/classic.header.vue
+++ b/packages/client/src/ui/classic.header.vue
@@ -83,7 +83,6 @@
 					<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 fa72c5765..b70a3c984 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -5,7 +5,7 @@
 			class="item _button account"
 			@click="openAccountMenu"
 		>
-			<MkAvatar :user="$i" class="avatar" disableLink /><MkAcct
+			<MkAvatar :user="$i" class="avatar" /><MkAcct
 				class="text"
 				:user="$i"
 			/>
@@ -299,7 +299,6 @@ function openInstanceMenu(ev: MouseEvent) {
 				width: 46px;
 				height: 46px;
 				padding: 0;
-				margin-inline: 0 !important;
 			}
 		}
 
@@ -373,7 +372,6 @@ 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 266effd9a..a721ffd0b 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -227,8 +227,6 @@ onMounted(() => {
 }
 
 .gbhvwtnk {
-	display: flex;
-	justify-content: center;
 	$ui-font-size: 1em;
 	$widgets-hide-threshold: 1200px;
 
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 60e42e11a..9f0b4195d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,12 +19,6 @@ 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
@@ -3809,12 +3803,14 @@ 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==}
@@ -3837,12 +3833,14 @@ 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==}
@@ -3852,17 +3850,20 @@ 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==}
@@ -3870,6 +3871,7 @@ 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==}
@@ -3879,9 +3881,11 @@ 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==}
@@ -6070,6 +6074,7 @@ 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==}
@@ -6974,6 +6979,7 @@ 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==}
@@ -7439,22 +7445,6 @@ 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'}
@@ -10360,6 +10350,7 @@ 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==}
@@ -13276,6 +13267,7 @@ 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==}
@@ -13694,10 +13686,6 @@ 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'}
@@ -14788,6 +14776,7 @@ 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 5831f0a2c5ff69d5e51dac40749e5ca57d273b97 Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 19:14:47 -0700
Subject: [PATCH 4/8] hotfix

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 80942ff54..61c247ab2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "calckey",
-	"version": "13.2.0-dev40",
+	"version": "13.2.0-dev41",
 	"codename": "aqua",
 	"repository": {
 		"type": "git",

From 9c40247df21b2a8c4daf25cc7b0735fca32e9062 Mon Sep 17 00:00:00 2001
From: Laker Turner <la@laker.gay>
Date: Sat, 29 Apr 2023 10:28:49 +0000
Subject: [PATCH 5/8] chore: Translated using Weblate (English)

Currently translated at 100.0% (1735 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/en/
---
 locales/en-US.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 2feb2cd94..f9d4d23f0 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1042,7 +1042,7 @@ moveFromLabel: "Account you're moving from:"
 moveFromDescription: "This will set an alias of your old account so that you can move\
   \ from that account to this current one. Do this BEFORE moving from your older account.\
   \ Please enter the tag of the account formatted like @person@instance.com"
-migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}?\
+migrationConfirm: "Are you absolutely sure you want to migrate your account to {account}?\
   \ Once you do this, you won't be able to reverse it, and you won't be able to use\
   \ your account normally again.\nAlso, please ensure that you've set this current\
   \ account as the account you're moving from."

From bcfadc086bd523c408da31c69db24c228b8d7d51 Mon Sep 17 00:00:00 2001
From: jolupa <jolupameister@gmail.com>
Date: Sat, 29 Apr 2023 11:53:12 +0000
Subject: [PATCH 6/8] chore: Translated using Weblate (Catalan)

Currently translated at 37.1% (644 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/ca/
---
 locales/ca-ES.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 24e4b6d7d..b226d641e 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -675,3 +675,48 @@ useGlobalSetting: Fes servir els ajustos globals
 useGlobalSettingDesc: Si s'activa, es faran servir els ajustos de notificacions del
   teu compte. Si es desactiva , es poden fer configuracions individuals.
 other: Altres
+menu: Menú
+addItem: Afegeix un element
+divider: Divisor
+relays: Relés
+addRelay: Afegeix un Relé
+inboxUrl: Adreça de la safata d'entrada
+addedRelays: Relés afegits
+serviceworkerInfo: Ha de estar activat per les notificacions push.
+poll: Enquesta
+deletedNote: Article eliminat
+disablePlayer: Tancar el reproductor de vídeo
+fileIdOrUrl: ID o adreça URL del fitxer
+behavior: Comportament
+regenerateLoginTokenDescription: Regenera el token que es fa servir de manera interna
+  durant l'inici de sessió. Normalment això no és necessari. Si es torna a genera
+  el token, es tancarà la sessió a tots els dispositius.
+setMultipleBySeparatingWithSpace: Separa diferents entrades amb espais.
+reportAbuseOf: Informa sobre {name}
+sample: Exemple
+abuseReports: Informes
+reportAbuse: Informe
+reporter: Informador
+reporterOrigin: Origen d'el informador
+forwardReport: Envia l'informe a una instancia remota
+abuseReported: El teu informe ha sigut enviat. Moltes gràcies.
+reporteeOrigin: Origen de l'informe
+send: Enviar
+abuseMarkAsResolved: Marcar l'informe com a resolt
+visibility: Visibilitat
+useCw: Amaga el contingut
+enablePlayer: Obre el reproductor de vídeo
+yourAccountSuspendedDescription: Aquest compte ha sigut suspesa per no seguir els
+  termes de servei del servidor o quelcom similar. Contacte amb l'administrador si
+  vols conèixer la raó amb més detall. Si us plau no facis un compte nou.
+invisibleNote: Article ocult
+enableInfiniteScroll: Carregar més de forma automàtica
+fillAbuseReportDescription: Si us plau omple els detalls sobre aquest informe. Si
+  es sobre un article en concret, si us plau inclou l'adreça URL.
+forwardReportIsAnonymous: Com a informador a l'instància remota no es mostrarà el
+  teu compte, si no un compte anònim.
+openInNewTab: Obrir en una pestanya nova
+openInSideView: Obrir a la vista lateral
+defaultNavigationBehaviour: Navegació per defecte
+editTheseSettingsMayBreakAccount: Si edites aquestes configuracions pots fer mal bé
+  el teu compte.

From b2dc43aa01d4b809d0a869b264b24557a719461f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ville=20Lepist=C3=B6?= <ville.lepisto@protonmail.com>
Date: Sat, 29 Apr 2023 08:52:45 +0000
Subject: [PATCH 7/8] chore: Translated using Weblate (Finnish)

Currently translated at 11.7% (204 of 1735 strings)

Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/calckey/locales/fi/
---
 locales/fi.yml | 179 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 179 insertions(+)

diff --git a/locales/fi.yml b/locales/fi.yml
index c9e5e06e0..558d1afc2 100644
--- a/locales/fi.yml
+++ b/locales/fi.yml
@@ -41,3 +41,182 @@ favorite: Lisää kirjanmerkkeihin
 copyContent: Kopioi sisältö
 deleteAndEdit: Poista ja muokkaa
 copyLink: Kopioi linkki
+makeFollowManuallyApprove: Seuraajapyyntö vaatii hyväksymistä
+follow: Seuraa
+pinned: Kiinnitä profiiliin
+followRequestPending: Seuraajapyyntö odottaa
+you: Sinä
+unrenote: Peruuta buustaus
+reaction: Reaktiot
+reactionSettingDescription2: Vedä uudelleenjärjestelläksesi, napsauta poistaaksesi,
+  paina "+" lisätäksesi.
+attachCancel: Poista liite
+enterFileName: Anna tiedostonimi
+mute: Hiljennä
+unmute: Poista hiljennys
+headlineMisskey: Avoimen lähdekoodin, hajautettu sosiaalisen median alusta, joka on
+  ikuisesti ilmainen! 🚀
+monthAndDay: '{day}/{month}'
+deleteAndEditConfirm: Oletko varma, että haluat poistaa tämän lähetyksen ja muokata
+  sitä? Menetät kaikki reaktiot, buustaukset ja vastaukset lähetyksestäsi.
+addToList: Lisää listaan
+sendMessage: Lähetä viesti
+reply: Vastaa
+loadMore: Lataa enemmän
+showMore: Näytä enemmän
+receiveFollowRequest: Seuraajapyyntö vastaanotettu
+followRequestAccepted: Seuraajapyyntö hyväksytty
+mentions: Maininnat
+importAndExport: Tuo/Vie Tietosisältö
+import: Tuo
+export: Vie
+files: Tiedostot
+download: Lataa
+unfollowConfirm: Oletko varma, ettet halua seurata enää käyttäjää {name}?
+noLists: Sinulla ei ole listoja
+note: Lähetys
+notes: Lähetykset
+following: Seuraa
+createList: Luo lista
+manageLists: Hallitse listoja
+error: Virhe
+somethingHappened: On tapahtunut virhe
+retry: Yritä uudelleen
+pageLoadError: Virhe ladattaessa sivua.
+serverIsDead: Tämä palvelin ei vastaa. Yritä hetken kuluttua uudelleen.
+youShouldUpgradeClient: Nähdäksesi tämän sivun, virkistä päivittääksesi asiakasohjelmasi.
+privacy: Tietosuoja
+defaultNoteVisibility: Oletusnäkyvyys
+followRequest: Seuraajapyyntö
+followRequests: Seuraajapyynnöt
+unfollow: Poista seuraaminen
+enterEmoji: Syötä emoji
+renote: Buustaa
+renoted: Buustattu.
+cantRenote: Tätä lähetystä ei voi buustata.
+cantReRenote: Buustausta ei voi buustata.
+quote: Lainaus
+pinnedNote: Lukittu lähetys
+clickToShow: Napsauta nähdäksesi
+sensitive: Herkkää sisältöä (NSFW)
+add: Lisää
+enableEmojiReactions: Ota käyttöön emoji-reaktiot
+showEmojisInReactionNotifications: Näytä emojit reaktioilmoituksissa
+reactionSetting: Reaktiot näytettäväksi reaktiovalitsimessa
+rememberNoteVisibility: Muista lähetyksen näkyvyysasetukset
+markAsSensitive: Merkitse herkäksi sisällöksi (NSFW)
+unmarkAsSensitive: Poista merkintä herkkää sisältöä (NSFW)
+renoteMute: Hiljennä buustit
+renoteUnmute: Poista buustien hiljennys
+block: Estä
+unblock: Poista esto
+unsuspend: Poista keskeytys
+suspend: Keskeytys
+blockConfirm: Oletko varma, että haluat estää tämän tilin?
+unblockConfirm: Oletko varma, että haluat poistaa tämän tilin eston?
+selectAntenna: Valitse antenni
+selectWidget: Valitse vimpain
+editWidgets: Muokkaa vimpaimia
+editWidgetsExit: Valmis
+emoji: Emoji
+emojis: Emojit
+emojiName: Emojin nimi
+emojiUrl: Emojin URL-linkki
+cacheRemoteFiles: Taltioi etätiedostot välimuistiin
+flagAsBot: Merkitse tili botiksi
+flagAsBotDescription: Ota tämä vaihtoehto käyttöön, jos tätä tiliä ohjaa ohjelma.
+  Jos se on käytössä, se toimii lippuna muille kehittäjille, jotta estetään loputtomat
+  vuorovaikutusketjut muiden bottien kanssa ja säädetään Calckeyn sisäiset järjestelmät
+  käsittelemään tätä tiliä botina.
+flagAsCat: Oletko kissa? 🐱
+flagAsCatDescription: Saat kissan korvat ja puhut kuin kissa!
+flagSpeakAsCat: Puhu kuin kissa
+flagShowTimelineReplies: Näytä vastaukset aikajanalla
+addAccount: Lisää tili
+loginFailed: Kirjautuminen epäonnistui
+showOnRemote: Katsele etäinstanssilla
+general: Yleistä
+accountMoved: 'Käyttäjä on muuttanut uuteen tiliin:'
+wallpaper: Taustakuva
+setWallpaper: Aseta taustakuva
+searchWith: 'Etsi: {q}'
+youHaveNoLists: Sinulla ei ole listoja
+followConfirm: Oletko varma, että haluat seurata käyttäjää {name}?
+host: Isäntä
+selectUser: Valitse käyttäjä
+annotation: Kommentit
+registeredAt: Rekisteröity
+latestRequestReceivedAt: Viimeisin pyyntö vastaanotettu
+latestRequestSentAt: Viimeisin pyyntö lähetetty
+storageUsage: Tallennustilan käyttö
+charts: Kaaviot
+stopActivityDelivery: Lopeta toimintojen lähettäminen
+blockThisInstance: Estä tämä instanssi
+operations: Toiminnot
+metadata: Metatieto
+monitor: Seuranta
+jobQueue: Työjono
+cpuAndMemory: Prosessori ja muisti
+network: Verkko
+disk: Levy
+clearCachedFiles: Tyhjennä välimuisti
+clearCachedFilesConfirm: Oletko varma, että haluat tyhjentää kaikki välimuistiin tallennetut
+  etätiedostot?
+blockedInstances: Estetyt instanssit
+hiddenTags: Piilotetut asiatunnisteet
+mention: Maininta
+copyUsername: Kopioi käyttäjänimi
+searchUser: Etsi käyttäjää
+showLess: Sulje
+youGotNewFollower: seurasi sinua
+directNotes: Yksityisviestit
+driveFileDeleteConfirm: Oletko varma, että haluat poistaa tiedoston " {name}"? Lähetykset,
+  jotka sisältyvät tiedostoon, poistuvat myös.
+importRequested: Olet pyytänyt viemistä. Tämä voi viedä hetken.
+exportRequested: Olet pyytänyt tuomista. Tämä voi viedä hetken. Se lisätään asemaan
+  kun tuonti valmistuu.
+lists: Listat
+followers: Seuraajat
+followsYou: Seuraa sinua
+pageLoadErrorDescription: Tämä yleensä johtuu verkkovirheistä tai selaimen välimuistista.
+  Kokeile tyhjentämällä välimuisti ja yritä sitten hetken kuluttua uudelleen.
+enterListName: Anna listalle nimi
+withNFiles: '{n} tiedosto(t)'
+instanceInfo: Instanssin tiedot
+clearQueue: Tyhjennä jono
+suspendConfirm: Oletko varma, että haluat keskeyttää tämän tilin?
+unsuspendConfirm: Oletko varma, että haluat poistaa tämän tilin keskeytyksen?
+selectList: Valitse lista
+customEmojis: Kustomoitu Emoji
+addEmoji: Lisää
+settingGuide: Suositellut asetukset
+cacheRemoteFilesDescription: Kun tämä asetus ei ole käytössä, etätiedostot on ladattu
+  suoraan etäinstanssilta. Asetuksen poistaminen käytöstä vähentää tallennustilan
+  käyttöä, mutta lisää verkkoliikennettä kun pienoiskuvat eivät muodostu.
+flagSpeakAsCatDescription: Lähetyksesi nyanifioidaan, kun olet kissatilassa
+flagShowTimelineRepliesDescription: Näyttää käyttäjien vastaukset muiden käyttäjien
+  lähetyksiin aikajanalla, jos se on päällä.
+autoAcceptFollowed: Automaattisesti hyväksy seuraamispyynnöt käyttäjiltä, joita seuraat
+perHour: Tunnissa
+removeWallpaper: Poista taustakuva
+recipient: Vastaanottaja(t)
+federation: Federaatio
+software: Ohjelmisto
+proxyAccount: Proxy-tili
+proxyAccountDescription: Välitystili (Proxy-tili) on tili, joka toimii käyttäjien
+  etäseuraajana tietyin edellytyksin. Kun käyttäjä esimerkiksi lisää etäkäyttäjän
+  luetteloon, etäkäyttäjän toimintaa ei toimiteta instanssiin, jos yksikään paikallinen
+  käyttäjä ei seuraa kyseistä käyttäjää, joten välitystili seuraa sen sijaan.
+latestStatus: Viimeisin tila
+selectInstance: Valitse instanssi
+instances: Instanssit
+perDay: Päivässä
+version: Versio
+statistics: Tilastot
+clearQueueConfirmTitle: Oletko varma, että haluat tyhjentää jonon?
+introMisskey: Tervetuloa! Calckey on avoimen lähdekoodin, hajautettu sosiaalisen median
+  alusta, joka on ikuisesti ilmainen! 🚀
+clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jonossa, eivät
+  federoidu. Yleensä tätä toimintoa ei tarvita.
+blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
+  Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.

From 8db240756e996edcacb811102b6c8dd25c508179 Mon Sep 17 00:00:00 2001
From: Kainoa Kanter <kainoa@t1c.dev>
Date: Sat, 29 Apr 2023 08:11:18 +0000
Subject: [PATCH 8/8] chore: Translated using Weblate (Finnish)

Currently translated at 11.7% (204 of 1735 strings)

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

diff --git a/locales/fi.yml b/locales/fi.yml
index 558d1afc2..fd6c160e6 100644
--- a/locales/fi.yml
+++ b/locales/fi.yml
@@ -3,7 +3,7 @@ fetchingAsApObject: Hae Fedeversestä
 gotIt: Selvä!
 cancel: Peruuta
 enterUsername: Anna käyttäjänimi
-renotedBy: Buustannut {käyttäjä}
+renotedBy: Buustannut {user}
 noNotes: Ei lähetyksiä
 noNotifications: Ei ilmoituksia
 instance: Instanssi
@@ -220,3 +220,4 @@ clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jo
   federoidu. Yleensä tätä toimintoa ei tarvita.
 blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
   Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.
+_lang_: Suomi