From c548ec9906947c72743e611254a6557e8e8d057c Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 5 Feb 2025 19:01:44 +0900
Subject: refactor(frontend): verbatimModuleSyntaxを有効化 (#15323)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* wip

* wip

* wip

* revert unnecessary changes

* wip

* refactor(frontend): enforce verbatimModuleSyntax

* fix

* refactor(frontend-shared): enforce verbatimModuleSyntax

* wip

* refactor(frontend-embed): enforce verbatimModuleSyntax

* enforce consistent-type-imports

* fix lint config

* attemt to fix ci

* fix lint

* fix

* fix

* fix
---
 packages/frontend/src/components/MkSystemWebhookEditor.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 485d003f93..6a03c8acaa 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -96,7 +96,7 @@ import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import {
+import type {
 	MkSystemWebhookEditorProps,
 	MkSystemWebhookResult,
 	SystemWebhookEventType,
-- 
cgit v1.2.3-freya


From ffd8cf07e683eae95858d89fa9e5d85bf271c4d6 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 15 Feb 2025 10:24:22 +0900
Subject: update deps (#15311)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* bump misskey-dev/eslint-plugin

* lint fixes (backend)

* lint fixes (frontend)

* lint fixes (frontend-embed)

* rollback nsfwjs to 4.2.0

ref: infinitered/nsfwjs#904

* rollback openapi-typescript to v6

v7でOpenAPIのバリデーションが入るようになった関係でスコープ外での変更が避けられないため一時的に戻した

* lint fixes (misskey-js)

* temporarily disable errored lint rule (frontend-shared)

* fix lint

* temporarily ignore errored file for lint (frontend-shared)

* rollback simplewebauthn/server to 12.0.0

v13 contains breaking changes that require some decision making

* lint fixes (frontend-shared)

* build misskey-js with types

* fix(backend): migrate simplewebauthn/server to v12

* fix(misskey-js/autogen): ignore indent rules to generate consistent output

* attempt to fix test

changes due to capricorn86/happy-dom#1617 (XMLSerializer now produces valid XML)

* attempt to fix test

changes due to capricorn86/happy-dom#1617 (XMLSerializer now produces valid XML)

* fix test

* fix test

* fix test

* Apply suggestions from code review

Co-authored-by: anatawa12 <anatawa12@icloud.com>

* bump summaly to v5.2.0

* update tabler-icons to v3.30.0-based

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Co-authored-by: anatawa12 <anatawa12@icloud.com>
---
 package.json                                       |   30 +-
 packages/backend/package.json                      |   82 +-
 packages/backend/src/config.ts                     |    8 +-
 packages/backend/src/core/CaptchaService.ts        |    6 +-
 packages/backend/src/core/FanoutTimelineService.ts |    3 +-
 packages/backend/src/core/GlobalEventService.ts    |    2 +-
 packages/backend/src/core/MfmService.ts            |    3 +-
 packages/backend/src/core/UserWebhookService.ts    |    2 +-
 packages/backend/src/core/WebAuthnService.ts       |   18 +-
 .../backend/src/core/entities/UserEntityService.ts |    4 +-
 packages/backend/src/misc/json-schema.ts           |    4 +-
 packages/backend/src/misc/json-value.ts            |    2 +-
 packages/backend/src/models/User.ts                |    8 +-
 packages/backend/src/postgres.ts                   |    2 +-
 .../CheckModeratorsActivityProcessorService.ts     |    2 +-
 packages/backend/src/queue/types.ts                |   10 +-
 packages/backend/src/server/api/endpoints.ts       |    2 +-
 packages/backend/src/server/api/stream/channel.ts  |    6 +-
 packages/backend/test-federation/test/utils.ts     |    2 +-
 packages/backend/test/e2e/fetch-resource.ts        |   10 +-
 packages/backend/test/unit/MfmService.ts           |    4 +-
 packages/frontend-embed/package.json               |   48 +-
 packages/frontend-embed/src/post-message.ts        |    2 +-
 packages/frontend-shared/eslint.config.js          |    7 +
 packages/frontend-shared/js/embed-page.ts          |    2 +-
 packages/frontend-shared/js/emojilist.ts           |    2 +-
 packages/frontend-shared/js/i18n.ts                |    1 +
 packages/frontend/eslint.config.js                 |    2 +
 packages/frontend/package.json                     |  102 +-
 packages/frontend/src/components/MkCaptcha.vue     |    2 +
 packages/frontend/src/components/MkDialog.vue      |    1 +
 .../src/components/MkNotificationSelectWindow.vue  |    2 +-
 .../src/components/MkSortOrderEditor.define.ts     |    4 +-
 .../src/components/MkSystemWebhookEditor.vue       |    2 +-
 packages/frontend/src/components/MkTimeline.vue    |   18 +-
 .../components/MkUserAnnouncementEditDialog.vue    |    2 +-
 packages/frontend/src/components/grid/MkGrid.vue   |    2 +-
 .../src/components/grid/cell-validators.ts         |    8 +-
 packages/frontend/src/components/grid/cell.ts      |    6 +-
 packages/frontend/src/components/grid/column.ts    |    2 +-
 packages/frontend/src/components/grid/grid.ts      |    8 +-
 packages/frontend/src/components/grid/row.ts       |    6 +-
 packages/frontend/src/local-storage.ts             |    3 +-
 packages/frontend/src/nirax.ts                     |    2 +-
 .../admin/custom-emojis-manager.local.list.vue     |    2 +-
 .../admin/custom-emojis-manager.local.register.vue |    2 +-
 .../pages/admin/custom-emojis-manager.remote.vue   |    2 +-
 packages/frontend/src/pages/search.note.vue        |    2 +-
 packages/frontend/src/scripts/aiscript/api.ts      |    2 +-
 packages/frontend/src/scripts/file-drop.ts         |    2 +-
 .../frontend/src/scripts/format-time-string.ts     |    2 +-
 packages/frontend/src/scripts/key-event.ts         |   14 +-
 packages/frontend/src/scripts/stream-mock.ts       |    6 +-
 packages/frontend/src/store.ts                     |    2 +-
 packages/frontend/src/types/menu.ts                |    2 +-
 packages/misskey-bubble-game/package.json          |   14 +-
 packages/misskey-js/eslint.config.js               |    6 +
 packages/misskey-js/etc/misskey-js.api.md          |    6 +-
 packages/misskey-js/generator/eslint.config.js     |    6 +
 packages/misskey-js/generator/package.json         |   12 +-
 packages/misskey-js/generator/src/generator.ts     |    2 +-
 packages/misskey-js/generator/tsconfig.json        |    2 +-
 packages/misskey-js/package.json                   |   18 +-
 packages/misskey-js/src/api.types.ts               |    8 +-
 packages/misskey-js/src/autogen/endpoint.ts        |    2 +-
 packages/misskey-js/src/entities.ts                |    6 +-
 packages/misskey-js/src/streaming.ts               |    6 +-
 packages/misskey-reversi/package.json              |   14 +-
 packages/sw/package.json                           |   10 +-
 pnpm-lock.yaml                                     | 5856 +++++++++++---------
 70 files changed, 3409 insertions(+), 3041 deletions(-)

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/package.json b/package.json
index 52c139342f..cbe5caa0e4 100644
--- a/package.json
+++ b/package.json
@@ -51,29 +51,29 @@
 		"lodash": "4.17.21"
 	},
 	"dependencies": {
-		"cssnano": "6.1.2",
+		"cssnano": "7.0.6",
 		"execa": "8.0.1",
-		"fast-glob": "3.3.2",
+		"fast-glob": "3.3.3",
 		"ignore-walk": "6.0.5",
 		"js-yaml": "4.1.0",
-		"postcss": "8.4.49",
+		"postcss": "8.5.1",
 		"tar": "6.2.1",
-		"terser": "5.36.0",
-		"typescript": "5.6.3",
-		"esbuild": "0.24.0",
-		"glob": "11.0.0"
+		"terser": "5.37.0",
+		"typescript": "5.7.3",
+		"esbuild": "0.24.2",
+		"glob": "11.0.1"
 	},
 	"devDependencies": {
-		"@misskey-dev/eslint-plugin": "2.0.3",
-		"@types/node": "22.9.0",
-		"@typescript-eslint/eslint-plugin": "7.17.0",
-		"@typescript-eslint/parser": "7.17.0",
+		"@misskey-dev/eslint-plugin": "2.1.0",
+		"@types/node": "22.10.7",
+		"@typescript-eslint/eslint-plugin": "8.20.0",
+		"@typescript-eslint/parser": "8.20.0",
 		"cross-env": "7.0.3",
-		"cypress": "13.15.2",
-		"eslint": "9.14.0",
-		"globals": "15.12.0",
+		"cypress": "14.0.0",
+		"eslint": "9.18.0",
+		"globals": "15.14.0",
 		"ncp": "2.0.0",
-		"start-server-and-test": "2.0.8"
+		"start-server-and-test": "2.0.10"
 	},
 	"optionalDependencies": {
 		"@tensorflow/tfjs-core": "4.4.0"
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 757912755a..d6d87c54ae 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -69,28 +69,28 @@
 	"dependencies": {
 		"@aws-sdk/client-s3": "3.620.0",
 		"@aws-sdk/lib-storage": "3.620.0",
-		"@bull-board/api": "6.5.0",
-		"@bull-board/fastify": "6.5.0",
-		"@bull-board/ui": "6.5.0",
+		"@bull-board/api": "6.7.0",
+		"@bull-board/fastify": "6.7.0",
+		"@bull-board/ui": "6.7.0",
 		"@discordapp/twemoji": "15.1.0",
-		"@fastify/accepts": "5.0.1",
-		"@fastify/cookie": "11.0.1",
-		"@fastify/cors": "10.0.1",
-		"@fastify/express": "4.0.1",
+		"@fastify/accepts": "5.0.2",
+		"@fastify/cookie": "11.0.2",
+		"@fastify/cors": "10.0.2",
+		"@fastify/express": "4.0.2",
 		"@fastify/http-proxy": "10.0.1",
-		"@fastify/multipart": "9.0.1",
-		"@fastify/static": "8.0.2",
-		"@fastify/view": "10.0.1",
+		"@fastify/multipart": "9.0.2",
+		"@fastify/static": "8.0.4",
+		"@fastify/view": "10.0.2",
 		"@misskey-dev/sharp-read-bmp": "1.2.0",
-		"@misskey-dev/summaly": "5.1.0",
-		"@napi-rs/canvas": "0.1.56",
-		"@nestjs/common": "10.4.7",
-		"@nestjs/core": "10.4.7",
-		"@nestjs/testing": "10.4.7",
+		"@misskey-dev/summaly": "5.2.0",
+		"@napi-rs/canvas": "0.1.65",
+		"@nestjs/common": "11.0.1",
+		"@nestjs/core": "11.0.1",
+		"@nestjs/testing": "11.0.1",
 		"@peertube/http-signature": "1.7.0",
-		"@sentry/node": "8.38.0",
-		"@sentry/profiling-node": "8.38.0",
-		"@simplewebauthn/server": "10.0.1",
+		"@sentry/node": "8.50.0",
+		"@sentry/profiling-node": "8.50.0",
+		"@simplewebauthn/server": "12.0.0",
 		"@sinonjs/fake-timers": "11.2.2",
 		"@smithy/node-http-handler": "2.5.0",
 		"@swc/cli": "0.3.12",
@@ -103,10 +103,10 @@
 		"bcryptjs": "2.4.3",
 		"blurhash": "2.0.5",
 		"body-parser": "1.20.3",
-		"bullmq": "5.26.1",
+		"bullmq": "5.34.10",
 		"cacheable-lookup": "7.0.0",
 		"cbor": "9.0.2",
-		"chalk": "5.3.0",
+		"chalk": "5.4.1",
 		"chalk-template": "1.1.0",
 		"chokidar": "3.6.0",
 		"cli-highlight": "2.1.11",
@@ -114,35 +114,35 @@
 		"content-disposition": "0.5.4",
 		"date-fns": "2.30.0",
 		"deep-email-validator": "0.1.21",
-		"fastify": "5.0.0",
+		"fastify": "5.2.1",
 		"fastify-raw-body": "5.0.0",
 		"feed": "4.2.2",
 		"file-type": "19.6.0",
 		"fluent-ffmpeg": "2.1.3",
 		"form-data": "4.0.1",
-		"got": "14.4.4",
-		"happy-dom": "15.11.4",
+		"got": "14.4.5",
+		"happy-dom": "16.6.0",
 		"hpagent": "1.2.0",
 		"htmlescape": "1.1.1",
 		"http-link-header": "1.1.3",
-		"ioredis": "5.4.1",
+		"ioredis": "5.4.2",
 		"ip-cidr": "4.0.2",
 		"ipaddr.js": "2.2.0",
 		"is-svg": "5.1.0",
 		"js-yaml": "4.1.0",
-		"jsdom": "24.1.1",
+		"jsdom": "26.0.0",
 		"json5": "2.2.3",
-		"jsonld": "8.3.2",
+		"jsonld": "8.3.3",
 		"jsrsasign": "11.1.0",
 		"juice": "11.0.0",
-		"meilisearch": "0.45.0",
+		"meilisearch": "0.48.2",
 		"mfm-js": "0.24.0",
 		"microformats-parser": "2.0.2",
 		"mime-types": "2.1.35",
 		"misskey-js": "workspace:*",
 		"misskey-reversi": "workspace:*",
 		"ms": "3.0.0-canary.1",
-		"nanoid": "5.0.8",
+		"nanoid": "5.0.9",
 		"nested-property": "4.0.0",
 		"node-fetch": "3.3.2",
 		"nodemailer": "6.9.16",
@@ -151,7 +151,7 @@
 		"oauth2orize": "1.12.0",
 		"oauth2orize-pkce": "0.1.2",
 		"os-utils": "0.0.14",
-		"otpauth": "9.3.4",
+		"otpauth": "9.3.6",
 		"parse5": "7.2.1",
 		"pg": "8.13.1",
 		"pkce-challenge": "4.1.0",
@@ -167,19 +167,19 @@
 		"rename": "1.0.4",
 		"rss-parser": "3.13.0",
 		"rxjs": "7.8.1",
-		"sanitize-html": "2.13.1",
-		"secure-json-parse": "2.7.0",
+		"sanitize-html": "2.14.0",
+		"secure-json-parse": "3.0.2",
 		"sharp": "0.33.5",
 		"slacc": "0.0.10",
 		"strict-event-emitter-types": "2.0.0",
 		"stringz": "2.1.0",
-		"systeminformation": "5.23.5",
+		"systeminformation": "5.25.11",
 		"tinycolor2": "1.6.0",
 		"tmp": "0.2.3",
 		"tsc-alias": "1.8.10",
 		"tsconfig-paths": "4.2.0",
 		"typeorm": "0.3.20",
-		"typescript": "5.6.3",
+		"typescript": "5.7.3",
 		"ulid": "2.3.0",
 		"vary": "1.1.2",
 		"web-push": "3.6.7",
@@ -189,7 +189,7 @@
 	"devDependencies": {
 		"@jest/globals": "29.7.0",
 		"@nestjs/platform-express": "10.4.7",
-		"@simplewebauthn/types": "10.0.0",
+		"@simplewebauthn/types": "12.0.0",
 		"@swc/jest": "0.2.37",
 		"@types/accepts": "1.3.7",
 		"@types/archiver": "6.0.3",
@@ -204,11 +204,11 @@
 		"@types/js-yaml": "4.0.9",
 		"@types/jsdom": "21.1.7",
 		"@types/jsonld": "1.5.15",
-		"@types/jsrsasign": "10.5.14",
+		"@types/jsrsasign": "10.5.15",
 		"@types/mime-types": "2.1.4",
 		"@types/ms": "0.7.34",
-		"@types/node": "22.9.0",
-		"@types/nodemailer": "6.4.16",
+		"@types/node": "22.10.7",
+		"@types/nodemailer": "6.4.17",
 		"@types/oauth": "0.9.6",
 		"@types/oauth2orize": "1.11.5",
 		"@types/oauth2orize-pkce": "0.1.2",
@@ -227,16 +227,16 @@
 		"@types/vary": "1.1.3",
 		"@types/web-push": "3.6.4",
 		"@types/ws": "8.5.13",
-		"@typescript-eslint/eslint-plugin": "7.17.0",
-		"@typescript-eslint/parser": "7.17.0",
+		"@typescript-eslint/eslint-plugin": "8.20.0",
+		"@typescript-eslint/parser": "8.20.0",
 		"aws-sdk-client-mock": "4.0.1",
 		"cross-env": "7.0.3",
-		"eslint-plugin-import": "2.30.0",
+		"eslint-plugin-import": "2.31.0",
 		"execa": "8.0.1",
 		"fkill": "9.0.0",
 		"jest": "29.7.0",
 		"jest-mock": "29.7.0",
-		"nodemon": "3.1.7",
+		"nodemon": "3.1.9",
 		"pid-port": "1.0.0",
 		"simple-oauth2": "5.1.0"
 	}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index c0b1484804..d5fd2ba558 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -105,8 +105,8 @@ type Source = {
 
 	logging?: {
 		sql?: {
-			disableQueryTruncation? : boolean,
-			enableQueryParamLogging? : boolean,
+			disableQueryTruncation?: boolean,
+			enableQueryParamLogging?: boolean,
 		}
 	}
 };
@@ -166,8 +166,8 @@ export type Config = {
 	signToActivityPubGet: boolean | undefined;
 	logging?: {
 		sql?: {
-			disableQueryTruncation? : boolean,
-			enableQueryParamLogging? : boolean,
+			disableQueryTruncation?: boolean,
+			enableQueryParamLogging?: boolean,
 		}
 	}
 
diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index 8c7f66236e..ee081f29b0 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -43,7 +43,7 @@ export type CaptchaSetting = {
 		siteKey: string | null;
 		secretKey: string | null;
 	}
-}
+};
 
 export class CaptchaError extends Error {
 	public readonly code: CaptchaErrorCode;
@@ -59,11 +59,11 @@ export class CaptchaError extends Error {
 
 export type CaptchaSaveSuccess = {
 	success: true;
-}
+};
 export type CaptchaSaveFailure = {
 	success: false;
 	error: CaptchaError;
-}
+};
 export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure;
 
 type CaptchaResponse = {
diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts
index f6dabfadcd..24999bf4da 100644
--- a/packages/backend/src/core/FanoutTimelineService.ts
+++ b/packages/backend/src/core/FanoutTimelineService.ts
@@ -9,7 +9,7 @@ import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
 
-export type FanoutTimelineName =
+export type FanoutTimelineName = (
 	// home timeline
 	| `homeTimeline:${string}`
 	| `homeTimelineWithFiles:${string}` // only notes with files are included
@@ -37,6 +37,7 @@ export type FanoutTimelineName =
 
 	// role timelines
 	| `roleTimeline:${string}` // any notes are included
+);
 
 @Injectable()
 export class FanoutTimelineService {
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 03646ff566..224fdabc4c 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -211,7 +211,7 @@ type SerializedAll<T> = {
 
 type UndefinedAsNullAll<T> = {
 	[K in keyof T]: T[K] extends undefined ? null : T[K];
-}
+};
 
 export interface InternalEventTypes {
 	userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index bf06d4457e..00208927e2 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -492,7 +492,8 @@ export class MfmService {
 
 		appendChildren(nodes, body);
 
-		const serialized = new XMLSerializer().serializeToString(body);
+		// Remove the unnecessary namespace
+		const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*<p xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">/, '<p>');
 
 		happyDOM.close().catch(err => {});
 
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
index b1728671ae..9b0a598a1b 100644
--- a/packages/backend/src/core/UserWebhookService.ts
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -15,7 +15,7 @@ import { QueueService } from '@/core/QueueService.js';
 import type { OnApplicationShutdown } from '@nestjs/common';
 
 export type UserWebhookPayload<T extends WebhookEventTypes> =
-	T extends 'note' | 'reply' | 'renote' |'mention' ? {
+	T extends 'note' | 'reply' | 'renote' | 'mention' ? {
 		note: Packed<'Note'>,
 	} :
 	T extends 'follow' | 'unfollow' ? {
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index ed75e4f467..372e1e2ab7 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -127,11 +127,11 @@ export class WebAuthnService {
 		const { registrationInfo } = verification;
 
 		return {
-			credentialID: registrationInfo.credentialID,
-			credentialPublicKey: registrationInfo.credentialPublicKey,
+			credentialID: registrationInfo.credential.id,
+			credentialPublicKey: registrationInfo.credential.publicKey,
 			attestationObject: registrationInfo.attestationObject,
 			fmt: registrationInfo.fmt,
-			counter: registrationInfo.counter,
+			counter: registrationInfo.credential.counter,
 			userVerified: registrationInfo.userVerified,
 			credentialDeviceType: registrationInfo.credentialDeviceType,
 			credentialBackedUp: registrationInfo.credentialBackedUp,
@@ -212,9 +212,9 @@ export class WebAuthnService {
 				expectedChallenge: challenge,
 				expectedOrigin: relyingParty.origin,
 				expectedRPID: relyingParty.rpId,
-				authenticator: {
-					credentialID: key.id,
-					credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
+				credential: {
+					id: key.id,
+					publicKey: Buffer.from(key.publicKey, 'base64url'),
 					counter: key.counter,
 					transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
 				},
@@ -292,9 +292,9 @@ export class WebAuthnService {
 				expectedChallenge: challenge,
 				expectedOrigin: relyingParty.origin,
 				expectedRPID: relyingParty.rpId,
-				authenticator: {
-					credentialID: key.id,
-					credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
+				credential: {
+					id: key.id,
+					publicKey: Buffer.from(key.publicKey, 'base64url'),
 					counter: key.counter,
 					transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
 				},
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index d3c087a153..fbd3892dd4 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -57,12 +57,14 @@ const ajv = new Ajv();
 
 function isLocalUser(user: MiUser): user is MiLocalUser;
 function isLocalUser<T extends { host: MiUser['host'] }>(user: T): user is (T & { host: null; });
+
 function isLocalUser(user: MiUser | { host: MiUser['host'] }): boolean {
 	return user.host == null;
 }
 
 function isRemoteUser(user: MiUser): user is MiRemoteUser;
 function isRemoteUser<T extends { host: MiUser['host'] }>(user: T): user is (T & { host: string; });
+
 function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
 	return !isLocalUser(user);
 }
@@ -78,7 +80,7 @@ export type UserRelation = {
 	isBlocked: boolean
 	isMuted: boolean
 	isRenoteMuted: boolean
-}
+};
 
 @Injectable()
 export class UserEntityService implements OnModuleInit {
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index f612591eda..ac74d68c95 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -143,7 +143,7 @@ type OfSchema = {
 	readonly anyOf?: ReadonlyArray<Schema>;
 	readonly oneOf?: ReadonlyArray<Schema>;
 	readonly allOf?: ReadonlyArray<Schema>;
-}
+};
 
 export interface Schema extends OfSchema {
 	readonly type?: TypeStringef;
@@ -217,7 +217,7 @@ type ObjectSchemaTypeDef<p extends Schema> =
 	:
 	p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
 	p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
-	any
+	any;
 
 type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
 
diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts
index bd7fe12058..195f7c4d47 100644
--- a/packages/backend/src/misc/json-value.ts
+++ b/packages/backend/src/misc/json-value.ts
@@ -4,7 +4,7 @@
  */
 
 export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
-export type JsonObject = {[K in string]?: JsonValue};
+export type JsonObject = { [K in string]?: JsonValue };
 export type JsonArray = JsonValue[];
 
 export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 96de30c4c2..549d78a22c 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -288,24 +288,24 @@ export class MiUser {
 export type MiLocalUser = MiUser & {
 	host: null;
 	uri: null;
-}
+};
 
 export type MiPartialLocalUser = Partial<MiUser> & {
 	id: MiUser['id'];
 	host: null;
 	uri: null;
-}
+};
 
 export type MiRemoteUser = MiUser & {
 	host: string;
 	uri: string;
-}
+};
 
 export type MiPartialRemoteUser = Partial<MiUser> & {
 	id: MiUser['id'];
 	host: string;
 	uri: string;
-}
+};
 
 export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
 export const passwordSchema = { type: 'string', minLength: 1 } as const;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index d09240eba1..8a0b7d97d7 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -92,7 +92,7 @@ const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
 export type LoggerProps = {
 	disableQueryTruncation?: boolean;
 	enableQueryParamLogging?: boolean;
-}
+};
 
 function highlightSql(sql: string) {
 	return highlight.highlight(sql, {
diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
index 2e84430e72..c9fe4fca73 100644
--- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -29,7 +29,7 @@ export type ModeratorInactivityEvaluationResult = {
 	isModeratorsInactive: boolean;
 	inactiveModerators: MiUser[];
 	remainingTime: ModeratorInactivityRemainingTime;
-}
+};
 
 export type ModeratorInactivityRemainingTime = {
 	time: number;
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index 5db919a149..757daea88b 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -38,7 +38,7 @@ export type RelationshipJobData = {
 	silent?: boolean;
 	requestId?: string;
 	withReplies?: boolean;
-}
+};
 
 export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
 
@@ -61,11 +61,11 @@ export type DbJobMap = {
 	importUserLists: DbUserImportJobData;
 	importCustomEmojis: DbUserImportJobData;
 	deleteAccount: DbUserDeleteJobData;
-}
+};
 
 export type DbJobDataWithUser = {
 	user: ThinUser;
-}
+};
 
 export type DbExportFollowingData = {
 	user: ThinUser;
@@ -75,7 +75,7 @@ export type DbExportFollowingData = {
 
 export type DBExportAntennasData = {
 	user: ThinUser
-}
+};
 
 export type DbUserDeleteJobData = {
 	user: ThinUser;
@@ -91,7 +91,7 @@ export type DbUserImportJobData = {
 export type DBAntennaImportJobData = {
 	user: ThinUser,
 	antenna: Antenna
-}
+};
 
 export type DbUserImportToDbJobData = {
 	user: ThinUser;
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index a9a2ebc041..7f4ca9c0e0 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -122,7 +122,7 @@ export type IEndpointMeta = (Omit<IEndpointMetaBase, 'requireCrential' | 'requir
 }) | (Omit<IEndpointMetaBase, 'requireAdmin' | 'kind'> & {
 	requireAdmin: true,
 	kind: (typeof permissions)[number],
-})
+});
 
 export interface IEndpoint {
 	name: string;
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index 84cb552369..686aea423c 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -82,8 +82,8 @@ export default abstract class Channel {
 		this.connection = connection;
 	}
 
-	public send(payload: { type: string, body: JsonValue }): void
-	public send(type: string, payload: JsonValue): void
+	public send(payload: { type: string, body: JsonValue }): void;
+	public send(type: string, payload: JsonValue): void;
 	@bindThis
 	public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
 		const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
@@ -108,4 +108,4 @@ export type MiChannelService<T extends boolean> = {
 	requireCredential: T;
 	kind: T extends true ? string : string | null | undefined;
 	create: (id: string, connection: Connection) => Channel;
-}
+};
diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts
index 093277cdb4..db8da5025a 100644
--- a/packages/backend/test-federation/test/utils.ts
+++ b/packages/backend/test-federation/test/utils.ts
@@ -22,7 +22,7 @@ export type LoginUser = SigninResponse & {
 	client: Misskey.api.APIClient;
 	username: string;
 	password: string;
-}
+};
 
 /** used for avoiding overload and some endpoints */
 export type Request = <
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
index 8ea4cb9800..b85cebf724 100644
--- a/packages/backend/test/e2e/fetch-resource.ts
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -10,13 +10,13 @@ import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet
 import type { SimpleGetResponse } from '../utils.js';
 import type * as misskey from 'misskey-js';
 
-// Request Accept
+// Request Accept in lowercase
 const ONLY_AP = 'application/activity+json';
 const PREFER_AP = 'application/activity+json, */*';
 const PREFER_HTML = 'text/html, */*';
 const UNSPECIFIED = '*/*';
 
-// Response Content-Type
+// Response Content-Type in lowercase
 const AP = 'application/activity+json; charset=utf-8';
 const HTML = 'text/html; charset=utf-8';
 const JSON_UTF8 = 'application/json; charset=utf-8';
@@ -44,7 +44,8 @@ describe('Webリソース', () => {
 		const { path, accept, cookie, type } = param;
 		const res = await simpleGet(path, accept, cookie);
 		assert.strictEqual(res.status, 200);
-		assert.strictEqual(res.type, type ?? HTML);
+		// Header values are case-insensitive
+		assert.strictEqual(res.type?.toLowerCase(), (type ?? HTML).toLowerCase());
 		return res;
 	};
 
@@ -95,8 +96,7 @@ describe('Webリソース', () => {
 	describe.each([
 		{ path: '/', type: HTML },
 		{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
-		// fastify-static gives charset=UTF-8 instead of utf-8 and that's okay
-		{ path: '/api-doc', type: 'text/html; charset=UTF-8' },
+		{ path: '/api-doc', type: HTML },
 		{ path: '/api.json', type: JSON_UTF8 },
 		{ path: '/api-console', type: HTML },
 		{ path: '/_info_card_', type: HTML },
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index 36af8823f6..7350da3cae 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -24,13 +24,13 @@ describe('MfmService', () => {
 	describe('toHtml', () => {
 		test('br', () => {
 			const input = 'foo\nbar\nbaz';
-			const output = '<p><span>foo<br>bar<br>baz</span></p>';
+			const output = '<p><span>foo<br />bar<br />baz</span></p>';
 			assert.equal(mfmService.toHtml(mfm.parse(input)), output);
 		});
 
 		test('br alt', () => {
 			const input = 'foo\r\nbar\rbaz';
-			const output = '<p><span>foo<br>bar<br>baz</span></p>';
+			const output = '<p><span>foo<br />bar<br />baz</span></p>';
 			assert.equal(mfmService.toHtml(mfm.parse(input)), output);
 		});
 
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index ab5026ab0d..c173dc53d0 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -13,11 +13,11 @@
 		"@discordapp/twemoji": "15.1.0",
 		"@rollup/plugin-json": "6.1.0",
 		"@rollup/plugin-replace": "5.0.7",
-		"@rollup/pluginutils": "5.1.3",
-		"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
+		"@rollup/pluginutils": "5.1.4",
+		"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
 		"@twemoji/parser": "15.1.1",
-		"@vitejs/plugin-vue": "5.2.0",
-		"@vue/compiler-sfc": "3.5.12",
+		"@vitejs/plugin-vue": "5.2.1",
+		"@vue/compiler-sfc": "3.5.13",
 		"astring": "1.9.0",
 		"buraha": "0.0.1",
 		"estree-walker": "3.0.3",
@@ -25,47 +25,47 @@
 		"misskey-js": "workspace:*",
 		"frontend-shared": "workspace:*",
 		"punycode.js": "2.3.1",
-		"rollup": "4.26.0",
-		"sass": "1.79.4",
-		"shiki": "1.22.2",
+		"rollup": "4.31.0",
+		"sass": "1.83.4",
+		"shiki": "1.27.2",
 		"tinycolor2": "1.6.0",
 		"tsc-alias": "1.8.10",
 		"tsconfig-paths": "4.2.0",
-		"typescript": "5.6.3",
+		"typescript": "5.7.3",
 		"uuid": "10.0.0",
 		"json5": "2.2.3",
-		"vite": "5.4.11",
-		"vue": "3.5.12"
+		"vite": "6.0.7",
+		"vue": "3.5.13"
 	},
 	"devDependencies": {
-		"@misskey-dev/summaly": "5.1.0",
+		"@misskey-dev/summaly": "5.2.0",
 		"@testing-library/vue": "8.1.0",
 		"@types/estree": "1.0.6",
 		"@types/micromatch": "4.0.9",
-		"@types/node": "22.9.0",
+		"@types/node": "22.10.7",
 		"@types/punycode.js": "npm:@types/punycode@2.1.4",
 		"@types/tinycolor2": "1.4.6",
 		"@types/uuid": "10.0.0",
 		"@types/ws": "8.5.13",
-		"@typescript-eslint/eslint-plugin": "7.17.0",
-		"@typescript-eslint/parser": "7.17.0",
+		"@typescript-eslint/eslint-plugin": "8.20.0",
+		"@typescript-eslint/parser": "8.20.0",
 		"@vitest/coverage-v8": "1.6.0",
-		"@vue/runtime-core": "3.5.12",
+		"@vue/runtime-core": "3.5.13",
 		"acorn": "8.14.0",
 		"cross-env": "7.0.3",
 		"eslint-plugin-import": "2.31.0",
-		"eslint-plugin-vue": "9.31.0",
-		"fast-glob": "3.3.2",
-		"happy-dom": "10.0.3",
+		"eslint-plugin-vue": "9.32.0",
+		"fast-glob": "3.3.3",
+		"happy-dom": "16.6.0",
 		"intersection-observer": "0.12.2",
 		"micromatch": "4.0.8",
-		"msw": "2.6.4",
-		"nodemon": "3.1.7",
-		"prettier": "3.3.3",
-		"start-server-and-test": "2.0.8",
+		"msw": "2.7.0",
+		"nodemon": "3.1.9",
+		"prettier": "3.4.2",
+		"start-server-and-test": "2.0.10",
 		"vite-plugin-turbosnap": "1.0.3",
-		"vue-component-type-helpers": "2.1.10",
+		"vue-component-type-helpers": "2.2.0",
 		"vue-eslint-parser": "9.4.3",
-		"vue-tsc": "2.1.10"
+		"vue-tsc": "2.2.0"
 	}
 }
diff --git a/packages/frontend-embed/src/post-message.ts b/packages/frontend-embed/src/post-message.ts
index fd8eb8a5d2..93b57c380b 100644
--- a/packages/frontend-embed/src/post-message.ts
+++ b/packages/frontend-embed/src/post-message.ts
@@ -21,7 +21,7 @@ export type MiPostMessageEvent<T extends PostMessageEventType = PostMessageEvent
 	type: T;
 	iframeId?: string;
 	payload?: PostMessageEventPayload[T];
-}
+};
 
 let defaultIframeId: string | null = null;
 
diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js
index a3ab438336..63323aeddc 100644
--- a/packages/frontend-shared/eslint.config.js
+++ b/packages/frontend-shared/eslint.config.js
@@ -98,4 +98,11 @@ export default [
 			'vue/attribute-hyphenation': ['error', 'never'],
 		},
 	},
+	{
+		ignores: [
+			// TODO: Error while loading rule '@typescript-eslint/naming-convention': Cannot use 'in' operator to search for 'type' in undefined のため一時的に無効化
+			// See https://github.com/misskey-dev/misskey/pull/15311
+			'js/i18n.ts',
+		],
+	},
 ];
diff --git a/packages/frontend-shared/js/embed-page.ts b/packages/frontend-shared/js/embed-page.ts
index d5555a98c3..2a9b7eb478 100644
--- a/packages/frontend-shared/js/embed-page.ts
+++ b/packages/frontend-shared/js/embed-page.ts
@@ -6,7 +6,7 @@
 //#region Embed関連の定義
 
 /** 埋め込みの対象となるエンティティ（/embed/xxx の xxx の部分と対応させる） */
-const embeddableEntities = [
+export const embeddableEntities = [
 	'notes',
 	'user-timeline',
 	'clips',
diff --git a/packages/frontend-shared/js/emojilist.ts b/packages/frontend-shared/js/emojilist.ts
index bde30a864f..adccb60ac2 100644
--- a/packages/frontend-shared/js/emojilist.ts
+++ b/packages/frontend-shared/js/emojilist.ts
@@ -9,7 +9,7 @@ export type UnicodeEmojiDef = {
 	name: string;
 	char: string;
 	category: typeof unicodeEmojiCategories[number];
-}
+};
 
 // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
 import _emojilist from './emojilist.json';
diff --git a/packages/frontend-shared/js/i18n.ts b/packages/frontend-shared/js/i18n.ts
index 18232691fa..480cfcd642 100644
--- a/packages/frontend-shared/js/i18n.ts
+++ b/packages/frontend-shared/js/i18n.ts
@@ -2,6 +2,7 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
+
 import type { ILocale, ParameterizedString } from '../../../locales/index.js';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js
index 7805256fd4..8aa70c66a2 100644
--- a/packages/frontend/eslint.config.js
+++ b/packages/frontend/eslint.config.js
@@ -47,6 +47,8 @@ export default [
 			'@typescript-eslint/no-empty-interface': ['error', {
 				allowSingleExtends: true,
 			}],
+			// defineExposeが誤検知されてしまう
+			'@typescript-eslint/no-unused-expressions': 'off',
 			'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
 			// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
 			// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 804160baad..8e486d8125 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -22,23 +22,23 @@
 		"@misskey-dev/browser-image-resizer": "2024.1.0",
 		"@rollup/plugin-json": "6.1.0",
 		"@rollup/plugin-replace": "5.0.7",
-		"@rollup/pluginutils": "5.1.3",
+		"@rollup/pluginutils": "5.1.4",
 		"@syuilo/aiscript": "0.19.0",
-		"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
+		"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz",
 		"@twemoji/parser": "15.1.1",
-		"@vitejs/plugin-vue": "5.2.0",
-		"@vue/compiler-sfc": "3.5.12",
+		"@vitejs/plugin-vue": "5.2.1",
+		"@vue/compiler-sfc": "3.5.13",
 		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
 		"astring": "1.9.0",
 		"broadcast-channel": "7.0.0",
 		"buraha": "0.0.1",
 		"canvas-confetti": "1.9.3",
-		"chart.js": "4.4.6",
+		"chart.js": "4.4.7",
 		"chartjs-adapter-date-fns": "3.0.0",
 		"chartjs-chart-matrix": "2.0.1",
 		"chartjs-plugin-gradient": "0.6.1",
-		"chartjs-plugin-zoom": "2.0.1",
-		"chromatic": "11.18.1",
+		"chartjs-plugin-zoom": "2.2.0",
+		"chromatic": "11.25.0",
 		"compare-versions": "6.1.1",
 		"cropperjs": "2.0.0-rc.2",
 		"date-fns": "2.30.0",
@@ -56,50 +56,50 @@
 		"misskey-reversi": "workspace:*",
 		"photoswipe": "5.4.4",
 		"punycode.js": "2.3.1",
-		"rollup": "4.26.0",
-		"sanitize-html": "2.13.1",
-		"sass": "1.79.3",
-		"shiki": "1.22.2",
+		"rollup": "4.31.0",
+		"sanitize-html": "2.14.0",
+		"sass": "1.83.4",
+		"shiki": "1.27.2",
 		"strict-event-emitter-types": "2.0.0",
 		"textarea-caret": "3.1.0",
-		"three": "0.169.0",
+		"three": "0.172.0",
 		"throttle-debounce": "5.0.2",
 		"tinycolor2": "1.6.0",
 		"tsc-alias": "1.8.10",
 		"tsconfig-paths": "4.2.0",
-		"typescript": "5.6.3",
-		"uuid": "10.0.0",
+		"typescript": "5.7.3",
+		"uuid": "11.0.5",
 		"v-code-diff": "1.13.1",
-		"vite": "5.4.11",
-		"vue": "3.5.12",
+		"vite": "6.0.7",
+		"vue": "3.5.13",
 		"vuedraggable": "next"
 	},
 	"devDependencies": {
-		"@misskey-dev/summaly": "5.1.0",
-		"@storybook/addon-actions": "8.4.4",
-		"@storybook/addon-essentials": "8.4.4",
-		"@storybook/addon-interactions": "8.4.4",
-		"@storybook/addon-links": "8.4.4",
-		"@storybook/addon-mdx-gfm": "8.4.4",
-		"@storybook/addon-storysource": "8.4.4",
-		"@storybook/blocks": "8.4.4",
-		"@storybook/components": "8.4.4",
-		"@storybook/core-events": "8.4.4",
-		"@storybook/manager-api": "8.4.4",
-		"@storybook/preview-api": "8.4.4",
-		"@storybook/react": "8.4.4",
-		"@storybook/react-vite": "8.4.4",
-		"@storybook/test": "8.4.4",
-		"@storybook/theming": "8.4.4",
-		"@storybook/types": "8.4.4",
-		"@storybook/vue3": "8.4.4",
-		"@storybook/vue3-vite": "8.4.4",
+		"@misskey-dev/summaly": "5.2.0",
+		"@storybook/addon-actions": "8.5.0",
+		"@storybook/addon-essentials": "8.5.0",
+		"@storybook/addon-interactions": "8.5.0",
+		"@storybook/addon-links": "8.5.0",
+		"@storybook/addon-mdx-gfm": "8.5.0",
+		"@storybook/addon-storysource": "8.5.0",
+		"@storybook/blocks": "8.5.0",
+		"@storybook/components": "8.5.0",
+		"@storybook/core-events": "8.5.0",
+		"@storybook/manager-api": "8.5.0",
+		"@storybook/preview-api": "8.5.0",
+		"@storybook/react": "8.5.0",
+		"@storybook/react-vite": "8.5.0",
+		"@storybook/test": "8.5.0",
+		"@storybook/theming": "8.5.0",
+		"@storybook/types": "8.5.0",
+		"@storybook/vue3": "8.5.0",
+		"@storybook/vue3-vite": "8.5.0",
 		"@testing-library/vue": "8.1.0",
-		"@types/canvas-confetti": "^1.6.4",
+		"@types/canvas-confetti": "1.6.4",
 		"@types/estree": "1.0.6",
 		"@types/matter-js": "0.19.7",
 		"@types/micromatch": "4.0.9",
-		"@types/node": "22.9.0",
+		"@types/node": "22.10.7",
 		"@types/punycode.js": "npm:@types/punycode@2.1.4",
 		"@types/sanitize-html": "2.13.0",
 		"@types/seedrandom": "3.0.8",
@@ -107,34 +107,34 @@
 		"@types/tinycolor2": "1.4.6",
 		"@types/uuid": "10.0.0",
 		"@types/ws": "8.5.13",
-		"@typescript-eslint/eslint-plugin": "7.17.0",
-		"@typescript-eslint/parser": "7.17.0",
+		"@typescript-eslint/eslint-plugin": "8.20.0",
+		"@typescript-eslint/parser": "8.20.0",
 		"@vitest/coverage-v8": "1.6.0",
-		"@vue/runtime-core": "3.5.12",
+		"@vue/runtime-core": "3.5.13",
 		"acorn": "8.14.0",
 		"cross-env": "7.0.3",
-		"cypress": "13.15.2",
+		"cypress": "14.0.0",
 		"eslint-plugin-import": "2.31.0",
-		"eslint-plugin-vue": "9.31.0",
-		"fast-glob": "3.3.2",
-		"happy-dom": "10.0.3",
+		"eslint-plugin-vue": "9.32.0",
+		"fast-glob": "3.3.3",
+		"happy-dom": "16.6.0",
 		"intersection-observer": "0.12.2",
 		"micromatch": "4.0.8",
-		"msw": "2.6.4",
+		"msw": "2.7.0",
 		"msw-storybook-addon": "2.0.4",
-		"nodemon": "3.1.7",
-		"prettier": "3.3.3",
+		"nodemon": "3.1.9",
+		"prettier": "3.4.2",
 		"react": "18.3.1",
 		"react-dom": "18.3.1",
 		"seedrandom": "3.0.5",
-		"start-server-and-test": "2.0.8",
-		"storybook": "8.4.4",
+		"start-server-and-test": "2.0.10",
+		"storybook": "8.5.0",
 		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
 		"vite-plugin-turbosnap": "1.0.3",
 		"vitest": "1.6.0",
 		"vitest-fetch-mock": "0.2.2",
-		"vue-component-type-helpers": "2.1.10",
+		"vue-component-type-helpers": "2.2.0",
 		"vue-eslint-parser": "9.4.3",
-		"vue-tsc": "2.1.10"
+		"vue-tsc": "2.2.0"
 	}
 }
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index b1167bbac6..134f8226d4 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -50,6 +50,8 @@ type CaptchaContainer = {
 };
 
 declare global {
+	// Window を拡張してるため、空ではない
+	// eslint-disable-next-line @typescript-eslint/no-empty-object-type
 	interface Window extends CaptchaContainer { }
 }
 
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index b095a1cd4a..6c9fa3167a 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -142,6 +142,7 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
 // overload function を使いたいので lint エラーを無視する
 function done(canceled: true): void;
 function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
+
 function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
 	emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
 	modal.value?.close();
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 90a3e48187..a0fb7fea83 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -39,7 +39,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import { notificationTypes } from '@@/js/const.js';
 import { i18n } from '@/i18n.js';
 
-type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
+type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>;
 
 const emit = defineEmits<{
 	(ev: 'done', v: { excludeTypes: string[] }): void,
diff --git a/packages/frontend/src/components/MkSortOrderEditor.define.ts b/packages/frontend/src/components/MkSortOrderEditor.define.ts
index f023b5d72b..e56b93f98a 100644
--- a/packages/frontend/src/components/MkSortOrderEditor.define.ts
+++ b/packages/frontend/src/components/MkSortOrderEditor.define.ts
@@ -3,9 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-export type SortOrderDirection = '+' | '-'
+export type SortOrderDirection = '+' | '-';
 
 export type SortOrder<T extends string> = {
 	key: T;
 	direction: SortOrderDirection;
-}
+};
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 6a03c8acaa..7e92726dcb 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -114,7 +114,7 @@ type EventType = {
 	userCreated: boolean;
 	inactiveModeratorsWarning: boolean;
 	inactiveModeratorsInvitationOnlyChanged: boolean;
-}
+};
 
 const emit = defineEmits<{
 	(ev: 'submitted', result: MkSystemWebhookResult): void;
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index d88ff60a51..7bae240ddd 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -57,15 +57,15 @@ provide('tl_withSensitive', computed(() => props.withSensitive));
 provide('inChannel', computed(() => props.src === 'channel'));
 
 type TimelineQueryType = {
-  antennaId?: string,
-  withRenotes?: boolean,
-  withReplies?: boolean,
-  withFiles?: boolean,
-  visibility?: string,
-  listId?: string,
-  channelId?: string,
-  roleId?: string
-}
+	antennaId?: string,
+	withRenotes?: boolean,
+	withReplies?: boolean,
+	withFiles?: boolean,
+	visibility?: string,
+	listId?: string,
+	channelId?: string,
+	roleId?: string
+};
 
 const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>();
 const tlComponent = shallowRef<InstanceType<typeof MkNotes>>();
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index fe499fabbf..34991fa0dd 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -62,7 +62,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 
-type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
+type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; };
 
 const props = defineProps<{
 	user: Misskey.entities.User,
diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue
index 84f1f754b2..c89e23c135 100644
--- a/packages/frontend/src/components/grid/MkGrid.vue
+++ b/packages/frontend/src/components/grid/MkGrid.vue
@@ -79,7 +79,7 @@ type RowHolder = {
 	row: GridRow,
 	cells: GridCell[],
 	origin: DataSource,
-}
+};
 
 const emit = defineEmits<{
 	(ev: 'event', event: GridEvent, context: GridContext): void;
diff --git a/packages/frontend/src/components/grid/cell-validators.ts b/packages/frontend/src/components/grid/cell-validators.ts
index 56d7f0f13d..7310a82c9e 100644
--- a/packages/frontend/src/components/grid/cell-validators.ts
+++ b/packages/frontend/src/components/grid/cell-validators.ts
@@ -18,25 +18,25 @@ export type ValidatorParams = {
 export type ValidatorResult = {
 	valid: boolean;
 	message?: string;
-}
+};
 
 export type GridCellValidator = {
 	name?: string;
 	ignoreViolation?: boolean;
 	validate: (params: ValidatorParams) => ValidatorResult;
-}
+};
 
 export type ValidateViolation = {
 	valid: boolean;
 	params: ValidatorParams;
 	violations: ValidateViolationItem[];
-}
+};
 
 export type ValidateViolationItem = {
 	valid: boolean;
 	validator: GridCellValidator;
 	result: ValidatorResult;
-}
+};
 
 export function cellValidation(allCells: GridCell[], cell: GridCell, newValue: CellValue): ValidateViolation {
 	const { column, row } = cell;
diff --git a/packages/frontend/src/components/grid/cell.ts b/packages/frontend/src/components/grid/cell.ts
index 2569c6d50d..d347d05bdb 100644
--- a/packages/frontend/src/components/grid/cell.ts
+++ b/packages/frontend/src/components/grid/cell.ts
@@ -15,7 +15,7 @@ export type CellValue = string | boolean | number | undefined | null | Array<unk
 export type CellAddress = {
 	row: number;
 	col: number;
-}
+};
 
 export const CELL_ADDRESS_NONE: CellAddress = {
 	row: -1,
@@ -32,13 +32,13 @@ export type GridCell = {
 	contentSize: Size;
 	setting: GridCellSetting;
 	violation: ValidateViolation;
-}
+};
 
 export type GridCellContextMenuFactory = (col: GridColumn, row: GridRow, value: CellValue, context: GridContext) => MenuItem[];
 
 export type GridCellSetting = {
 	contextMenuFactory?: GridCellContextMenuFactory;
-}
+};
 
 export function createCell(
 	column: GridColumn,
diff --git a/packages/frontend/src/components/grid/column.ts b/packages/frontend/src/components/grid/column.ts
index 6780c8a3a7..6a694b39ec 100644
--- a/packages/frontend/src/components/grid/column.ts
+++ b/packages/frontend/src/components/grid/column.ts
@@ -40,7 +40,7 @@ export type GridColumn = {
 	setting: GridColumnSetting;
 	width: string;
 	contentSize: Size;
-}
+};
 
 export function createColumn(setting: GridColumnSetting, index: number): GridColumn {
 	return {
diff --git a/packages/frontend/src/components/grid/grid.ts b/packages/frontend/src/components/grid/grid.ts
index 03947b7ee3..0428e6493c 100644
--- a/packages/frontend/src/components/grid/grid.ts
+++ b/packages/frontend/src/components/grid/grid.ts
@@ -21,7 +21,7 @@ export type GridSetting = {
 
 export type DataSource = Record<string, CellValue>;
 
-export type GridState =
+export type GridState = (
 	'normal' |
 	'cellSelecting' |
 	'cellEditing' |
@@ -29,19 +29,19 @@ export type GridState =
 	'colSelecting' |
 	'rowSelecting' |
 	'hidden'
-	;
+);
 
 export type Size = {
 	width: number;
 	height: number;
-}
+};
 
 export type SizeStyle = number | 'auto' | undefined;
 
 export type AdditionalStyle = {
 	className?: string;
 	style?: Record<string, string | number>;
-}
+};
 
 export class GridEventEmitter extends EventEmitter<{
 	'forceRefreshContentSize': void;
diff --git a/packages/frontend/src/components/grid/row.ts b/packages/frontend/src/components/grid/row.ts
index f34770312a..42da22193f 100644
--- a/packages/frontend/src/components/grid/row.ts
+++ b/packages/frontend/src/components/grid/row.ts
@@ -27,7 +27,7 @@ export type GridRowStyleRuleConditionParams = {
 export type GridRowStyleRule = {
 	condition: (params: GridRowStyleRuleConditionParams) => boolean;
 	applyStyle: AdditionalStyle;
-}
+};
 
 export type GridRowContextMenuFactory = (row: GridRow, context: GridContext) => MenuItem[];
 
@@ -40,7 +40,7 @@ export type GridRowSetting = {
 	events?: {
 		delete?: (rows: GridRow[]) => void;
 	}
-}
+};
 
 export type GridRow = {
 	index: number;
@@ -48,7 +48,7 @@ export type GridRow = {
 	using: boolean;
 	setting: GridRowSetting;
 	additionalStyles: AdditionalStyle[];
-}
+};
 
 export function createRow(index: number, using: boolean, setting: GridRowSetting): GridRow {
 	return {
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 5b8ba77e01..9c8863f863 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-export type Keys =
+export type Keys = (
 	'v' |
 	'lastVersion' |
 	'instance' |
@@ -40,6 +40,7 @@ export type Keys =
 	'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
 	`channelLastReadedAt:${string}` |
 	`idbfallback::${string}`
+);
 
 // セッション毎に廃棄されるLocalStorage代替（セーフモードなどで使用できそう）
 //const safeSessionStorage = new Map<Keys, string>();
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index ec4ac9d91a..0374d03c48 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -65,7 +65,7 @@ export type RouterEvent = {
 		key: string;
 	}) => void;
 	same: () => void;
-}
+};
 
 export type Resolved = {
 	route: RouteDef;
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
index b69457e05e..5916efbc52 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
@@ -109,7 +109,7 @@ type GridItem = {
 	publicUrl?: string | null;
 	originalUrl?: string | null;
 	type: string | null;
-}
+};
 
 function setupGrid(): GridSetting {
 	const $style = useCssModule();
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
index da85d38a40..d6ee8ea49c 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
@@ -124,7 +124,7 @@ type GridItem = {
 	localOnly: boolean;
 	roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
 	type: string | null;
-}
+};
 
 function setupGrid(): GridSetting {
 	const $style = useCssModule();
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
index 21ffe558ca..609d445d79 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
@@ -168,7 +168,7 @@ type GridItem = {
 	url: string;
 	name: string;
 	host: string;
-}
+};
 
 function setupGrid(): GridSetting {
 	const $style = useCssModule();
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 4cb149a58b..14b9f7a741 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -93,7 +93,7 @@ const noteSearchableScope = instance.noteSearchableScope ?? 'local';
 
 const hostSelect = ref<'all' | 'local' | 'specified'>('all');
 
-const setHostSelectWithInput = (after:string|undefined|null, before:string|undefined|null) => {
+const setHostSelectWithInput = (after: string | undefined | null, before: string | undefined | null) => {
 	if (before === after) return;
 	if (after === '') hostSelect.value = 'all';
 	else hostSelect.value = 'specified';
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts
index e203c51bba..2c0c8c816e 100644
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ b/packages/frontend/src/scripts/aiscript/api.ts
@@ -76,7 +76,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
 				// バグがあればundefinedもあり得るため念のため
 				if (typeof token.value !== 'string') throw new Error('invalid token');
 			}
-			const actualToken: string|null = token?.value ?? opts.token ?? null;
+			const actualToken: string | null = token?.value ?? opts.token ?? null;
 			if (param == null) {
 				throw new errors.AiScriptRuntimeError('expected param');
 			}
diff --git a/packages/frontend/src/scripts/file-drop.ts b/packages/frontend/src/scripts/file-drop.ts
index c2e863c0dc..4259fe25e9 100644
--- a/packages/frontend/src/scripts/file-drop.ts
+++ b/packages/frontend/src/scripts/file-drop.ts
@@ -15,7 +15,7 @@ export type DroppedDirectory = {
 	isFile: false;
 	path: string;
 	children: DroppedItem[];
-}
+};
 
 export async function extractDroppedItems(ev: DragEvent): Promise<DroppedItem[]> {
 	const dropItems = ev.dataTransfer?.items;
diff --git a/packages/frontend/src/scripts/format-time-string.ts b/packages/frontend/src/scripts/format-time-string.ts
index 35ad77d982..d383f143e1 100644
--- a/packages/frontend/src/scripts/format-time-string.ts
+++ b/packages/frontend/src/scripts/format-time-string.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-const defaultLocaleStringFormats: {[index: string]: string} = {
+const defaultLocaleStringFormats: { [index: string]: string } = {
 	'weekday': 'narrow',
 	'era': 'narrow',
 	'year': 'numeric',
diff --git a/packages/frontend/src/scripts/key-event.ts b/packages/frontend/src/scripts/key-event.ts
index a72776d48c..020a6c2174 100644
--- a/packages/frontend/src/scripts/key-event.ts
+++ b/packages/frontend/src/scripts/key-event.ts
@@ -7,7 +7,7 @@
  * {@link KeyboardEvent.code} の値を表す文字列。不足分は適宜追加する
  * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
  */
-export type KeyCode =
+export type KeyCode = (
 	| 'Backspace'
 	| 'Tab'
 	| 'Enter'
@@ -94,32 +94,32 @@ export type KeyCode =
 	| 'Quote'
 	| 'Meta'
 	| 'AltGraph'
-	;
+);
 
 /**
  * 修飾キーを表す文字列。不足分は適宜追加する。
  */
-export type KeyModifier =
+export type KeyModifier = (
 	| 'Shift'
 	| 'Control'
 	| 'Alt'
 	| 'Meta'
-	;
+);
 
 /**
  * 押下されたキー以外の状態を表す文字列。不足分は適宜追加する。
  */
-export type KeyState =
+export type KeyState = (
 	| 'composing'
 	| 'repeat'
-	;
+);
 
 export type KeyEventHandler = {
 	modifiers?: KeyModifier[];
 	states?: KeyState[];
 	code: KeyCode | 'any';
 	handler: (event: KeyboardEvent) => void;
-}
+};
 
 export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) {
 	function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) {
diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts
index cb0e607fcb..9b1b368de4 100644
--- a/packages/frontend/src/scripts/stream-mock.ts
+++ b/packages/frontend/src/scripts/stream-mock.ts
@@ -37,9 +37,9 @@ export class StreamMock extends EventEmitter<StreamEvents> implements IStream {
 		// do nothing
 	}
 
-	public send(typeOrPayload: string): void
-	public send(typeOrPayload: string, payload: any): void
-	public send(typeOrPayload: Record<string, any> | any[]): void
+	public send(typeOrPayload: string): void;
+	public send(typeOrPayload: string, payload: any): void;
+	public send(typeOrPayload: Record<string, any> | any[]): void;
 	public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
 		// do nothing
 	}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index e9650bcf1f..16b900e052 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -56,7 +56,7 @@ export type SoundStore = {
 	fileUrl: string;
 
 	volume: number;
-}
+};
 
 export const postFormActions: PostFormAction[] = [];
 export const userActions: UserAction[] = [];
diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts
index 046799536f..7cadef136d 100644
--- a/packages/frontend/src/types/menu.ts
+++ b/packages/frontend/src/types/menu.ts
@@ -6,7 +6,7 @@
 import * as Misskey from 'misskey-js';
 import type { ComputedRef, Ref } from 'vue';
 
-interface MenuRadioOptionsDef extends Record<string, any> { }
+type MenuRadioOptionsDef = Record<string, any>;
 
 export type MenuAction = (ev: MouseEvent) => void;
 
diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json
index 97620e484a..96c453e73e 100644
--- a/packages/misskey-bubble-game/package.json
+++ b/packages/misskey-bubble-game/package.json
@@ -24,14 +24,14 @@
 	"devDependencies": {
 		"@types/matter-js": "0.19.7",
 		"@types/seedrandom": "3.0.8",
-		"@types/node": "22.9.0",
-		"@typescript-eslint/eslint-plugin": "7.1.0",
-		"@typescript-eslint/parser": "7.1.0",
-		"nodemon": "3.1.7",
+		"@types/node": "22.10.7",
+		"@typescript-eslint/eslint-plugin": "8.20.0",
+		"@typescript-eslint/parser": "8.20.0",
+		"nodemon": "3.1.9",
 		"execa": "8.0.1",
-		"typescript": "5.6.3",
-		"esbuild": "0.24.0",
-		"glob": "11.0.0"
+		"typescript": "5.7.3",
+		"esbuild": "0.24.2",
+		"glob": "11.0.1"
 	},
 	"files": [
 		"built"
diff --git a/packages/misskey-js/eslint.config.js b/packages/misskey-js/eslint.config.js
index d8173f30e9..496892d897 100644
--- a/packages/misskey-js/eslint.config.js
+++ b/packages/misskey-js/eslint.config.js
@@ -26,4 +26,10 @@ export default [
 			},
 		},
 	},
+	{
+		files: ['src/autogen/**/*.ts', 'src/autogen/**/*.tsx'],
+		rules: {
+			'@stylistic/indent': 'off',
+		},
+	},
 ];
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index ac7babb250..1b935364f1 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -7,7 +7,7 @@
 import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
 import { EventEmitter } from 'eventemitter3';
 import { Options } from 'reconnecting-websocket';
-import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
+import type { PublicKeyCredentialRequestOptionsJSON as PublicKeyCredentialRequestOptionsJSON_2 } from '@simplewebauthn/types';
 import _ReconnectingWebSocket from 'reconnecting-websocket';
 
 // Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
@@ -3106,12 +3106,12 @@ type SigninFlowResponse = {
 } | {
     finished: false;
     next: 'passkey';
-    authRequest: PublicKeyCredentialRequestOptionsJSON;
+    authRequest: PublicKeyCredentialRequestOptionsJSON_2;
 };
 
 // @public (undocumented)
 type SigninWithPasskeyInitResponse = {
-    option: PublicKeyCredentialRequestOptionsJSON;
+    option: PublicKeyCredentialRequestOptionsJSON_2;
     context: string;
 };
 
diff --git a/packages/misskey-js/generator/eslint.config.js b/packages/misskey-js/generator/eslint.config.js
index 4bf78c3b91..7139f88f5b 100644
--- a/packages/misskey-js/generator/eslint.config.js
+++ b/packages/misskey-js/generator/eslint.config.js
@@ -14,4 +14,10 @@ export default [
 			},
 		},
 	},
+	{
+		files: ['built/autogen/**.ts'],
+		rules: {
+			'@stylistic/indent': 'off',
+		},
+	},
 ];
diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json
index b796bf8775..4203456889 100644
--- a/packages/misskey-js/generator/package.json
+++ b/packages/misskey-js/generator/package.json
@@ -8,14 +8,14 @@
 	},
 	"devDependencies": {
 		"@readme/openapi-parser": "2.6.0",
-		"@types/node": "22.9.0",
-		"@typescript-eslint/eslint-plugin": "7.17.0",
-		"@typescript-eslint/parser": "7.17.0",
+		"@types/node": "22.10.7",
+		"@typescript-eslint/eslint-plugin": "8.20.0",
+		"@typescript-eslint/parser": "8.20.0",
 		"openapi-types": "12.1.3",
-		"openapi-typescript": "6.7.3",
+		"openapi-typescript": "6.7.6",
 		"ts-case-convert": "2.1.0",
-		"tsx": "4.4.0",
-		"typescript": "5.6.3"
+		"tsx": "4.19.2",
+		"typescript": "5.7.3"
 	},
 	"files": [
 		"built"
diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts
index a77ffeedc2..889051f3e8 100644
--- a/packages/misskey-js/generator/src/generator.ts
+++ b/packages/misskey-js/generator/src/generator.ts
@@ -173,7 +173,7 @@ async function generateEndpoints(
 	endpointOutputLine.push(
 		...endpoints.map(it => '\t' + it.toLine()),
 	);
-	endpointOutputLine.push('}');
+	endpointOutputLine.push('};');
 	endpointOutputLine.push('');
 
 	function generateEndpointReqMediaTypesType() {
diff --git a/packages/misskey-js/generator/tsconfig.json b/packages/misskey-js/generator/tsconfig.json
index c814df612e..d65042dc6d 100644
--- a/packages/misskey-js/generator/tsconfig.json
+++ b/packages/misskey-js/generator/tsconfig.json
@@ -3,7 +3,7 @@
 	"compilerOptions": {
 		"target": "ESNext",
 		"module": "ESNext",
-		"moduleResolution": "nodenext",
+		"moduleResolution": "node16",
 		"strict": true,
 		"strictFunctionTypes": true,
 		"strictNullChecks": true,
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 601c261a05..afc1e6a1d1 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -35,29 +35,29 @@
 		"directory": "packages/misskey-js"
 	},
 	"devDependencies": {
-		"@microsoft/api-extractor": "7.47.11",
+		"@microsoft/api-extractor": "7.49.1",
 		"@swc/jest": "0.2.37",
 		"@types/jest": "29.5.14",
-		"@types/node": "22.9.0",
-		"@typescript-eslint/eslint-plugin": "7.17.0",
-		"@typescript-eslint/parser": "7.17.0",
+		"@types/node": "22.10.7",
+		"@typescript-eslint/eslint-plugin": "8.20.0",
+		"@typescript-eslint/parser": "8.20.0",
 		"jest": "29.7.0",
 		"jest-fetch-mock": "3.0.3",
 		"jest-websocket-mock": "2.5.0",
 		"mock-socket": "9.3.1",
 		"ncp": "2.0.0",
-		"nodemon": "3.1.7",
+		"nodemon": "3.1.9",
 		"execa": "8.0.1",
 		"tsd": "0.31.2",
-		"typescript": "5.6.3",
-		"esbuild": "0.24.0",
-		"glob": "11.0.0"
+		"typescript": "5.7.3",
+		"esbuild": "0.24.2",
+		"glob": "11.0.1"
 	},
 	"files": [
 		"built"
 	],
 	"dependencies": {
-		"@simplewebauthn/types": "11.0.0",
+		"@simplewebauthn/types": "12.0.0",
 		"eventemitter3": "5.0.1",
 		"reconnecting-websocket": "4.4.0"
 	}
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index 838949f8e1..fa803194bf 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -33,14 +33,15 @@ type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C e
 	Endpoints[E]['res'] extends SwitchCase
 		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		? IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false
-		: false
+		: false;
 
 type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> =
 	Endpoints[E]['res'] extends SwitchCase
 		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		? StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1]
-		: never
+		: never;
 
+/* eslint-disable @stylistic/indent */
 export type SwitchCaseResponseType<E extends keyof Endpoints, P extends Endpoints[E]['req']> = Endpoints[E]['res'] extends SwitchCase
 	? IsCaseMatched<E, P, 0> extends true ? GetCaseResult<E, P, 0> :
 		IsCaseMatched<E, P, 1> extends true ? GetCaseResult<E, P, 1> :
@@ -53,6 +54,7 @@ export type SwitchCaseResponseType<E extends keyof Endpoints, P extends Endpoint
 									IsCaseMatched<E, P, 8> extends true ? GetCaseResult<E, P, 8> :
 										IsCaseMatched<E, P, 9> extends true ? GetCaseResult<E, P, 9> :
 											Endpoints[E]['res']['$switch']['$default'] : Endpoints[E]['res'];
+/* eslint-enable @stylistic/indent */
 
 export type Endpoints = Overwrite<
 	Gen,
@@ -106,4 +108,4 @@ export type Endpoints = Overwrite<
 			res: AdminRolesCreateResponse;
 		}
 	}
->
+>;
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index a9903b9139..2de0bc9808 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -973,7 +973,7 @@ export type Endpoints = {
 	'users/show': { req: UsersShowRequest; res: UsersShowResponse };
 	'users/update-memo': { req: UsersUpdateMemoRequest; res: EmptyResponse };
 	'v2/admin/emoji/list': { req: V2AdminEmojiListRequest; res: V2AdminEmojiListResponse };
-}
+};
 
 /**
  * NOTE: The content-type for all endpoints not listed here is application/json.
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index dd88791ed0..f04ab25148 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -258,11 +258,11 @@ export type SignupRequest = {
 	'g-recaptcha-response'?: string | null;
 	'turnstile-response'?: string | null;
 	'm-captcha-response'?: string | null;
-}
+};
 
 export type SignupResponse = MeDetailed & {
 	token: string;
-}
+};
 
 export type SignupPendingRequest = {
 	code: string;
@@ -313,4 +313,4 @@ export type SigninWithPasskeyResponse = {
 
 type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];
 
-export type PartialRolePolicyOverride = Partial<{[k in keyof RolePolicies]: Omit<Values<Role['policies']>, 'value'> & { value: RolePolicies[k] }}>;
+export type PartialRolePolicyOverride = Partial<{ [k in keyof RolePolicies]: Omit<Values<Role['policies']>, 'value'> & { value: RolePolicies[k] } }>;
diff --git a/packages/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts
index 0ef2d1e7a1..371f3f1d13 100644
--- a/packages/misskey-js/src/streaming.ts
+++ b/packages/misskey-js/src/streaming.ts
@@ -197,9 +197,9 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
 	 * Send a message to connection
 	 * ! ストリーム上のやり取りはすべてJSONで行われます !
 	 */
-	public send(typeOrPayload: string): void
-	public send(typeOrPayload: string, payload: unknown): void
-	public send(typeOrPayload: Record<string, unknown> | unknown[]): void
+	public send(typeOrPayload: string): void;
+	public send(typeOrPayload: string, payload: unknown): void;
+	public send(typeOrPayload: Record<string, unknown> | unknown[]): void;
 	public send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void {
 		if (typeof typeOrPayload === 'string') {
 			this.stream.send(JSON.stringify({
diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json
index 951dd1c65a..cc1ae90313 100644
--- a/packages/misskey-reversi/package.json
+++ b/packages/misskey-reversi/package.json
@@ -22,14 +22,14 @@
 		"lint": "pnpm typecheck && pnpm eslint"
 	},
 	"devDependencies": {
-		"@types/node": "22.9.0",
-		"@typescript-eslint/eslint-plugin": "7.1.0",
-		"@typescript-eslint/parser": "7.1.0",
+		"@types/node": "22.10.7",
+		"@typescript-eslint/eslint-plugin": "8.20.0",
+		"@typescript-eslint/parser": "8.20.0",
 		"execa": "8.0.1",
-		"nodemon": "3.1.7",
-		"typescript": "5.6.3",
-		"esbuild": "0.24.0",
-		"glob": "11.0.0"
+		"nodemon": "3.1.9",
+		"typescript": "5.7.3",
+		"esbuild": "0.24.2",
+		"glob": "11.0.1"
 	},
 	"files": [
 		"built"
diff --git a/packages/sw/package.json b/packages/sw/package.json
index dddd6e8bd7..95e1510181 100644
--- a/packages/sw/package.json
+++ b/packages/sw/package.json
@@ -9,16 +9,16 @@
 		"lint": "pnpm typecheck && pnpm eslint"
 	},
 	"dependencies": {
-		"esbuild": "0.24.0",
+		"esbuild": "0.24.2",
 		"idb-keyval": "6.2.1",
 		"misskey-js": "workspace:*"
 	},
 	"devDependencies": {
-		"@typescript-eslint/parser": "7.17.0",
+		"@typescript-eslint/parser": "8.20.0",
 		"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
-		"eslint-plugin-import": "2.30.0",
-		"nodemon": "3.1.7",
-		"typescript": "5.6.3"
+		"eslint-plugin-import": "2.31.0",
+		"nodemon": "3.1.9",
+		"typescript": "5.7.3"
 	},
 	"type": "module"
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 27dfcfccc6..3e8362347f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,20 +13,20 @@ importers:
   .:
     dependencies:
       cssnano:
-        specifier: 6.1.2
-        version: 6.1.2(postcss@8.4.49)
+        specifier: 7.0.6
+        version: 7.0.6(postcss@8.5.1)
       esbuild:
-        specifier: 0.24.0
-        version: 0.24.0
+        specifier: 0.24.2
+        version: 0.24.2
       execa:
         specifier: 8.0.1
         version: 8.0.1
       fast-glob:
-        specifier: 3.3.2
-        version: 3.3.2
+        specifier: 3.3.3
+        version: 3.3.3
       glob:
-        specifier: 11.0.0
-        version: 11.0.0
+        specifier: 11.0.1
+        version: 11.0.1
       ignore-walk:
         specifier: 6.0.5
         version: 6.0.5
@@ -34,52 +34,52 @@ importers:
         specifier: 4.1.0
         version: 4.1.0
       postcss:
-        specifier: 8.4.49
-        version: 8.4.49
+        specifier: 8.5.1
+        version: 8.5.1
       tar:
         specifier: 6.2.1
         version: 6.2.1
       terser:
-        specifier: 5.36.0
-        version: 5.36.0
+        specifier: 5.37.0
+        version: 5.37.0
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
     optionalDependencies:
       '@tensorflow/tfjs-core':
         specifier: 4.4.0
         version: 4.4.0(encoding@0.1.13)
     devDependencies:
       '@misskey-dev/eslint-plugin':
-        specifier: 2.0.3
-        version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3))(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0))(eslint@9.14.0)(globals@15.12.0)
+        specifier: 2.1.0
+        version: 2.1.0(@eslint/compat@1.1.1)(@stylistic/eslint-plugin@2.13.0(eslint@9.18.0)(typescript@5.7.3))(@typescript-eslint/eslint-plugin@8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3))(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0))(eslint@9.18.0)(globals@15.14.0)
       '@types/node':
-        specifier: 22.9.0
-        version: 22.9.0
+        specifier: 22.10.7
+        version: 22.10.7
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.17.0
-        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
-        specifier: 7.17.0
-        version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       cross-env:
         specifier: 7.0.3
         version: 7.0.3
       cypress:
-        specifier: 13.15.2
-        version: 13.15.2
+        specifier: 14.0.0
+        version: 14.0.0
       eslint:
-        specifier: 9.14.0
-        version: 9.14.0
+        specifier: 9.18.0
+        version: 9.18.0
       globals:
-        specifier: 15.12.0
-        version: 15.12.0
+        specifier: 15.14.0
+        version: 15.14.0
       ncp:
         specifier: 2.0.0
         version: 2.0.0
       start-server-and-test:
-        specifier: 2.0.8
-        version: 2.0.8
+        specifier: 2.0.10
+        version: 2.0.10
 
   packages/backend:
     dependencies:
@@ -90,71 +90,71 @@ importers:
         specifier: 3.620.0
         version: 3.620.0(@aws-sdk/client-s3@3.620.0)
       '@bull-board/api':
-        specifier: 6.5.0
-        version: 6.5.0(@bull-board/ui@6.5.0)
+        specifier: 6.7.0
+        version: 6.7.0(@bull-board/ui@6.7.0)
       '@bull-board/fastify':
-        specifier: 6.5.0
-        version: 6.5.0
+        specifier: 6.7.0
+        version: 6.7.0
       '@bull-board/ui':
-        specifier: 6.5.0
-        version: 6.5.0
+        specifier: 6.7.0
+        version: 6.7.0
       '@discordapp/twemoji':
         specifier: 15.1.0
         version: 15.1.0
       '@fastify/accepts':
-        specifier: 5.0.1
-        version: 5.0.1
+        specifier: 5.0.2
+        version: 5.0.2
       '@fastify/cookie':
-        specifier: 11.0.1
-        version: 11.0.1
+        specifier: 11.0.2
+        version: 11.0.2
       '@fastify/cors':
-        specifier: 10.0.1
-        version: 10.0.1
+        specifier: 10.0.2
+        version: 10.0.2
       '@fastify/express':
-        specifier: 4.0.1
-        version: 4.0.1
+        specifier: 4.0.2
+        version: 4.0.2
       '@fastify/http-proxy':
         specifier: 10.0.1
         version: 10.0.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       '@fastify/multipart':
-        specifier: 9.0.1
-        version: 9.0.1
+        specifier: 9.0.2
+        version: 9.0.2
       '@fastify/static':
-        specifier: 8.0.2
-        version: 8.0.2
+        specifier: 8.0.4
+        version: 8.0.4
       '@fastify/view':
-        specifier: 10.0.1
-        version: 10.0.1
+        specifier: 10.0.2
+        version: 10.0.2
       '@misskey-dev/sharp-read-bmp':
         specifier: 1.2.0
         version: 1.2.0
       '@misskey-dev/summaly':
-        specifier: 5.1.0
-        version: 5.1.0
+        specifier: 5.2.0
+        version: 5.2.0
       '@napi-rs/canvas':
-        specifier: 0.1.56
-        version: 0.1.56
+        specifier: 0.1.65
+        version: 0.1.65
       '@nestjs/common':
-        specifier: 10.4.7
-        version: 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)
+        specifier: 11.0.1
+        version: 11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1)
       '@nestjs/core':
-        specifier: 10.4.7
-        version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+        specifier: 11.0.1
+        version: 11.0.1(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)
       '@nestjs/testing':
-        specifier: 10.4.7
-        version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)
+        specifier: 11.0.1
+        version: 11.0.1(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.1)(@nestjs/platform-express@10.4.7)
       '@peertube/http-signature':
         specifier: 1.7.0
         version: 1.7.0
       '@sentry/node':
-        specifier: 8.38.0
-        version: 8.38.0
+        specifier: 8.50.0
+        version: 8.50.0
       '@sentry/profiling-node':
-        specifier: 8.38.0
-        version: 8.38.0
+        specifier: 8.50.0
+        version: 8.50.0
       '@simplewebauthn/server':
-        specifier: 10.0.1
-        version: 10.0.1(encoding@0.1.13)
+        specifier: 12.0.0
+        version: 12.0.0(encoding@0.1.13)
       '@sinonjs/fake-timers':
         specifier: 11.2.2
         version: 11.2.2
@@ -192,8 +192,8 @@ importers:
         specifier: 1.20.3
         version: 1.20.3
       bullmq:
-        specifier: 5.26.1
-        version: 5.26.1
+        specifier: 5.34.10
+        version: 5.34.10
       cacheable-lookup:
         specifier: 7.0.0
         version: 7.0.0
@@ -201,8 +201,8 @@ importers:
         specifier: 9.0.2
         version: 9.0.2
       chalk:
-        specifier: 5.3.0
-        version: 5.3.0
+        specifier: 5.4.1
+        version: 5.4.1
       chalk-template:
         specifier: 1.1.0
         version: 1.1.0
@@ -225,8 +225,8 @@ importers:
         specifier: 0.1.21
         version: 0.1.21
       fastify:
-        specifier: 5.0.0
-        version: 5.0.0
+        specifier: 5.2.1
+        version: 5.2.1
       fastify-raw-body:
         specifier: 5.0.0
         version: 5.0.0
@@ -243,11 +243,11 @@ importers:
         specifier: 4.0.1
         version: 4.0.1
       got:
-        specifier: 14.4.4
-        version: 14.4.4
+        specifier: 14.4.5
+        version: 14.4.5
       happy-dom:
-        specifier: 15.11.4
-        version: 15.11.4
+        specifier: 16.6.0
+        version: 16.6.0
       hpagent:
         specifier: 1.2.0
         version: 1.2.0
@@ -258,8 +258,8 @@ importers:
         specifier: 1.1.3
         version: 1.1.3
       ioredis:
-        specifier: 5.4.1
-        version: 5.4.1
+        specifier: 5.4.2
+        version: 5.4.2
       ip-cidr:
         specifier: 4.0.2
         version: 4.0.2
@@ -273,14 +273,14 @@ importers:
         specifier: 4.1.0
         version: 4.1.0
       jsdom:
-        specifier: 24.1.1
-        version: 24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+        specifier: 26.0.0
+        version: 26.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       json5:
         specifier: 2.2.3
         version: 2.2.3
       jsonld:
-        specifier: 8.3.2
-        version: 8.3.2(web-streams-polyfill@4.0.0)
+        specifier: 8.3.3
+        version: 8.3.3(web-streams-polyfill@4.0.0)
       jsrsasign:
         specifier: 11.1.0
         version: 11.1.0
@@ -288,8 +288,8 @@ importers:
         specifier: 11.0.0
         version: 11.0.0
       meilisearch:
-        specifier: 0.45.0
-        version: 0.45.0
+        specifier: 0.48.2
+        version: 0.48.2
       mfm-js:
         specifier: 0.24.0
         version: 0.24.0
@@ -309,8 +309,8 @@ importers:
         specifier: 3.0.0-canary.1
         version: 3.0.0-canary.1
       nanoid:
-        specifier: 5.0.8
-        version: 5.0.8
+        specifier: 5.0.9
+        version: 5.0.9
       nested-property:
         specifier: 4.0.0
         version: 4.0.0
@@ -336,8 +336,8 @@ importers:
         specifier: 0.0.14
         version: 0.0.14
       otpauth:
-        specifier: 9.3.4
-        version: 9.3.4
+        specifier: 9.3.6
+        version: 9.3.6
       parse5:
         specifier: 7.2.1
         version: 7.2.1
@@ -384,11 +384,11 @@ importers:
         specifier: 7.8.1
         version: 7.8.1
       sanitize-html:
-        specifier: 2.13.1
-        version: 2.13.1
+        specifier: 2.14.0
+        version: 2.14.0
       secure-json-parse:
-        specifier: 2.7.0
-        version: 2.7.0
+        specifier: 3.0.2
+        version: 3.0.2
       sharp:
         specifier: 0.33.5
         version: 0.33.5
@@ -402,8 +402,8 @@ importers:
         specifier: 2.1.0
         version: 2.1.0
       systeminformation:
-        specifier: 5.23.5
-        version: 5.23.5
+        specifier: 5.25.11
+        version: 5.25.11
       tinycolor2:
         specifier: 1.6.0
         version: 1.6.0
@@ -418,10 +418,10 @@ importers:
         version: 4.2.0
       typeorm:
         specifier: 0.3.20
-        version: 0.3.20(ioredis@5.4.1)(pg@8.13.1)
+        version: 0.3.20(ioredis@5.4.2)(pg@8.13.1)
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
       ulid:
         specifier: 2.3.0
         version: 2.3.0
@@ -531,10 +531,10 @@ importers:
         version: 29.7.0
       '@nestjs/platform-express':
         specifier: 10.4.7
-        version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)
+        version: 10.4.7(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.1)
       '@simplewebauthn/types':
-        specifier: 10.0.0
-        version: 10.0.0
+        specifier: 12.0.0
+        version: 12.0.0
       '@swc/jest':
         specifier: 0.2.37
         version: 0.2.37(@swc/core@1.9.2)
@@ -578,8 +578,8 @@ importers:
         specifier: 1.5.15
         version: 1.5.15
       '@types/jsrsasign':
-        specifier: 10.5.14
-        version: 10.5.14
+        specifier: 10.5.15
+        version: 10.5.15
       '@types/mime-types':
         specifier: 2.1.4
         version: 2.1.4
@@ -587,11 +587,11 @@ importers:
         specifier: 0.7.34
         version: 0.7.34
       '@types/node':
-        specifier: 22.9.0
-        version: 22.9.0
+        specifier: 22.10.7
+        version: 22.10.7
       '@types/nodemailer':
-        specifier: 6.4.16
-        version: 6.4.16
+        specifier: 6.4.17
+        version: 6.4.17
       '@types/oauth':
         specifier: 0.9.6
         version: 0.9.6
@@ -647,11 +647,11 @@ importers:
         specifier: 8.5.13
         version: 8.5.13
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.17.0
-        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
-        specifier: 7.17.0
-        version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       aws-sdk-client-mock:
         specifier: 4.0.1
         version: 4.0.1
@@ -659,8 +659,8 @@ importers:
         specifier: 7.0.3
         version: 7.0.3
       eslint-plugin-import:
-        specifier: 2.30.0
-        version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)
+        specifier: 2.31.0
+        version: 2.31.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)
       execa:
         specifier: 8.0.1
         version: 8.0.1
@@ -669,13 +669,13 @@ importers:
         version: 9.0.0
       jest:
         specifier: 29.7.0
-        version: 29.7.0(@types/node@22.9.0)
+        version: 29.7.0(@types/node@22.10.7)
       jest-mock:
         specifier: 29.7.0
         version: 29.7.0
       nodemon:
-        specifier: 3.1.7
-        version: 3.1.7
+        specifier: 3.1.9
+        version: 3.1.9
       pid-port:
         specifier: 1.0.0
         version: 1.0.0
@@ -699,28 +699,28 @@ importers:
         version: 2024.1.0
       '@rollup/plugin-json':
         specifier: 6.1.0
-        version: 6.1.0(rollup@4.26.0)
+        version: 6.1.0(rollup@4.31.0)
       '@rollup/plugin-replace':
         specifier: 5.0.7
-        version: 5.0.7(rollup@4.26.0)
+        version: 5.0.7(rollup@4.31.0)
       '@rollup/pluginutils':
-        specifier: 5.1.3
-        version: 5.1.3(rollup@4.26.0)
+        specifier: 5.1.4
+        version: 5.1.4(rollup@4.31.0)
       '@syuilo/aiscript':
         specifier: 0.19.0
         version: 0.19.0
       '@tabler/icons-webfont':
-        specifier: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz
-        version: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz
+        specifier: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz
+        version: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz
       '@twemoji/parser':
         specifier: 15.1.1
         version: 15.1.1
       '@vitejs/plugin-vue':
-        specifier: 5.2.0
-        version: 5.2.0(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))
+        specifier: 5.2.1
+        version: 5.2.1(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.7.3))
       '@vue/compiler-sfc':
-        specifier: 3.5.12
-        version: 3.5.12
+        specifier: 3.5.13
+        version: 3.5.13
       aiscript-vscode:
         specifier: github:aiscript-dev/aiscript-vscode#v0.1.15
         version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7
@@ -737,23 +737,23 @@ importers:
         specifier: 1.9.3
         version: 1.9.3
       chart.js:
-        specifier: 4.4.6
-        version: 4.4.6
+        specifier: 4.4.7
+        version: 4.4.7
       chartjs-adapter-date-fns:
         specifier: 3.0.0
-        version: 3.0.0(chart.js@4.4.6)(date-fns@2.30.0)
+        version: 3.0.0(chart.js@4.4.7)(date-fns@2.30.0)
       chartjs-chart-matrix:
         specifier: 2.0.1
-        version: 2.0.1(chart.js@4.4.6)
+        version: 2.0.1(chart.js@4.4.7)
       chartjs-plugin-gradient:
         specifier: 0.6.1
-        version: 0.6.1(chart.js@4.4.6)
+        version: 0.6.1(chart.js@4.4.7)
       chartjs-plugin-zoom:
-        specifier: 2.0.1
-        version: 2.0.1(chart.js@4.4.6)
+        specifier: 2.2.0
+        version: 2.2.0(chart.js@4.4.7)
       chromatic:
-        specifier: 11.18.1
-        version: 11.18.1
+        specifier: 11.25.0
+        version: 11.25.0
       compare-versions:
         specifier: 6.1.1
         version: 6.1.1
@@ -806,17 +806,17 @@ importers:
         specifier: 2.3.1
         version: 2.3.1
       rollup:
-        specifier: 4.26.0
-        version: 4.26.0
+        specifier: 4.31.0
+        version: 4.31.0
       sanitize-html:
-        specifier: 2.13.1
-        version: 2.13.1
+        specifier: 2.14.0
+        version: 2.14.0
       sass:
-        specifier: 1.79.3
-        version: 1.79.3
+        specifier: 1.83.4
+        version: 1.83.4
       shiki:
-        specifier: 1.22.2
-        version: 1.22.2
+        specifier: 1.27.2
+        version: 1.27.2
       strict-event-emitter-types:
         specifier: 2.0.0
         version: 2.0.0
@@ -824,8 +824,8 @@ importers:
         specifier: 3.1.0
         version: 3.1.0
       three:
-        specifier: 0.169.0
-        version: 0.169.0
+        specifier: 0.172.0
+        version: 0.172.0
       throttle-debounce:
         specifier: 5.0.2
         version: 5.0.2
@@ -839,86 +839,86 @@ importers:
         specifier: 4.2.0
         version: 4.2.0
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
       uuid:
-        specifier: 10.0.0
-        version: 10.0.0
+        specifier: 11.0.5
+        version: 11.0.5
       v-code-diff:
         specifier: 1.13.1
-        version: 1.13.1(vue@3.5.12(typescript@5.6.3))
+        version: 1.13.1(vue@3.5.13(typescript@5.7.3))
       vite:
-        specifier: 5.4.11
-        version: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
+        specifier: 6.0.7
+        version: 6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2)
       vue:
-        specifier: 3.5.12
-        version: 3.5.12(typescript@5.6.3)
+        specifier: 3.5.13
+        version: 3.5.13(typescript@5.7.3)
       vuedraggable:
         specifier: next
-        version: 4.1.0(vue@3.5.12(typescript@5.6.3))
+        version: 4.1.0(vue@3.5.13(typescript@5.7.3))
     devDependencies:
       '@misskey-dev/summaly':
-        specifier: 5.1.0
-        version: 5.1.0
+        specifier: 5.2.0
+        version: 5.2.0
       '@storybook/addon-actions':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/addon-essentials':
-        specifier: 8.4.4
-        version: 8.4.4(@types/react@18.0.28)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(@types/react@18.0.28)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/addon-interactions':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/addon-links':
-        specifier: 8.4.4
-        version: 8.4.4(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/addon-mdx-gfm':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/addon-storysource':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/blocks':
-        specifier: 8.4.4
-        version: 8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/components':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/core-events':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/manager-api':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/preview-api':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/react':
-        specifier: 8.4.4
-        version: 8.4.4(@storybook/test@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(typescript@5.6.3)
+        specifier: 8.5.0
+        version: 8.5.0(@storybook/test@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(typescript@5.7.3)
       '@storybook/react-vite':
-        specifier: 8.4.4
-        version: 8.4.4(@storybook/test@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.26.0)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))
+        specifier: 8.5.0
+        version: 8.5.0(@storybook/test@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.31.0)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(typescript@5.7.3)(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))
       '@storybook/test':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/theming':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/types':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/vue3':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(vue@3.5.12(typescript@5.6.3))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(vue@3.5.13(typescript@5.7.3))
       '@storybook/vue3-vite':
-        specifier: 8.4.4
-        version: 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))
+        specifier: 8.5.0
+        version: 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.7.3))
       '@testing-library/vue':
         specifier: 8.1.0
-        version: 8.1.0(@vue/compiler-sfc@3.5.12)(@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))
+        version: 8.1.0(@vue/compiler-sfc@3.5.13)(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
       '@types/canvas-confetti':
-        specifier: ^1.6.4
+        specifier: 1.6.4
         version: 1.6.4
       '@types/estree':
         specifier: 1.0.6
@@ -930,8 +930,8 @@ importers:
         specifier: 4.0.9
         version: 4.0.9
       '@types/node':
-        specifier: 22.9.0
-        version: 22.9.0
+        specifier: 22.10.7
+        version: 22.10.7
       '@types/punycode.js':
         specifier: npm:@types/punycode@2.1.4
         version: '@types/punycode@2.1.4'
@@ -954,17 +954,17 @@ importers:
         specifier: 8.5.13
         version: 8.5.13
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.17.0
-        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
-        specifier: 7.17.0
-        version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       '@vitest/coverage-v8':
         specifier: 1.6.0
-        version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.36.0))
+        version: 1.6.0(vitest@1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0))
       '@vue/runtime-core':
-        specifier: 3.5.12
-        version: 3.5.12
+        specifier: 3.5.13
+        version: 3.5.13
       acorn:
         specifier: 8.14.0
         version: 8.14.0
@@ -972,20 +972,20 @@ importers:
         specifier: 7.0.3
         version: 7.0.3
       cypress:
-        specifier: 13.15.2
-        version: 13.15.2
+        specifier: 14.0.0
+        version: 14.0.0
       eslint-plugin-import:
         specifier: 2.31.0
-        version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)
+        version: 2.31.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)
       eslint-plugin-vue:
-        specifier: 9.31.0
-        version: 9.31.0(eslint@9.14.0)
+        specifier: 9.32.0
+        version: 9.32.0(eslint@9.18.0)
       fast-glob:
-        specifier: 3.3.2
-        version: 3.3.2
+        specifier: 3.3.3
+        version: 3.3.3
       happy-dom:
-        specifier: 10.0.3
-        version: 10.0.3
+        specifier: 16.6.0
+        version: 16.6.0
       intersection-observer:
         specifier: 0.12.2
         version: 0.12.2
@@ -993,17 +993,17 @@ importers:
         specifier: 4.0.8
         version: 4.0.8
       msw:
-        specifier: 2.6.4
-        version: 2.6.4(@types/node@22.9.0)(typescript@5.6.3)
+        specifier: 2.7.0
+        version: 2.7.0(@types/node@22.10.7)(typescript@5.7.3)
       msw-storybook-addon:
         specifier: 2.0.4
-        version: 2.0.4(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3))
+        version: 2.0.4(msw@2.7.0(@types/node@22.10.7)(typescript@5.7.3))
       nodemon:
-        specifier: 3.1.7
-        version: 3.1.7
+        specifier: 3.1.9
+        version: 3.1.9
       prettier:
-        specifier: 3.3.3
-        version: 3.3.3
+        specifier: 3.4.2
+        version: 3.4.2
       react:
         specifier: 18.3.1
         version: 18.3.1
@@ -1014,32 +1014,32 @@ importers:
         specifier: 3.0.5
         version: 3.0.5
       start-server-and-test:
-        specifier: 2.0.8
-        version: 2.0.8
+        specifier: 2.0.10
+        version: 2.0.10
       storybook:
-        specifier: 8.4.4
-        version: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+        specifier: 8.5.0
+        version: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       storybook-addon-misskey-theme:
         specifier: github:misskey-dev/storybook-addon-misskey-theme
-        version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/components@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/core-events@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/theming@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/types@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/components@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/core-events@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/theming@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/types@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       vite-plugin-turbosnap:
         specifier: 1.0.3
         version: 1.0.3
       vitest:
         specifier: 1.6.0
-        version: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.36.0)
+        version: 1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0)
       vitest-fetch-mock:
         specifier: 0.2.2
-        version: 0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.36.0))
+        version: 0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0))
       vue-component-type-helpers:
-        specifier: 2.1.10
-        version: 2.1.10
+        specifier: 2.2.0
+        version: 2.2.0
       vue-eslint-parser:
         specifier: 9.4.3
-        version: 9.4.3(eslint@9.14.0)
+        version: 9.4.3(eslint@9.18.0)
       vue-tsc:
-        specifier: 2.1.10
-        version: 2.1.10(typescript@5.6.3)
+        specifier: 2.2.0
+        version: 2.2.0(typescript@5.7.3)
 
   packages/frontend-embed:
     dependencies:
@@ -1048,25 +1048,25 @@ importers:
         version: 15.1.0
       '@rollup/plugin-json':
         specifier: 6.1.0
-        version: 6.1.0(rollup@4.26.0)
+        version: 6.1.0(rollup@4.31.0)
       '@rollup/plugin-replace':
         specifier: 5.0.7
-        version: 5.0.7(rollup@4.26.0)
+        version: 5.0.7(rollup@4.31.0)
       '@rollup/pluginutils':
-        specifier: 5.1.3
-        version: 5.1.3(rollup@4.26.0)
+        specifier: 5.1.4
+        version: 5.1.4(rollup@4.31.0)
       '@tabler/icons-webfont':
-        specifier: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz
-        version: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz
+        specifier: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz
+        version: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz
       '@twemoji/parser':
         specifier: 15.1.1
         version: 15.1.1
       '@vitejs/plugin-vue':
-        specifier: 5.2.0
-        version: 5.2.0(vite@5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))
+        specifier: 5.2.1
+        version: 5.2.1(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.7.3))
       '@vue/compiler-sfc':
-        specifier: 3.5.12
-        version: 3.5.12
+        specifier: 3.5.13
+        version: 3.5.13
       astring:
         specifier: 1.9.0
         version: 1.9.0
@@ -1092,14 +1092,14 @@ importers:
         specifier: 2.3.1
         version: 2.3.1
       rollup:
-        specifier: 4.26.0
-        version: 4.26.0
+        specifier: 4.31.0
+        version: 4.31.0
       sass:
-        specifier: 1.79.4
-        version: 1.79.4
+        specifier: 1.83.4
+        version: 1.83.4
       shiki:
-        specifier: 1.22.2
-        version: 1.22.2
+        specifier: 1.27.2
+        version: 1.27.2
       tinycolor2:
         specifier: 1.6.0
         version: 1.6.0
@@ -1110,24 +1110,24 @@ importers:
         specifier: 4.2.0
         version: 4.2.0
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
       uuid:
         specifier: 10.0.0
         version: 10.0.0
       vite:
-        specifier: 5.4.11
-        version: 5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0)
+        specifier: 6.0.7
+        version: 6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2)
       vue:
-        specifier: 3.5.12
-        version: 3.5.12(typescript@5.6.3)
+        specifier: 3.5.13
+        version: 3.5.13(typescript@5.7.3)
     devDependencies:
       '@misskey-dev/summaly':
-        specifier: 5.1.0
-        version: 5.1.0
+        specifier: 5.2.0
+        version: 5.2.0
       '@testing-library/vue':
         specifier: 8.1.0
-        version: 8.1.0(@vue/compiler-sfc@3.5.12)(@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))
+        version: 8.1.0(@vue/compiler-sfc@3.5.13)(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
       '@types/estree':
         specifier: 1.0.6
         version: 1.0.6
@@ -1135,8 +1135,8 @@ importers:
         specifier: 4.0.9
         version: 4.0.9
       '@types/node':
-        specifier: 22.9.0
-        version: 22.9.0
+        specifier: 22.10.7
+        version: 22.10.7
       '@types/punycode.js':
         specifier: npm:@types/punycode@2.1.4
         version: '@types/punycode@2.1.4'
@@ -1150,17 +1150,17 @@ importers:
         specifier: 8.5.13
         version: 8.5.13
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.17.0
-        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
-        specifier: 7.17.0
-        version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       '@vitest/coverage-v8':
         specifier: 1.6.0
-        version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))
+        version: 1.6.0(vitest@1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0))
       '@vue/runtime-core':
-        specifier: 3.5.12
-        version: 3.5.12
+        specifier: 3.5.13
+        version: 3.5.13
       acorn:
         specifier: 8.14.0
         version: 8.14.0
@@ -1169,16 +1169,16 @@ importers:
         version: 7.0.3
       eslint-plugin-import:
         specifier: 2.31.0
-        version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)
+        version: 2.31.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)
       eslint-plugin-vue:
-        specifier: 9.31.0
-        version: 9.31.0(eslint@9.14.0)
+        specifier: 9.32.0
+        version: 9.32.0(eslint@9.18.0)
       fast-glob:
-        specifier: 3.3.2
-        version: 3.3.2
+        specifier: 3.3.3
+        version: 3.3.3
       happy-dom:
-        specifier: 10.0.3
-        version: 10.0.3
+        specifier: 16.6.0
+        version: 16.6.0
       intersection-observer:
         specifier: 0.12.2
         version: 0.12.2
@@ -1186,29 +1186,29 @@ importers:
         specifier: 4.0.8
         version: 4.0.8
       msw:
-        specifier: 2.6.4
-        version: 2.6.4(@types/node@22.9.0)(typescript@5.6.3)
+        specifier: 2.7.0
+        version: 2.7.0(@types/node@22.10.7)(typescript@5.7.3)
       nodemon:
-        specifier: 3.1.7
-        version: 3.1.7
+        specifier: 3.1.9
+        version: 3.1.9
       prettier:
-        specifier: 3.3.3
-        version: 3.3.3
+        specifier: 3.4.2
+        version: 3.4.2
       start-server-and-test:
-        specifier: 2.0.8
-        version: 2.0.8
+        specifier: 2.0.10
+        version: 2.0.10
       vite-plugin-turbosnap:
         specifier: 1.0.3
         version: 1.0.3
       vue-component-type-helpers:
-        specifier: 2.1.10
-        version: 2.1.10
+        specifier: 2.2.0
+        version: 2.2.0
       vue-eslint-parser:
         specifier: 9.4.3
-        version: 9.4.3(eslint@9.14.0)
+        version: 9.4.3(eslint@9.18.0)
       vue-tsc:
-        specifier: 2.1.10
-        version: 2.1.10(typescript@5.6.3)
+        specifier: 2.2.0
+        version: 2.2.0(typescript@5.7.3)
 
   packages/frontend-shared:
     dependencies:
@@ -1224,16 +1224,16 @@ importers:
         version: 22.9.0
       '@typescript-eslint/eslint-plugin':
         specifier: 7.17.0
-        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.18.0)(typescript@5.6.3))(eslint@9.18.0)(typescript@5.6.3)
       '@typescript-eslint/parser':
         specifier: 7.17.0
-        version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+        version: 7.17.0(eslint@9.18.0)(typescript@5.6.3)
       esbuild:
         specifier: 0.24.0
         version: 0.24.0
       eslint-plugin-vue:
         specifier: 9.31.0
-        version: 9.31.0(eslint@9.14.0)
+        version: 9.31.0(eslint@9.18.0)
       nodemon:
         specifier: 3.1.7
         version: 3.1.7
@@ -1242,7 +1242,7 @@ importers:
         version: 5.6.3
       vue-eslint-parser:
         specifier: 9.4.3
-        version: 9.4.3(eslint@9.14.0)
+        version: 9.4.3(eslint@9.18.0)
 
   packages/misskey-bubble-game:
     dependencies:
@@ -1260,38 +1260,38 @@ importers:
         specifier: 0.19.7
         version: 0.19.7
       '@types/node':
-        specifier: 22.9.0
-        version: 22.9.0
+        specifier: 22.10.7
+        version: 22.10.7
       '@types/seedrandom':
         specifier: 3.0.8
         version: 3.0.8
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.1.0
-        version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
-        specifier: 7.1.0
-        version: 7.1.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       esbuild:
-        specifier: 0.24.0
-        version: 0.24.0
+        specifier: 0.24.2
+        version: 0.24.2
       execa:
         specifier: 8.0.1
         version: 8.0.1
       glob:
-        specifier: 11.0.0
-        version: 11.0.0
+        specifier: 11.0.1
+        version: 11.0.1
       nodemon:
-        specifier: 3.1.7
-        version: 3.1.7
+        specifier: 3.1.9
+        version: 3.1.9
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
 
   packages/misskey-js:
     dependencies:
       '@simplewebauthn/types':
-        specifier: 11.0.0
-        version: 11.0.0
+        specifier: 12.0.0
+        version: 12.0.0
       eventemitter3:
         specifier: 5.0.1
         version: 5.0.1
@@ -1300,8 +1300,8 @@ importers:
         version: 4.4.0
     devDependencies:
       '@microsoft/api-extractor':
-        specifier: 7.47.11
-        version: 7.47.11(@types/node@22.9.0)
+        specifier: 7.49.1
+        version: 7.49.1(@types/node@22.10.7)
       '@swc/jest':
         specifier: 0.2.37
         version: 0.2.37(@swc/core@1.9.2)
@@ -1309,26 +1309,26 @@ importers:
         specifier: 29.5.14
         version: 29.5.14
       '@types/node':
-        specifier: 22.9.0
-        version: 22.9.0
+        specifier: 22.10.7
+        version: 22.10.7
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.17.0
-        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
-        specifier: 7.17.0
-        version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       esbuild:
-        specifier: 0.24.0
-        version: 0.24.0
+        specifier: 0.24.2
+        version: 0.24.2
       execa:
         specifier: 8.0.1
         version: 8.0.1
       glob:
-        specifier: 11.0.0
-        version: 11.0.0
+        specifier: 11.0.1
+        version: 11.0.1
       jest:
         specifier: 29.7.0
-        version: 29.7.0(@types/node@22.9.0)
+        version: 29.7.0(@types/node@22.10.7)
       jest-fetch-mock:
         specifier: 3.0.3
         version: 3.0.3(encoding@0.1.13)
@@ -1342,14 +1342,14 @@ importers:
         specifier: 2.0.0
         version: 2.0.0
       nodemon:
-        specifier: 3.1.7
-        version: 3.1.7
+        specifier: 3.1.9
+        version: 3.1.9
       tsd:
         specifier: 0.31.2
         version: 0.31.2
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
 
   packages/misskey-js/generator:
     devDependencies:
@@ -1357,29 +1357,29 @@ importers:
         specifier: 2.6.0
         version: 2.6.0(openapi-types@12.1.3)
       '@types/node':
-        specifier: 22.9.0
-        version: 22.9.0
+        specifier: 22.10.7
+        version: 22.10.7
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.17.0
-        version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
-        specifier: 7.17.0
-        version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       openapi-types:
         specifier: 12.1.3
         version: 12.1.3
       openapi-typescript:
-        specifier: 6.7.3
-        version: 6.7.3
+        specifier: 6.7.6
+        version: 6.7.6
       ts-case-convert:
         specifier: 2.1.0
         version: 2.1.0
       tsx:
-        specifier: 4.4.0
-        version: 4.4.0
+        specifier: 4.19.2
+        version: 4.19.2
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
 
   packages/misskey-reversi:
     dependencies:
@@ -1388,35 +1388,35 @@ importers:
         version: 1.2.2
     devDependencies:
       '@types/node':
-        specifier: 22.9.0
-        version: 22.9.0
+        specifier: 22.10.7
+        version: 22.10.7
       '@typescript-eslint/eslint-plugin':
-        specifier: 7.1.0
-        version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
-        specifier: 7.1.0
-        version: 7.1.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       esbuild:
-        specifier: 0.24.0
-        version: 0.24.0
+        specifier: 0.24.2
+        version: 0.24.2
       execa:
         specifier: 8.0.1
         version: 8.0.1
       glob:
-        specifier: 11.0.0
-        version: 11.0.0
+        specifier: 11.0.1
+        version: 11.0.1
       nodemon:
-        specifier: 3.1.7
-        version: 3.1.7
+        specifier: 3.1.9
+        version: 3.1.9
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
 
   packages/sw:
     dependencies:
       esbuild:
-        specifier: 0.24.0
-        version: 0.24.0
+        specifier: 0.24.2
+        version: 0.24.2
       idb-keyval:
         specifier: 6.2.1
         version: 6.2.1
@@ -1425,20 +1425,20 @@ importers:
         version: link:../misskey-js
     devDependencies:
       '@typescript-eslint/parser':
-        specifier: 7.17.0
-        version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+        specifier: 8.20.0
+        version: 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       '@typescript/lib-webworker':
         specifier: npm:@types/serviceworker@0.0.67
         version: '@types/serviceworker@0.0.67'
       eslint-plugin-import:
-        specifier: 2.30.0
-        version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)
+        specifier: 2.31.0
+        version: 2.31.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)
       nodemon:
-        specifier: 3.1.7
-        version: 3.1.7
+        specifier: 3.1.9
+        version: 3.1.9
       typescript:
-        specifier: 5.6.3
-        version: 5.6.3
+        specifier: 5.7.3
+        version: 5.7.3
 
 packages:
 
@@ -1457,6 +1457,9 @@ packages:
   '@apidevtools/swagger-methods@3.0.2':
     resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
 
+  '@asamuzakjp/css-color@2.8.3':
+    resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==}
+
   '@aws-crypto/crc32@5.2.0':
     resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
     engines: {node: '>=16.0.0'}
@@ -1670,26 +1673,14 @@ packages:
     resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-environment-visitor@7.22.20':
-    resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/helper-environment-visitor@7.24.7':
     resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-function-name@7.23.0':
-    resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/helper-function-name@7.24.7':
     resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-hoist-variables@7.22.5':
-    resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/helper-hoist-variables@7.24.7':
     resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==}
     engines: {node: '>=6.9.0'}
@@ -1726,10 +1717,6 @@ packages:
     resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-split-export-declaration@7.22.6':
-    resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/helper-split-export-declaration@7.24.7':
     resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==}
     engines: {node: '>=6.9.0'}
@@ -1869,10 +1856,6 @@ packages:
     resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/traverse@7.23.5':
-    resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/traverse@7.24.7':
     resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==}
     engines: {node: '>=6.9.0'}
@@ -1888,16 +1871,16 @@ packages:
   '@bcoe/v8-coverage@0.2.3':
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
 
-  '@bull-board/api@6.5.0':
-    resolution: {integrity: sha512-sFHxmqtbBBkQaJxHdRq2sAR0+l9TBInItXaIdBMjeBXrFW881g4aLAbO7Lno6cDPKBMauYg4TdBtRPTkjhr43w==}
+  '@bull-board/api@6.7.0':
+    resolution: {integrity: sha512-1kUTVc7UCodShxMU2nVJA8yY4N1GlypY2tTx5dghlWo/ngrbujgirD4BgEKW+K4uSVGnKZkd82HbWGGXuzzUAA==}
     peerDependencies:
-      '@bull-board/ui': 6.5.0
+      '@bull-board/ui': 6.7.0
 
-  '@bull-board/fastify@6.5.0':
-    resolution: {integrity: sha512-oPLqIJPkis13WMPeuephkGeP/++AB5Qw3aw0qESU1K+e1pdwzf1kYIFpOZuTe9L/MrEulKe2ZgENw6RIgC1RBw==}
+  '@bull-board/fastify@6.7.0':
+    resolution: {integrity: sha512-I53F9doNWpHiy/EYeG7wrmSFUDvPwfTKYD8R3fnzV2EIwlDQa2Eo8wMNCbrpyiWI5hA5UsXLxL6g12yXa55vFg==}
 
-  '@bull-board/ui@6.5.0':
-    resolution: {integrity: sha512-gIoOgKNVAnPdKBBUaBUSNS2cBJz8UYGfEuYzD/H9HIpkCHiPTUVoMO48w/D+urJoko2nW8OSkU1kf2OkZsqP0Q==}
+  '@bull-board/ui@6.7.0':
+    resolution: {integrity: sha512-f398IerHbJTeIubAbeOTEfLJa06Tw6ameSidAlwn7m6jK7oXqbKiqtrBNQ4H1BG68EQ5KNLRvBxDe5QMMVEIZA==}
 
   '@bundled-es-modules/cookie@2.0.1':
     resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==}
@@ -1911,6 +1894,9 @@ packages:
   '@canvas/image-data@1.0.0':
     resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==}
 
+  '@chainsafe/is-ip@2.1.0':
+    resolution: {integrity: sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w==}
+
   '@colors/colors@1.5.0':
     resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
     engines: {node: '>=0.1.90'}
@@ -1948,6 +1934,34 @@ packages:
   '@cropper/utils@2.0.0-rc.2':
     resolution: {integrity: sha512-EEivNsyV6BtL496m4Q/IeAC6FGlyKjKIT1qMtwaxtkR+2ZlKnf9O7AdcGpClemIBA+TbwWAzp0UyIvYFtKUZ1Q==}
 
+  '@csstools/color-helpers@5.0.1':
+    resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==}
+    engines: {node: '>=18'}
+
+  '@csstools/css-calc@2.1.1':
+    resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@csstools/css-parser-algorithms': ^3.0.4
+      '@csstools/css-tokenizer': ^3.0.3
+
+  '@csstools/css-color-parser@3.0.7':
+    resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@csstools/css-parser-algorithms': ^3.0.4
+      '@csstools/css-tokenizer': ^3.0.3
+
+  '@csstools/css-parser-algorithms@3.0.4':
+    resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@csstools/css-tokenizer': ^3.0.3
+
+  '@csstools/css-tokenizer@3.0.3':
+    resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
+    engines: {node: '>=18'}
+
   '@cypress/request@3.0.6':
     resolution: {integrity: sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg==}
     engines: {node: '>= 6'}
@@ -1971,17 +1985,23 @@ packages:
     cpu: [ppc64]
     os: [aix]
 
+  '@esbuild/aix-ppc64@0.23.1':
+    resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
   '@esbuild/aix-ppc64@0.24.0':
     resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==}
     engines: {node: '>=18'}
     cpu: [ppc64]
     os: [aix]
 
-  '@esbuild/android-arm64@0.18.20':
-    resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [android]
+  '@esbuild/aix-ppc64@0.24.2':
+    resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
 
   '@esbuild/android-arm64@0.21.5':
     resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
@@ -1989,16 +2009,22 @@ packages:
     cpu: [arm64]
     os: [android]
 
+  '@esbuild/android-arm64@0.23.1':
+    resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
   '@esbuild/android-arm64@0.24.0':
     resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [android]
 
-  '@esbuild/android-arm@0.18.20':
-    resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
-    engines: {node: '>=12'}
-    cpu: [arm]
+  '@esbuild/android-arm64@0.24.2':
+    resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
     os: [android]
 
   '@esbuild/android-arm@0.21.5':
@@ -2007,16 +2033,22 @@ packages:
     cpu: [arm]
     os: [android]
 
+  '@esbuild/android-arm@0.23.1':
+    resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
   '@esbuild/android-arm@0.24.0':
     resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==}
     engines: {node: '>=18'}
     cpu: [arm]
     os: [android]
 
-  '@esbuild/android-x64@0.18.20':
-    resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
+  '@esbuild/android-arm@0.24.2':
+    resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
+    engines: {node: '>=18'}
+    cpu: [arm]
     os: [android]
 
   '@esbuild/android-x64@0.21.5':
@@ -2025,17 +2057,23 @@ packages:
     cpu: [x64]
     os: [android]
 
+  '@esbuild/android-x64@0.23.1':
+    resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
   '@esbuild/android-x64@0.24.0':
     resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [android]
 
-  '@esbuild/darwin-arm64@0.18.20':
-    resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [darwin]
+  '@esbuild/android-x64@0.24.2':
+    resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
 
   '@esbuild/darwin-arm64@0.21.5':
     resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
@@ -2043,16 +2081,22 @@ packages:
     cpu: [arm64]
     os: [darwin]
 
+  '@esbuild/darwin-arm64@0.23.1':
+    resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
   '@esbuild/darwin-arm64@0.24.0':
     resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [darwin]
 
-  '@esbuild/darwin-x64@0.18.20':
-    resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
+  '@esbuild/darwin-arm64@0.24.2':
+    resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
     os: [darwin]
 
   '@esbuild/darwin-x64@0.21.5':
@@ -2061,17 +2105,23 @@ packages:
     cpu: [x64]
     os: [darwin]
 
+  '@esbuild/darwin-x64@0.23.1':
+    resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
   '@esbuild/darwin-x64@0.24.0':
     resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [darwin]
 
-  '@esbuild/freebsd-arm64@0.18.20':
-    resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [freebsd]
+  '@esbuild/darwin-x64@0.24.2':
+    resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
 
   '@esbuild/freebsd-arm64@0.21.5':
     resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
@@ -2079,16 +2129,22 @@ packages:
     cpu: [arm64]
     os: [freebsd]
 
+  '@esbuild/freebsd-arm64@0.23.1':
+    resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
   '@esbuild/freebsd-arm64@0.24.0':
     resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [freebsd]
 
-  '@esbuild/freebsd-x64@0.18.20':
-    resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
+  '@esbuild/freebsd-arm64@0.24.2':
+    resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
     os: [freebsd]
 
   '@esbuild/freebsd-x64@0.21.5':
@@ -2097,21 +2153,33 @@ packages:
     cpu: [x64]
     os: [freebsd]
 
+  '@esbuild/freebsd-x64@0.23.1':
+    resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
   '@esbuild/freebsd-x64@0.24.0':
     resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [freebsd]
 
-  '@esbuild/linux-arm64@0.18.20':
-    resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
+  '@esbuild/freebsd-x64@0.24.2':
+    resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.21.5':
+    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [linux]
 
-  '@esbuild/linux-arm64@0.21.5':
-    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
-    engines: {node: '>=12'}
+  '@esbuild/linux-arm64@0.23.1':
+    resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==}
+    engines: {node: '>=18'}
     cpu: [arm64]
     os: [linux]
 
@@ -2121,10 +2189,10 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@esbuild/linux-arm@0.18.20':
-    resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
-    engines: {node: '>=12'}
-    cpu: [arm]
+  '@esbuild/linux-arm64@0.24.2':
+    resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
     os: [linux]
 
   '@esbuild/linux-arm@0.21.5':
@@ -2133,16 +2201,22 @@ packages:
     cpu: [arm]
     os: [linux]
 
+  '@esbuild/linux-arm@0.23.1':
+    resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
   '@esbuild/linux-arm@0.24.0':
     resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==}
     engines: {node: '>=18'}
     cpu: [arm]
     os: [linux]
 
-  '@esbuild/linux-ia32@0.18.20':
-    resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
+  '@esbuild/linux-arm@0.24.2':
+    resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
+    engines: {node: '>=18'}
+    cpu: [arm]
     os: [linux]
 
   '@esbuild/linux-ia32@0.21.5':
@@ -2151,16 +2225,22 @@ packages:
     cpu: [ia32]
     os: [linux]
 
+  '@esbuild/linux-ia32@0.23.1':
+    resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
   '@esbuild/linux-ia32@0.24.0':
     resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==}
     engines: {node: '>=18'}
     cpu: [ia32]
     os: [linux]
 
-  '@esbuild/linux-loong64@0.18.20':
-    resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
-    engines: {node: '>=12'}
-    cpu: [loong64]
+  '@esbuild/linux-ia32@0.24.2':
+    resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
     os: [linux]
 
   '@esbuild/linux-loong64@0.21.5':
@@ -2169,16 +2249,22 @@ packages:
     cpu: [loong64]
     os: [linux]
 
+  '@esbuild/linux-loong64@0.23.1':
+    resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
   '@esbuild/linux-loong64@0.24.0':
     resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==}
     engines: {node: '>=18'}
     cpu: [loong64]
     os: [linux]
 
-  '@esbuild/linux-mips64el@0.18.20':
-    resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
-    engines: {node: '>=12'}
-    cpu: [mips64el]
+  '@esbuild/linux-loong64@0.24.2':
+    resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
     os: [linux]
 
   '@esbuild/linux-mips64el@0.21.5':
@@ -2187,16 +2273,22 @@ packages:
     cpu: [mips64el]
     os: [linux]
 
+  '@esbuild/linux-mips64el@0.23.1':
+    resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
   '@esbuild/linux-mips64el@0.24.0':
     resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==}
     engines: {node: '>=18'}
     cpu: [mips64el]
     os: [linux]
 
-  '@esbuild/linux-ppc64@0.18.20':
-    resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
-    engines: {node: '>=12'}
-    cpu: [ppc64]
+  '@esbuild/linux-mips64el@0.24.2':
+    resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
     os: [linux]
 
   '@esbuild/linux-ppc64@0.21.5':
@@ -2205,16 +2297,22 @@ packages:
     cpu: [ppc64]
     os: [linux]
 
+  '@esbuild/linux-ppc64@0.23.1':
+    resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
   '@esbuild/linux-ppc64@0.24.0':
     resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==}
     engines: {node: '>=18'}
     cpu: [ppc64]
     os: [linux]
 
-  '@esbuild/linux-riscv64@0.18.20':
-    resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
-    engines: {node: '>=12'}
-    cpu: [riscv64]
+  '@esbuild/linux-ppc64@0.24.2':
+    resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
     os: [linux]
 
   '@esbuild/linux-riscv64@0.21.5':
@@ -2223,16 +2321,22 @@ packages:
     cpu: [riscv64]
     os: [linux]
 
+  '@esbuild/linux-riscv64@0.23.1':
+    resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
   '@esbuild/linux-riscv64@0.24.0':
     resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==}
     engines: {node: '>=18'}
     cpu: [riscv64]
     os: [linux]
 
-  '@esbuild/linux-s390x@0.18.20':
-    resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
-    engines: {node: '>=12'}
-    cpu: [s390x]
+  '@esbuild/linux-riscv64@0.24.2':
+    resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
     os: [linux]
 
   '@esbuild/linux-s390x@0.21.5':
@@ -2241,16 +2345,22 @@ packages:
     cpu: [s390x]
     os: [linux]
 
+  '@esbuild/linux-s390x@0.23.1':
+    resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
   '@esbuild/linux-s390x@0.24.0':
     resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==}
     engines: {node: '>=18'}
     cpu: [s390x]
     os: [linux]
 
-  '@esbuild/linux-x64@0.18.20':
-    resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
-    engines: {node: '>=12'}
-    cpu: [x64]
+  '@esbuild/linux-s390x@0.24.2':
+    resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
     os: [linux]
 
   '@esbuild/linux-x64@0.21.5':
@@ -2259,16 +2369,28 @@ packages:
     cpu: [x64]
     os: [linux]
 
+  '@esbuild/linux-x64@0.23.1':
+    resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
   '@esbuild/linux-x64@0.24.0':
     resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [linux]
 
-  '@esbuild/netbsd-x64@0.18.20':
-    resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
-    engines: {node: '>=12'}
+  '@esbuild/linux-x64@0.24.2':
+    resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
+    engines: {node: '>=18'}
     cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-arm64@0.24.2':
+    resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
     os: [netbsd]
 
   '@esbuild/netbsd-x64@0.21.5':
@@ -2277,22 +2399,40 @@ packages:
     cpu: [x64]
     os: [netbsd]
 
+  '@esbuild/netbsd-x64@0.23.1':
+    resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
   '@esbuild/netbsd-x64@0.24.0':
     resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [netbsd]
 
+  '@esbuild/netbsd-x64@0.24.2':
+    resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-arm64@0.23.1':
+    resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
   '@esbuild/openbsd-arm64@0.24.0':
     resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [openbsd]
 
-  '@esbuild/openbsd-x64@0.18.20':
-    resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
+  '@esbuild/openbsd-arm64@0.24.2':
+    resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
     os: [openbsd]
 
   '@esbuild/openbsd-x64@0.21.5':
@@ -2301,17 +2441,23 @@ packages:
     cpu: [x64]
     os: [openbsd]
 
+  '@esbuild/openbsd-x64@0.23.1':
+    resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
   '@esbuild/openbsd-x64@0.24.0':
     resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [openbsd]
 
-  '@esbuild/sunos-x64@0.18.20':
-    resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
-    engines: {node: '>=12'}
+  '@esbuild/openbsd-x64@0.24.2':
+    resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
+    engines: {node: '>=18'}
     cpu: [x64]
-    os: [sunos]
+    os: [openbsd]
 
   '@esbuild/sunos-x64@0.21.5':
     resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
@@ -2319,17 +2465,23 @@ packages:
     cpu: [x64]
     os: [sunos]
 
+  '@esbuild/sunos-x64@0.23.1':
+    resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
   '@esbuild/sunos-x64@0.24.0':
     resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [sunos]
 
-  '@esbuild/win32-arm64@0.18.20':
-    resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [win32]
+  '@esbuild/sunos-x64@0.24.2':
+    resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
 
   '@esbuild/win32-arm64@0.21.5':
     resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
@@ -2337,16 +2489,22 @@ packages:
     cpu: [arm64]
     os: [win32]
 
+  '@esbuild/win32-arm64@0.23.1':
+    resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
   '@esbuild/win32-arm64@0.24.0':
     resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==}
     engines: {node: '>=18'}
     cpu: [arm64]
     os: [win32]
 
-  '@esbuild/win32-ia32@0.18.20':
-    resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
+  '@esbuild/win32-arm64@0.24.2':
+    resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
     os: [win32]
 
   '@esbuild/win32-ia32@0.21.5':
@@ -2355,16 +2513,22 @@ packages:
     cpu: [ia32]
     os: [win32]
 
+  '@esbuild/win32-ia32@0.23.1':
+    resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
   '@esbuild/win32-ia32@0.24.0':
     resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==}
     engines: {node: '>=18'}
     cpu: [ia32]
     os: [win32]
 
-  '@esbuild/win32-x64@0.18.20':
-    resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
+  '@esbuild/win32-ia32@0.24.2':
+    resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
     os: [win32]
 
   '@esbuild/win32-x64@0.21.5':
@@ -2373,12 +2537,24 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@esbuild/win32-x64@0.23.1':
+    resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
   '@esbuild/win32-x64@0.24.0':
     resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==}
     engines: {node: '>=18'}
     cpu: [x64]
     os: [win32]
 
+  '@esbuild/win32-x64@0.24.2':
+    resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
   '@eslint-community/eslint-utils@4.4.0':
     resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2393,43 +2569,39 @@ packages:
     resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
 
-  '@eslint-community/regexpp@4.6.2':
-    resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==}
-    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
-
   '@eslint/compat@1.1.1':
     resolution: {integrity: sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/config-array@0.18.0':
-    resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==}
+  '@eslint/config-array@0.19.1':
+    resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/core@0.7.0':
-    resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==}
+  '@eslint/core@0.10.0':
+    resolution: {integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/eslintrc@3.1.0':
-    resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==}
+  '@eslint/eslintrc@3.2.0':
+    resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/js@9.14.0':
-    resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==}
+  '@eslint/js@9.18.0':
+    resolution: {integrity: sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/object-schema@2.1.4':
-    resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==}
+  '@eslint/object-schema@2.1.5':
+    resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/plugin-kit@0.2.0':
-    resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==}
+  '@eslint/plugin-kit@0.2.5':
+    resolution: {integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@fastify/accept-negotiator@2.0.0':
     resolution: {integrity: sha512-/Sce/kBzuTxIq5tJh85nVNOq9wKD8s+viIgX0fFMDBdw95gnpf53qmF1oBgJym3cPFliWUuSloVg/1w/rH0FcQ==}
 
-  '@fastify/accepts@5.0.1':
-    resolution: {integrity: sha512-8ji2MGTbceSnAXKYx/U9iWt6Fmf0zJovh0meO5rpwYS/vy0Z3QIR2J/hKmbcTpYfMu5NUliNpsAtMavmzBQhmA==}
+  '@fastify/accepts@5.0.2':
+    resolution: {integrity: sha512-pX0OrioMz3C2cuYFOGRCNMdP3sR6daTFjeSNFrWlZVutawpPIGI5opK5h4Qa6x6C9oavdDkAjA16orneE2jAFQ==}
 
   '@fastify/ajv-compiler@4.0.0':
     resolution: {integrity: sha512-dt0jyLAlay14LpIn4Fg1SY7V5NJ9KH0YFDpYVQY5cgIVBvdI8908AMx5zQ0bBYPGT6Wh+bM3f2caMmOXLP3QsQ==}
@@ -2441,11 +2613,11 @@ packages:
   '@fastify/busboy@3.0.0':
     resolution: {integrity: sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==}
 
-  '@fastify/cookie@11.0.1':
-    resolution: {integrity: sha512-n1Ooz4bgQ5LcOlJQboWPfsMNxIrGV0SgU85UkctdpTlCQE0mtA3rlspOPUdqk9ubiiZn053ucnia4DjTquI4/g==}
+  '@fastify/cookie@11.0.2':
+    resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
 
-  '@fastify/cors@10.0.1':
-    resolution: {integrity: sha512-O8JIf6448uQbOgzSkCqhClw6gFTAqrdfeA6R3fc/3gwTJGUp7gl8/3tbNB+6INuu4RmgVOq99BmvdGbtu5pgOA==}
+  '@fastify/cors@10.0.2':
+    resolution: {integrity: sha512-DGdxOG36sS/tZv1NFiCJGi7wGuXOSPL2CmNX5PbOVKx0C6LuIALRMrqLByHTCcX1Rbl8NJ9IWlJex32bzydvlw==}
 
   '@fastify/deepmerge@2.0.0':
     resolution: {integrity: sha512-fsaybTGDyQ5KpPsplQqb9yKdCf2x/pbNpMNk8Tvp3rRz7lVcupKysH4b2ELMN2P4Hak1+UqTYdTj/u4FNV2p0g==}
@@ -2453,32 +2625,38 @@ packages:
   '@fastify/error@4.0.0':
     resolution: {integrity: sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==}
 
-  '@fastify/express@4.0.1':
-    resolution: {integrity: sha512-mEQ6pawaENeZ3swqVtkxdLi8NQC5eKBkclE+7ma1qQMuB+yI6WxDyEp55pdbqPIqBQTN/cGgHv84qxVS7NKC2Q==}
+  '@fastify/express@4.0.2':
+    resolution: {integrity: sha512-lzu9MLdjlsK4Q2RiqEAwTgwQPrWQVP0kmbgAi/w9rIUqtnacjKvj3EHVTR6PIvXDs6Ut1jnTHiGbuNxHTsZwHQ==}
 
   '@fastify/fast-json-stringify-compiler@5.0.0':
     resolution: {integrity: sha512-tywfuZfXsyxLC5kEqrMubbFa9vpAxNtuPE7j9w5si1r+6p5b981pDfZ5Y8HBqmjDQl+PABT7cV5jZgXI2j+I5g==}
 
+  '@fastify/forwarded@3.0.0':
+    resolution: {integrity: sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==}
+
   '@fastify/http-proxy@10.0.1':
     resolution: {integrity: sha512-wCMwI9RXK5ISe9G1FGPDCCD2KlSAuLtDDU8XBEfiBxYV0nt+aYm4vPhU/0+IhUM6t+r7UWiV+9OYaJxcTem9+g==}
 
   '@fastify/merge-json-schemas@0.1.1':
     resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
 
-  '@fastify/multipart@9.0.1':
-    resolution: {integrity: sha512-vt2gOCw/O4EwpN4KlLVJxth4iQlDf7T5ggw2Db2C+UbO2WJBG7y0jEBvu/HT6JIW/lBYaqrrUy9MmTpCKgXEpw==}
+  '@fastify/multipart@9.0.2':
+    resolution: {integrity: sha512-UOB5zl62YYTrM99o06Hk8gFVpHBhPwuP7IXeihRjl4nK835XQG3XwN9a+Dd2b4aFYapsQtt1h+QqTCY3ZPhEww==}
+
+  '@fastify/proxy-addr@5.0.0':
+    resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==}
 
   '@fastify/reply-from@11.0.0':
     resolution: {integrity: sha512-dv3o8hyy4sxhg1RN9l6ueM+PMMaIPKLjtL2T99H5M7h1Xt8d1RX3r+xC+sL5AqJqLvReX4N+7mTq9QDeB8i6Lg==}
 
-  '@fastify/send@3.1.1':
-    resolution: {integrity: sha512-LdiV2mle/2tH8vh6GwGl0ubfUAgvY+9yF9oGI1iiwVyNUVOQamvw5n+OFu6iCNNoyuCY80FFURBn4TZCbTe8LA==}
+  '@fastify/send@3.3.1':
+    resolution: {integrity: sha512-6pofeVwaHN+E/MAofCwDqkWUliE3i++jlD0VH/LOfU8TJlCkMUSgKvA9bawDdVXxjve7XrdYMyDmkiYaoGWEtA==}
 
-  '@fastify/static@8.0.2':
-    resolution: {integrity: sha512-xJ+XaZVl4Y+lKztx8jGi+BE73aByhOmjMgaTx98E4XtVZxUpiaYQIMBlwACsJz+xohm0kvzV34BZoiZ+bsJtBQ==}
+  '@fastify/static@8.0.4':
+    resolution: {integrity: sha512-JdJIlXDYXZxbTFQazWOEfHxyD5uRXqRsLnp4rV9MwJnxadA0rrWBI8ZelPF2TPk/xDi5wunY/6ZmfwHXld13bA==}
 
-  '@fastify/view@10.0.1':
-    resolution: {integrity: sha512-rXtBN0oVDmoRZAS7lelrCIahf+qFtlMOOas8VPdA7JvrJ9ChcF7e36pIUPU0Vbs3KmHxESUb7XatavUZEe/k5Q==}
+  '@fastify/view@10.0.2':
+    resolution: {integrity: sha512-tGjXFyDUMj5a+E8BBrQ2wpsVnpOfMq3cqy4WD8pnjWPE/HGNItBASUPoPUcX/QjPhxfuZZTYv2XdCmKXdcMZPw==}
 
   '@github/webauthn-json@2.1.1':
     resolution: {integrity: sha512-XrftRn4z75SnaJOmZQbt7Mk+IIjqVHw+glDGOxuHwXkZBZh/MBoRS7MHjSZMDaLhT4RjN2VqiEU7EOYleuJWSQ==}
@@ -2739,19 +2917,15 @@ packages:
     resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
-  '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0':
-    resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==}
+  '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2':
+    resolution: {integrity: sha512-feQ+ntr+8hbVudnsTUapiMN9q8T90XA1d5jn9QzY09sNoj4iD9wi0PY1vsBFTda4ZjEaxRK9S81oarR2nj7TFQ==}
     peerDependencies:
       typescript: '>= 4.3.x'
-      vite: ^3.0.0 || ^4.0.0 || ^5.0.0
+      vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
     peerDependenciesMeta:
       typescript:
         optional: true
 
-  '@jridgewell/gen-mapping@0.3.2':
-    resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
-    engines: {node: '>=6.0.0'}
-
   '@jridgewell/gen-mapping@0.3.5':
     resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
     engines: {node: '>=6.0.0'}
@@ -2760,10 +2934,6 @@ packages:
     resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
     engines: {node: '>=6.0.0'}
 
-  '@jridgewell/set-array@1.1.2':
-    resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
-    engines: {node: '>=6.0.0'}
-
   '@jridgewell/set-array@1.2.1':
     resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
     engines: {node: '>=6.0.0'}
@@ -2771,18 +2941,12 @@ packages:
   '@jridgewell/source-map@0.3.6':
     resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
 
-  '@jridgewell/sourcemap-codec@1.4.14':
-    resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
-
   '@jridgewell/sourcemap-codec@1.4.15':
     resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
 
   '@jridgewell/sourcemap-codec@1.5.0':
     resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
 
-  '@jridgewell/trace-mapping@0.3.18':
-    resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
-
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
@@ -2819,37 +2983,38 @@ packages:
       '@types/react': '>=16'
       react: '>=16'
 
-  '@microsoft/api-extractor-model@7.29.8':
-    resolution: {integrity: sha512-t3Z/xcO6TRbMcnKGVMs4uMzv/gd5j0NhMiJIGjD4cJMeFJ1Hf8wnLSx37vxlRlL0GWlGJhnFgxvnaL6JlS+73g==}
+  '@microsoft/api-extractor-model@7.30.2':
+    resolution: {integrity: sha512-3/t2F+WhkJgBzSNwlkTIL0tBgUoBqDqL66pT+nh2mPbM0NIDGVGtpqbGWPgHIzn/mn7kGS/Ep8D8po58e8UUIw==}
 
-  '@microsoft/api-extractor@7.47.11':
-    resolution: {integrity: sha512-lrudfbPub5wzBhymfFtgZKuBvXxoSIAdrvS2UbHjoMT2TjIEddq6Z13pcve7A03BAouw0x8sW8G4txdgfiSwpQ==}
+  '@microsoft/api-extractor@7.49.1':
+    resolution: {integrity: sha512-jRTR/XbQF2kb+dYn8hfYSicOGA99+Fo00GrsdMwdfE3eIgLtKdH6Qa2M3wZV9S2XmbgCaGX1OdPtYctbfu5jQg==}
     hasBin: true
 
-  '@microsoft/tsdoc-config@0.17.0':
-    resolution: {integrity: sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==}
+  '@microsoft/tsdoc-config@0.17.1':
+    resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==}
 
-  '@microsoft/tsdoc@0.15.0':
-    resolution: {integrity: sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==}
+  '@microsoft/tsdoc@0.15.1':
+    resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
 
   '@misskey-dev/browser-image-resizer@2024.1.0':
     resolution: {integrity: sha512-4EnO0zLW5NDtng3Gaz5MuT761uiuoOuplwX18wBqgj8w56LTU5BjLn/vbHwDIIe0j2gwqDYhMb7bDjmr1/Fomg==}
 
-  '@misskey-dev/eslint-plugin@2.0.3':
-    resolution: {integrity: sha512-Gd7chezh53Gq4BZ+Tw/uRi4t5DocXR+vTFuTSRpwrZjbyTk6tWMHLV0EtaGY/6GT+Q6WH3UMJYExsFyHFK+JPw==}
+  '@misskey-dev/eslint-plugin@2.1.0':
+    resolution: {integrity: sha512-f++Vv1r3BQyGqEE0SB5algUZwRoTMZIYfVtpcuQ2fLuYUm0cQ5BBTs/gwAHPajVB2YD8F33gzPIReTKtuJyCwQ==}
     peerDependencies:
       '@eslint/compat': '>= 1'
-      '@typescript-eslint/eslint-plugin': '>= 7'
-      '@typescript-eslint/parser': '>= 7'
-      eslint: '>= 8'
+      '@stylistic/eslint-plugin': '>= 2'
+      '@typescript-eslint/eslint-plugin': '>= 8'
+      '@typescript-eslint/parser': '>= 8'
+      eslint: '>= 9'
       eslint-plugin-import: '>= 2'
       globals: '>= 15'
 
   '@misskey-dev/sharp-read-bmp@1.2.0':
     resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==}
 
-  '@misskey-dev/summaly@5.1.0':
-    resolution: {integrity: sha512-WAUrgX3/z4h4aI8Y/WVwmJcJ6Fa1Zf2LJCSS651t9MHoWVGABLsQ2KCXRGmlpk4i+cMDNIwweObUroosE7j8rg==}
+  '@misskey-dev/summaly@5.2.0':
+    resolution: {integrity: sha512-Djg6emAOdcY81a8TMTFi/qgIdTO96Z2qZfhHE+k+YRd8z9V/5MxauJr+U9z4kUVjSJJ1yzdOWqi26OsmVUaXZA==}
 
   '@mole-inc/bin-wrapper@8.0.1':
     resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==}
@@ -2885,70 +3050,76 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@mswjs/interceptors@0.36.10':
-    resolution: {integrity: sha512-GXrJgakgJW3DWKueebkvtYgGKkxA7s0u5B0P5syJM5rvQUnrpLPigvci8Hukl7yEM+sU06l+er2Fgvx/gmiRgg==}
+  '@mswjs/interceptors@0.37.5':
+    resolution: {integrity: sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==}
     engines: {node: '>=18'}
 
-  '@napi-rs/canvas-android-arm64@0.1.56':
-    resolution: {integrity: sha512-xBGqW2RZMAupkzar9t3gpbok9r524f3Wlk4PG2qnQdxbsiEND06OB8VxVtTcql6R02uJpXJGnyIhN02Te+GMVQ==}
+  '@napi-rs/canvas-android-arm64@0.1.65':
+    resolution: {integrity: sha512-ZYwqFYEKcT5Zr8lbiaJNJj/poLaeK2TncolY914r+gD2TJNeP7ZqvE7A2SX/1C9MB4E3DQEwm3YhL3WEf0x3MQ==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [android]
 
-  '@napi-rs/canvas-darwin-arm64@0.1.56':
-    resolution: {integrity: sha512-Pvuz6Ib9YZTB5MlGL9WSu9a2asUC0DZ1zBHozDiBXr/6Zurs9l/ZH5NxFYTM829BpkdkO8kuI8b8Rz7ek30zzQ==}
+  '@napi-rs/canvas-darwin-arm64@0.1.65':
+    resolution: {integrity: sha512-Pg1pfiJEyDIsX+V0QaJPRWvXbw5zmWAk3bivFCvt/5pwZb37/sT6E/RqPHT9NnqpDyKW6SriwY9ypjljysUA1Q==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [darwin]
 
-  '@napi-rs/canvas-darwin-x64@0.1.56':
-    resolution: {integrity: sha512-O393jWt7G6rg0X1ralbsbBeskSG0iwlkD7mEHhMLJxqRqe+eQn0/xnwhs9l6dUNFC+5dM8LOvfFca4o9Vs2Vww==}
+  '@napi-rs/canvas-darwin-x64@0.1.65':
+    resolution: {integrity: sha512-3Tr+/HjdJN7Z/VKIcsxV2DvDIibZCExgfYTgljCkUSFuoI7iNkOE6Dc1Q6j212EB9PeO8KmfrViBqHYT6IwWkA==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [darwin]
 
-  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.56':
-    resolution: {integrity: sha512-30NFb5lrF3YEwAO5XuATxpWDSXaBAgaFVswPJ+hYcAUyE3IkPPIFRY4ijQEh4frcSBvrzFGGYdNSoC18oLLWaQ==}
+  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.65':
+    resolution: {integrity: sha512-3KP+dYObH7CVkZMZWwk1WX9jRjL+EKdQtD43H8MOI+illf+dwqLlecdQ4d9bQRIxELKJ8dyPWY4fOp/Ngufrdg==}
     engines: {node: '>= 10'}
     cpu: [arm]
     os: [linux]
 
-  '@napi-rs/canvas-linux-arm64-gnu@0.1.56':
-    resolution: {integrity: sha512-ODbWH9TLvba+39UxFwPn2Hm1ImALmWOZ0pEv5do/pz0439326Oz49hlfGot4KmkSBeKK81knWxRj9EXMSPwXPg==}
+  '@napi-rs/canvas-linux-arm64-gnu@0.1.65':
+    resolution: {integrity: sha512-Ka3StKz7Dq7kjTF3nNJCq43UN/VlANS7qGE3dWkn1d+tQNsCRy/wRmyt1TUFzIjRqcTFMQNRbgYq84+53UBA0A==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [linux]
 
-  '@napi-rs/canvas-linux-arm64-musl@0.1.56':
-    resolution: {integrity: sha512-zqE4nz8CWiJJ0q5By7q9CDPicNkc0oyErgavK3ZV279zJL7Aapd3cIqayT6ynECArg7GgBl2WYSvr5AaRFmYgg==}
+  '@napi-rs/canvas-linux-arm64-musl@0.1.65':
+    resolution: {integrity: sha512-O4xMASm2JrmqYoiDyxVWi+z5C14H+oVEag2rZ5iIA67dhWqYZB+iO7wCFpBYRj31JPBR29FOsu6X9zL+DwBFdw==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [linux]
 
-  '@napi-rs/canvas-linux-x64-gnu@0.1.56':
-    resolution: {integrity: sha512-JTnGAtJBQMhfSpN8/rbMnf5oxuO/juUNa0n4LA0LlW0JS9UBpmsS2BwFNCakFqOeAPaqIM6sFFsK3M4hve+Esw==}
+  '@napi-rs/canvas-linux-riscv64-gnu@0.1.65':
+    resolution: {integrity: sha512-dblWDaA59ZU8bPbkfM+riSke7sFbNZ70LEevUdI5rgiFEUzYUQlU34gSBzemTACj5rCWt1BYeu0GfkLSjNMBSw==}
+    engines: {node: '>= 10'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@napi-rs/canvas-linux-x64-gnu@0.1.65':
+    resolution: {integrity: sha512-wsp+atutw13OJXGU3DDkdngtBDoEg01IuK5xMe0L6VFPV8maGkh17CXze078OD5QJOc6kFyw3DDscMLOPF8+oA==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [linux]
 
-  '@napi-rs/canvas-linux-x64-musl@0.1.56':
-    resolution: {integrity: sha512-mpws7DhVDIj8ZKa/qcnUVLAm0fxD9RK5ojfNNSI9TOzn2E0f+GUXx8sGsCxDpMVMtN+mtyrMwRqH3F3rTUMWXw==}
+  '@napi-rs/canvas-linux-x64-musl@0.1.65':
+    resolution: {integrity: sha512-odX+nN+IozWzhdj31INcHz3Iy9+EckNw+VqsZcaUxZOTu7/3FmktRNI6aC1qe5minZNv1m05YOS1FVf7fvmjlA==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [linux]
 
-  '@napi-rs/canvas-win32-x64-msvc@0.1.56':
-    resolution: {integrity: sha512-VKAAkgXF+lbFvRFawPOtkfV/P7ogAgWTu5FMCIiBn0Gc3vnkKFG2cLo/IHIJ7FuriToKEidkJGT88iAh7W7GDA==}
+  '@napi-rs/canvas-win32-x64-msvc@0.1.65':
+    resolution: {integrity: sha512-RZQX3luWnlNWgdMnLMQ1hyfQraeAn9lnxWWVCHuUM4tAWEV8UDdeb7cMwmJW7eyt8kAosmjeHt3cylQMHOxGFg==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [win32]
 
-  '@napi-rs/canvas@0.1.56':
-    resolution: {integrity: sha512-SujSchzG6lLc/wT+Mwxam/w30Kk2sFTiU6bLFcidecKSmlhenAhGMQhZh2iGFfKoh2+8iit0jrt99n6TqReICQ==}
+  '@napi-rs/canvas@0.1.65':
+    resolution: {integrity: sha512-YcFhXQcp+b2d38zFOJNbpyPHnIL7KAEkhJQ+UeeKI5IpE9B8Cpf/M6RiHPQXSsSqnYbrfFylnW49dyh2oeSblQ==}
     engines: {node: '>= 10'}
 
-  '@nestjs/common@10.4.7':
-    resolution: {integrity: sha512-gIOpjD3Mx8gfYGxYm/RHPcJzqdknNNFCyY+AxzBT3gc5Xvvik1Dn5OxaMGw5EbVfhZgJKVP0n83giUOAlZQe7w==}
+  '@nestjs/common@11.0.1':
+    resolution: {integrity: sha512-GbPKvDz/sYjhR86ebZu+0F5jAvYDZDXY5WcHek9gkEkMI9eIo3xeH/F/tNIPhdyeWp3eG+kNWqR9VPtC18G+TA==}
     peerDependencies:
       class-transformer: '*'
       class-validator: '*'
@@ -2960,13 +3131,14 @@ packages:
       class-validator:
         optional: true
 
-  '@nestjs/core@10.4.7':
-    resolution: {integrity: sha512-AIpQzW/vGGqSLkKvll1R7uaSNv99AxZI2EFyVJPNGDgFsfXaohfV1Ukl6f+s75Km+6Fj/7aNl80EqzNWQCS8Ig==}
+  '@nestjs/core@11.0.1':
+    resolution: {integrity: sha512-Yn7X2aInjmX7yxpH8TjJmgC0JPvs+tcreETkquSRmKbuK5J28dZDi8loiaw3eRTLLvzzUovv5mlqFxmVhDESOw==}
+    engines: {node: '>= 20'}
     peerDependencies:
-      '@nestjs/common': ^10.0.0
-      '@nestjs/microservices': ^10.0.0
-      '@nestjs/platform-express': ^10.0.0
-      '@nestjs/websockets': ^10.0.0
+      '@nestjs/common': ^11.0.0
+      '@nestjs/microservices': ^11.0.0
+      '@nestjs/platform-express': ^11.0.0
+      '@nestjs/websockets': ^11.0.0
       reflect-metadata: ^0.1.12 || ^0.2.0
       rxjs: ^7.1.0
     peerDependenciesMeta:
@@ -2983,21 +3155,21 @@ packages:
       '@nestjs/common': ^10.0.0
       '@nestjs/core': ^10.0.0
 
-  '@nestjs/testing@10.4.7':
-    resolution: {integrity: sha512-aS3sQ0v4g8cyHDzW3xJv1+8MiFAkxUNXmnau588IFFI/nBIo/kevLNHNPr85keYekkJ/lwNDW72h8UGg8BYd9w==}
+  '@nestjs/testing@11.0.1':
+    resolution: {integrity: sha512-s1hQAC46W/qp2JJqCeqhG/uT8IBtvgIoAHklUPl7sTYSJCs745q6X2+rrOPtAewVW8CuYNhQS7/0g94QR1IHXw==}
     peerDependencies:
-      '@nestjs/common': ^10.0.0
-      '@nestjs/core': ^10.0.0
-      '@nestjs/microservices': ^10.0.0
-      '@nestjs/platform-express': ^10.0.0
+      '@nestjs/common': ^11.0.0
+      '@nestjs/core': ^11.0.0
+      '@nestjs/microservices': ^11.0.0
+      '@nestjs/platform-express': ^11.0.0
     peerDependenciesMeta:
       '@nestjs/microservices':
         optional: true
       '@nestjs/platform-express':
         optional: true
 
-  '@noble/hashes@1.5.0':
-    resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
+  '@noble/hashes@1.6.1':
+    resolution: {integrity: sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==}
     engines: {node: ^14.21.3 || >=16}
 
   '@nodelib/fs.scandir@2.1.5':
@@ -3020,9 +3192,9 @@ packages:
     resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 
-  '@nuxtjs/opencollective@0.3.2':
-    resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==}
-    engines: {node: '>=8.0.0', npm: '>=5.0.0'}
+  '@nuxt/opencollective@0.4.1':
+    resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==}
+    engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'}
     hasBin: true
 
   '@one-ini/wasm@0.1.1':
@@ -3037,204 +3209,188 @@ packages:
   '@open-draft/until@2.1.0':
     resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
 
-  '@opentelemetry/api-logs@0.52.1':
-    resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==}
-    engines: {node: '>=14'}
-
   '@opentelemetry/api-logs@0.53.0':
     resolution: {integrity: sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==}
     engines: {node: '>=14'}
 
-  '@opentelemetry/api-logs@0.54.2':
-    resolution: {integrity: sha512-4MTVwwmLgUh5QrJnZpYo6YRO5IBLAggf2h8gWDblwRagDStY13aEvt7gGk3jewrMaPlHiF83fENhIx0HO97/cQ==}
+  '@opentelemetry/api-logs@0.56.0':
+    resolution: {integrity: sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g==}
     engines: {node: '>=14'}
 
   '@opentelemetry/api@1.9.0':
     resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
     engines: {node: '>=8.0.0'}
 
-  '@opentelemetry/context-async-hooks@1.25.1':
-    resolution: {integrity: sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==}
-    engines: {node: '>=14'}
-    peerDependencies:
-      '@opentelemetry/api': '>=1.0.0 <1.10.0'
-
-  '@opentelemetry/core@1.25.1':
-    resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==}
+  '@opentelemetry/context-async-hooks@1.30.1':
+    resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': '>=1.0.0 <1.10.0'
 
-  '@opentelemetry/core@1.26.0':
-    resolution: {integrity: sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==}
+  '@opentelemetry/core@1.29.0':
+    resolution: {integrity: sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': '>=1.0.0 <1.10.0'
 
-  '@opentelemetry/core@1.27.0':
-    resolution: {integrity: sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==}
+  '@opentelemetry/core@1.30.1':
+    resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': '>=1.0.0 <1.10.0'
 
-  '@opentelemetry/instrumentation-amqplib@0.43.0':
-    resolution: {integrity: sha512-ALjfQC+0dnIEcvNYsbZl/VLh7D2P1HhFF4vicRKHhHFIUV3Shpg4kXgiek5PLhmeKSIPiUB25IYH5RIneclL4A==}
+  '@opentelemetry/instrumentation-amqplib@0.45.0':
+    resolution: {integrity: sha512-SlKLsOS65NGMIBG1Lh/hLrMDU9WzTUF25apnV6ZmWZB1bBmUwan7qrwwrTu1cL5LzJWCXOdZPuTaxP7pC9qxnQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-connect@0.40.0':
-    resolution: {integrity: sha512-3aR/3YBQ160siitwwRLjwqrv2KBT16897+bo6yz8wIfel6nWOxTZBJudcbsK3p42pTC7qrbotJ9t/1wRLpv79Q==}
+  '@opentelemetry/instrumentation-connect@0.42.0':
+    resolution: {integrity: sha512-bOoYHBmbnq/jFaLHmXJ55VQ6jrH5fHDMAPjFM0d3JvR0dvIqW7anEoNC33QqYGFYUfVJ50S0d/eoyF61ALqQuA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-dataloader@0.12.0':
-    resolution: {integrity: sha512-pnPxatoFE0OXIZDQhL2okF//dmbiWFzcSc8pUg9TqofCLYZySSxDCgQc69CJBo5JnI3Gz1KP+mOjS4WAeRIH4g==}
+  '@opentelemetry/instrumentation-dataloader@0.15.0':
+    resolution: {integrity: sha512-5fP35A2jUPk4SerVcduEkpbRAIoqa2PaP5rWumn01T1uSbavXNccAr3Xvx1N6xFtZxXpLJq4FYqGFnMgDWgVng==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-express@0.44.0':
-    resolution: {integrity: sha512-GWgibp6Q0wxyFaaU8ERIgMMYgzcHmGrw3ILUtGchLtLncHNOKk0SNoWGqiylXWWT4HTn5XdV8MGawUgpZh80cA==}
+  '@opentelemetry/instrumentation-express@0.46.0':
+    resolution: {integrity: sha512-BCEClDj/HPq/1xYRAlOr6z+OUnbp2eFp18DSrgyQz4IT9pkdYk8eWHnMi9oZSqlC6J5mQzkFmaW5RrKb1GLQhg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-fastify@0.41.0':
-    resolution: {integrity: sha512-pNRjFvf0mvqfJueaeL/qEkuGJwgtE5pgjIHGYwjc2rMViNCrtY9/Sf+Nu8ww6dDd/Oyk2fwZZP7i0XZfCnETrA==}
+  '@opentelemetry/instrumentation-fastify@0.43.0':
+    resolution: {integrity: sha512-Lmdsg7tYiV+K3/NKVAQfnnLNGmakUOFdB0PhoTh2aXuSyCmyNnnDvhn2MsArAPTZ68wnD5Llh5HtmiuTkf+DyQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-fs@0.16.0':
-    resolution: {integrity: sha512-hMDRUxV38ln1R3lNz6osj3YjlO32ykbHqVrzG7gEhGXFQfu7LJUx8t9tEwE4r2h3CD4D0Rw4YGDU4yF4mP3ilg==}
+  '@opentelemetry/instrumentation-fs@0.18.0':
+    resolution: {integrity: sha512-kC40y6CEMONm8/MWwoF5GHWIC7gOdF+g3sgsjfwJaUkgD6bdWV+FgG0XApqSbTQndICKzw3RonVk8i7s6mHqhA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-generic-pool@0.39.0':
-    resolution: {integrity: sha512-y4v8Y+tSfRB3NNBvHjbjrn7rX/7sdARG7FuK6zR8PGb28CTa0kHpEGCJqvL9L8xkTNvTXo+lM36ajFGUaK1aNw==}
+  '@opentelemetry/instrumentation-generic-pool@0.42.0':
+    resolution: {integrity: sha512-J4QxqiQ1imtB9ogzsOnHra0g3dmmLAx4JCeoK3o0rFes1OirljNHnO8Hsj4s1jAir8WmWvnEEQO1y8yk6j2tog==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-graphql@0.44.0':
-    resolution: {integrity: sha512-FYXTe3Bv96aNpYktqm86BFUTpjglKD0kWI5T5bxYkLUPEPvFn38vWGMJTGrDMVou/i55E4jlWvcm6hFIqLsMbg==}
+  '@opentelemetry/instrumentation-graphql@0.46.0':
+    resolution: {integrity: sha512-tplk0YWINSECcK89PGM7IVtOYenXyoOuhOQlN0X0YrcDUfMS4tZMKkVc0vyhNWYYrexnUHwNry2YNBNugSpjlQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-hapi@0.41.0':
-    resolution: {integrity: sha512-jKDrxPNXDByPlYcMdZjNPYCvw0SQJjN+B1A+QH+sx+sAHsKSAf9hwFiJSrI6C4XdOls43V/f/fkp9ITkHhKFbQ==}
+  '@opentelemetry/instrumentation-hapi@0.44.0':
+    resolution: {integrity: sha512-4HdNIMNXWK1O6nsaQOrACo83QWEVoyNODTdVDbUqtqXiv2peDfD0RAPhSQlSGWLPw3S4d9UoOmrV7s2HYj6T2A==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-http@0.53.0':
-    resolution: {integrity: sha512-H74ErMeDuZfj7KgYCTOFGWF5W9AfaPnqLQQxeFq85+D29wwV2yqHbz2IKLYpkOh7EI6QwDEl7rZCIxjJLyc/CQ==}
+  '@opentelemetry/instrumentation-http@0.56.0':
+    resolution: {integrity: sha512-/bWHBUAq8VoATnH9iLk5w8CE9+gj+RgYSUphe7hry472n6fYl7+4PvuScoQMdmSUTprKq/gyr2kOWL6zrC7FkQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-ioredis@0.43.0':
-    resolution: {integrity: sha512-i3Dke/LdhZbiUAEImmRG3i7Dimm/BD7t8pDDzwepSvIQ6s2X6FPia7561gw+64w+nx0+G9X14D7rEfaMEmmjig==}
+  '@opentelemetry/instrumentation-ioredis@0.46.0':
+    resolution: {integrity: sha512-sOdsq8oGi29V58p1AkefHvuB3l2ymP1IbxRIX3y4lZesQWKL8fLhBmy8xYjINSQ5gHzWul2yoz7pe7boxhZcqQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-kafkajs@0.4.0':
-    resolution: {integrity: sha512-I9VwDG314g7SDL4t8kD/7+1ytaDBRbZQjhVaQaVIDR8K+mlsoBhLsWH79yHxhHQKvwCSZwqXF+TiTOhoQVUt7A==}
+  '@opentelemetry/instrumentation-kafkajs@0.6.0':
+    resolution: {integrity: sha512-MGQrzqEUAl0tacKJUFpuNHJesyTi51oUzSVizn7FdvJplkRIdS11FukyZBZJEscofSEdk7Ycmg+kNMLi5QHUFg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-knex@0.41.0':
-    resolution: {integrity: sha512-OhI1SlLv5qnsnm2dOVrian/x3431P75GngSpnR7c4fcVFv7prXGYu29Z6ILRWJf/NJt6fkbySmwdfUUnFnHCTg==}
+  '@opentelemetry/instrumentation-knex@0.43.0':
+    resolution: {integrity: sha512-mOp0TRQNFFSBj5am0WF67fRO7UZMUmsF3/7HSDja9g3H4pnj+4YNvWWyZn4+q0rGrPtywminAXe0rxtgaGYIqg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-koa@0.43.0':
-    resolution: {integrity: sha512-lDAhSnmoTIN6ELKmLJBplXzT/Jqs5jGZehuG22EdSMaTwgjMpxMDI1YtlKEhiWPWkrz5LUsd0aOO0ZRc9vn3AQ==}
+  '@opentelemetry/instrumentation-koa@0.46.0':
+    resolution: {integrity: sha512-RcWXMQdJQANnPUaXbHY5G0Fg6gmleZ/ZtZeSsekWPaZmQq12FGk0L1UwodIgs31OlYfviAZ4yTeytoSUkgo5vQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-lru-memoizer@0.40.0':
-    resolution: {integrity: sha512-21xRwZsEdMPnROu/QsaOIODmzw59IYpGFmuC4aFWvMj6stA8+Ei1tX67nkarJttlNjoM94um0N4X26AD7ff54A==}
+  '@opentelemetry/instrumentation-lru-memoizer@0.43.0':
+    resolution: {integrity: sha512-fZc+1eJUV+tFxaB3zkbupiA8SL3vhDUq89HbDNg1asweYrEb9OlHIB+Ot14ZiHUc1qCmmWmZHbPTwa56mVVwzg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-mongodb@0.48.0':
-    resolution: {integrity: sha512-9YWvaGvrrcrydMsYGLu0w+RgmosLMKe3kv/UNlsPy8RLnCkN2z+bhhbjjjuxtUmvEuKZMCoXFluABVuBr1yhjw==}
+  '@opentelemetry/instrumentation-mongodb@0.50.0':
+    resolution: {integrity: sha512-DtwJMjYFXFT5auAvv8aGrBj1h3ciA/dXQom11rxL7B1+Oy3FopSpanvwYxJ+z0qmBrQ1/iMuWELitYqU4LnlkQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-mongoose@0.42.0':
-    resolution: {integrity: sha512-AnWv+RaR86uG3qNEMwt3plKX1ueRM7AspfszJYVkvkehiicC3bHQA6vWdb6Zvy5HAE14RyFbu9+2hUUjR2NSyg==}
+  '@opentelemetry/instrumentation-mongoose@0.45.0':
+    resolution: {integrity: sha512-zHgNh+A01C5baI2mb5dAGyMC7DWmUpOfwpV8axtC0Hd5Uzqv+oqKgKbVDIVhOaDkPxjgVJwYF9YQZl2pw2qxIA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-mysql2@0.41.0':
-    resolution: {integrity: sha512-REQB0x+IzVTpoNgVmy5b+UnH1/mDByrneimP6sbDHkp1j8QOl1HyWOrBH/6YWR0nrbU3l825Em5PlybjT3232g==}
+  '@opentelemetry/instrumentation-mysql2@0.44.0':
+    resolution: {integrity: sha512-e9QY4AGsjGFwmfHd6kBa4yPaQZjAq2FuxMb0BbKlXCAjG+jwqw+sr9xWdJGR60jMsTq52hx3mAlE3dUJ9BipxQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-mysql@0.41.0':
-    resolution: {integrity: sha512-jnvrV6BsQWyHS2qb2fkfbfSb1R/lmYwqEZITwufuRl37apTopswu9izc0b1CYRp/34tUG/4k/V39PND6eyiNvw==}
+  '@opentelemetry/instrumentation-mysql@0.44.0':
+    resolution: {integrity: sha512-al7jbXvT/uT1KV8gdNDzaWd5/WXf+mrjrsF0/NtbnqLa0UUFGgQnoK3cyborgny7I+KxWhL8h7YPTf6Zq4nKsg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-nestjs-core@0.40.0':
-    resolution: {integrity: sha512-WF1hCUed07vKmf5BzEkL0wSPinqJgH7kGzOjjMAiTGacofNXjb/y4KQ8loj2sNsh5C/NN7s1zxQuCgbWbVTGKg==}
+  '@opentelemetry/instrumentation-nestjs-core@0.43.0':
+    resolution: {integrity: sha512-NEo4RU7HTjiaXk3curqXUvCb9alRiFWxQY//+hvDXwWLlADX2vB6QEmVCeEZrKO+6I/tBrI4vNdAnbCY9ldZVg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-pg@0.44.0':
-    resolution: {integrity: sha512-oTWVyzKqXud1BYEGX1loo2o4k4vaU1elr3vPO8NZolrBtFvQ34nx4HgUaexUDuEog00qQt+MLR5gws/p+JXMLQ==}
+  '@opentelemetry/instrumentation-pg@0.49.0':
+    resolution: {integrity: sha512-3alvNNjPXVdAPdY1G7nGRVINbDxRK02+KAugDiEpzw0jFQfU8IzFkSWA4jyU4/GbMxKvHD+XIOEfSjpieSodKw==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-redis-4@0.42.0':
-    resolution: {integrity: sha512-NaD+t2JNcOzX/Qa7kMy68JbmoVIV37fT/fJYzLKu2Wwd+0NCxt+K2OOsOakA8GVg8lSpFdbx4V/suzZZ2Pvdjg==}
+  '@opentelemetry/instrumentation-redis-4@0.45.0':
+    resolution: {integrity: sha512-Sjgym1xn3mdxPRH5CNZtoz+bFd3E3NlGIu7FoYr4YrQouCc9PbnmoBcmSkEdDy5LYgzNildPgsjx9l0EKNjKTQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-tedious@0.15.0':
-    resolution: {integrity: sha512-Kb7yo8Zsq2TUwBbmwYgTAMPK0VbhoS8ikJ6Bup9KrDtCx2JC01nCb+M0VJWXt7tl0+5jARUbKWh5jRSoImxdCw==}
+  '@opentelemetry/instrumentation-tedious@0.17.0':
+    resolution: {integrity: sha512-yRBz2409an03uVd1Q2jWMt3SqwZqRFyKoWYYX3hBAtPDazJ4w5L+1VOij71TKwgZxZZNdDBXImTQjii+VeuzLg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation-undici@0.6.0':
-    resolution: {integrity: sha512-ABJBhm5OdhGmbh0S/fOTE4N69IZ00CsHC5ijMYfzbw3E5NwLgpQk5xsljaECrJ8wz1SfXbO03FiSuu5AyRAkvQ==}
+  '@opentelemetry/instrumentation-undici@0.9.0':
+    resolution: {integrity: sha512-lxc3cpUZ28CqbrWcUHxGW/ObDpMOYbuxF/ZOzeFZq54P9uJ2Cpa8gcrC9F716mtuiMaekwk8D6n34vg/JtkkxQ==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.7.0
 
-  '@opentelemetry/instrumentation@0.52.1':
-    resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==}
-    engines: {node: '>=14'}
-    peerDependencies:
-      '@opentelemetry/api': ^1.3.0
-
   '@opentelemetry/instrumentation@0.53.0':
     resolution: {integrity: sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
 
-  '@opentelemetry/instrumentation@0.54.2':
-    resolution: {integrity: sha512-go6zpOVoZVztT9r1aPd79Fr3OWiD4N24bCPJsIKkBses8oyFo12F/Ew3UBTdIu6hsW4HC4MVEJygG6TEyJI/lg==}
+  '@opentelemetry/instrumentation@0.56.0':
+    resolution: {integrity: sha512-2KkGBKE+FPXU1F0zKww+stnlUxUTlBvLCiWdP63Z9sqXYeNI/ziNzsxAp4LAdUcTQmXjw1IWgvm5CAb/BHy99w==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.3.0
@@ -3243,32 +3399,114 @@ packages:
     resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==}
     engines: {node: '>=14'}
 
-  '@opentelemetry/resources@1.27.0':
-    resolution: {integrity: sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==}
+  '@opentelemetry/resources@1.30.1':
+    resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': '>=1.0.0 <1.10.0'
 
-  '@opentelemetry/sdk-trace-base@1.27.0':
-    resolution: {integrity: sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==}
+  '@opentelemetry/sdk-trace-base@1.30.1':
+    resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': '>=1.0.0 <1.10.0'
 
-  '@opentelemetry/semantic-conventions@1.25.1':
-    resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==}
-    engines: {node: '>=14'}
-
   '@opentelemetry/semantic-conventions@1.27.0':
     resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==}
     engines: {node: '>=14'}
 
+  '@opentelemetry/semantic-conventions@1.28.0':
+    resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==}
+    engines: {node: '>=14'}
+
   '@opentelemetry/sql-common@0.40.1':
     resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@opentelemetry/api': ^1.1.0
 
+  '@parcel/watcher-android-arm64@2.5.0':
+    resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [android]
+
+  '@parcel/watcher-darwin-arm64@2.5.0':
+    resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@parcel/watcher-darwin-x64@2.5.0':
+    resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@parcel/watcher-freebsd-x64@2.5.0':
+    resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@parcel/watcher-linux-arm-glibc@2.5.0':
+    resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm]
+    os: [linux]
+
+  '@parcel/watcher-linux-arm-musl@2.5.0':
+    resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm]
+    os: [linux]
+
+  '@parcel/watcher-linux-arm64-glibc@2.5.0':
+    resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@parcel/watcher-linux-arm64-musl@2.5.0':
+    resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@parcel/watcher-linux-x64-glibc@2.5.0':
+    resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [linux]
+
+  '@parcel/watcher-linux-x64-musl@2.5.0':
+    resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [linux]
+
+  '@parcel/watcher-win32-arm64@2.5.0':
+    resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@parcel/watcher-win32-ia32@2.5.0':
+    resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@parcel/watcher-win32-x64@2.5.0':
+    resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==}
+    engines: {node: '>= 10.0.0'}
+    cpu: [x64]
+    os: [win32]
+
+  '@parcel/watcher@2.5.0':
+    resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==}
+    engines: {node: '>= 10.0.0'}
+
   '@peculiar/asn1-android@2.3.10':
     resolution: {integrity: sha512-z9Rx9cFJv7UUablZISe7uksNbFJCq13hO0yEAOoIpAymALTLlvUOSLnGiQS7okPaM5dP42oTLhezH6XDXRXjGw==}
 
@@ -3292,8 +3530,8 @@ packages:
     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
     engines: {node: '>=14'}
 
-  '@prisma/instrumentation@5.19.1':
-    resolution: {integrity: sha512-VLnzMQq7CWroL5AeaW0Py2huiNKeoMfCH3SUxstdzPrlWQi6UQ9UrfcbUkNHlVFqOMacqy8X/8YtE0kuKDpD9w==}
+  '@prisma/instrumentation@5.22.0':
+    resolution: {integrity: sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==}
 
   '@readme/better-ajv-errors@1.6.0':
     resolution: {integrity: sha512-9gO9rld84Jgu13kcbKRU+WHseNhaVt76wYMeRDGsUGYxwJtI3RmEJ9LY9dZCYQGI8eUZLuxb5qDja0nqklpFjQ==}
@@ -3332,8 +3570,8 @@ packages:
       rollup:
         optional: true
 
-  '@rollup/pluginutils@5.1.3':
-    resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==}
+  '@rollup/pluginutils@5.1.4':
+    resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
       rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
@@ -3341,101 +3579,106 @@ packages:
       rollup:
         optional: true
 
-  '@rollup/rollup-android-arm-eabi@4.26.0':
-    resolution: {integrity: sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==}
+  '@rollup/rollup-android-arm-eabi@4.31.0':
+    resolution: {integrity: sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==}
     cpu: [arm]
     os: [android]
 
-  '@rollup/rollup-android-arm64@4.26.0':
-    resolution: {integrity: sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==}
+  '@rollup/rollup-android-arm64@4.31.0':
+    resolution: {integrity: sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==}
     cpu: [arm64]
     os: [android]
 
-  '@rollup/rollup-darwin-arm64@4.26.0':
-    resolution: {integrity: sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==}
+  '@rollup/rollup-darwin-arm64@4.31.0':
+    resolution: {integrity: sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==}
     cpu: [arm64]
     os: [darwin]
 
-  '@rollup/rollup-darwin-x64@4.26.0':
-    resolution: {integrity: sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==}
+  '@rollup/rollup-darwin-x64@4.31.0':
+    resolution: {integrity: sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==}
     cpu: [x64]
     os: [darwin]
 
-  '@rollup/rollup-freebsd-arm64@4.26.0':
-    resolution: {integrity: sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==}
+  '@rollup/rollup-freebsd-arm64@4.31.0':
+    resolution: {integrity: sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==}
     cpu: [arm64]
     os: [freebsd]
 
-  '@rollup/rollup-freebsd-x64@4.26.0':
-    resolution: {integrity: sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==}
+  '@rollup/rollup-freebsd-x64@4.31.0':
+    resolution: {integrity: sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==}
     cpu: [x64]
     os: [freebsd]
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.26.0':
-    resolution: {integrity: sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==}
+  '@rollup/rollup-linux-arm-gnueabihf@4.31.0':
+    resolution: {integrity: sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm-musleabihf@4.26.0':
-    resolution: {integrity: sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==}
+  '@rollup/rollup-linux-arm-musleabihf@4.31.0':
+    resolution: {integrity: sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-gnu@4.26.0':
-    resolution: {integrity: sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==}
+  '@rollup/rollup-linux-arm64-gnu@4.31.0':
+    resolution: {integrity: sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==}
     cpu: [arm64]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-musl@4.26.0':
-    resolution: {integrity: sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==}
+  '@rollup/rollup-linux-arm64-musl@4.31.0':
+    resolution: {integrity: sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==}
     cpu: [arm64]
     os: [linux]
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.26.0':
-    resolution: {integrity: sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==}
+  '@rollup/rollup-linux-loongarch64-gnu@4.31.0':
+    resolution: {integrity: sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==}
+    cpu: [loong64]
+    os: [linux]
+
+  '@rollup/rollup-linux-powerpc64le-gnu@4.31.0':
+    resolution: {integrity: sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==}
     cpu: [ppc64]
     os: [linux]
 
-  '@rollup/rollup-linux-riscv64-gnu@4.26.0':
-    resolution: {integrity: sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==}
+  '@rollup/rollup-linux-riscv64-gnu@4.31.0':
+    resolution: {integrity: sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==}
     cpu: [riscv64]
     os: [linux]
 
-  '@rollup/rollup-linux-s390x-gnu@4.26.0':
-    resolution: {integrity: sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==}
+  '@rollup/rollup-linux-s390x-gnu@4.31.0':
+    resolution: {integrity: sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==}
     cpu: [s390x]
     os: [linux]
 
-  '@rollup/rollup-linux-x64-gnu@4.26.0':
-    resolution: {integrity: sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==}
+  '@rollup/rollup-linux-x64-gnu@4.31.0':
+    resolution: {integrity: sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==}
     cpu: [x64]
     os: [linux]
 
-  '@rollup/rollup-linux-x64-musl@4.26.0':
-    resolution: {integrity: sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==}
+  '@rollup/rollup-linux-x64-musl@4.31.0':
+    resolution: {integrity: sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==}
     cpu: [x64]
     os: [linux]
 
-  '@rollup/rollup-win32-arm64-msvc@4.26.0':
-    resolution: {integrity: sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==}
+  '@rollup/rollup-win32-arm64-msvc@4.31.0':
+    resolution: {integrity: sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==}
     cpu: [arm64]
     os: [win32]
 
-  '@rollup/rollup-win32-ia32-msvc@4.26.0':
-    resolution: {integrity: sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==}
+  '@rollup/rollup-win32-ia32-msvc@4.31.0':
+    resolution: {integrity: sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==}
     cpu: [ia32]
     os: [win32]
 
-  '@rollup/rollup-win32-x64-msvc@4.26.0':
-    resolution: {integrity: sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==}
+  '@rollup/rollup-win32-x64-msvc@4.31.0':
+    resolution: {integrity: sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==}
     cpu: [x64]
     os: [win32]
 
   '@rtsao/scc@1.1.0':
     resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
 
-  '@rushstack/node-core-library@5.9.0':
-    resolution: {integrity: sha512-MMsshEWkTbXqxqFxD4gcIUWQOCeBChlGczdZbHfqmNZQFLHB3yWxDFSMHFUdu2/OB9NUk7Awn5qRL+rws4HQNg==}
+  '@rushstack/node-core-library@5.10.2':
+    resolution: {integrity: sha512-xOF/2gVJZTfjTxbo4BDj9RtQq/HFnrrKdtem4JkyRLnwsRz2UDTg8gA1/et10fBx5RxmZD9bYVGST69W8ME5OQ==}
     peerDependencies:
       '@types/node': '*'
     peerDependenciesMeta:
@@ -3445,65 +3688,63 @@ packages:
   '@rushstack/rig-package@0.5.3':
     resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==}
 
-  '@rushstack/terminal@0.14.2':
-    resolution: {integrity: sha512-2fC1wqu1VCExKC0/L+0noVcFQEXEnoBOtCIex1TOjBzEDWcw8KzJjjj7aTP6mLxepG0XIyn9OufeFb6SFsa+sg==}
+  '@rushstack/terminal@0.14.5':
+    resolution: {integrity: sha512-TEOpNwwmsZVrkp0omnuTUTGZRJKTr6n6m4OITiNjkqzLAkcazVpwR1SOtBg6uzpkIBLgrcNHETqI8rbw3uiUfw==}
     peerDependencies:
       '@types/node': '*'
     peerDependenciesMeta:
       '@types/node':
         optional: true
 
-  '@rushstack/ts-command-line@4.23.0':
-    resolution: {integrity: sha512-jYREBtsxduPV6ptNq8jOKp9+yx0ld1Tb/Tkdnlj8gTjazl1sF3DwX2VbluyYrNd0meWIL0bNeer7WDf5tKFjaQ==}
+  '@rushstack/ts-command-line@4.23.3':
+    resolution: {integrity: sha512-HazKL8fv4HMQMzrKJCrOrhyBPPdzk7iajUXgsASwjQ8ROo1cmgyqxt/k9+SdmrNLGE1zATgRqMUH3s/6smbRMA==}
 
   '@sec-ant/readable-stream@0.4.1':
     resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
 
-  '@sentry/core@8.38.0':
-    resolution: {integrity: sha512-sGD+5TEHU9G7X7zpyaoJxpOtwjTjvOd1f/MKBrWW2vf9UbYK+GUJrOzLhMoSWp/pHSYgvObkJkDb/HwieQjvhQ==}
+  '@sentry/core@8.50.0':
+    resolution: {integrity: sha512-q71m8Ha9YGwqn4Gd7sWvcFTRgbHXxEfU4QeIFtwMBpwHfq2Q+9koiF8DOoOHqIEOsnlvZWRQgGggIOdHzajnVw==}
     engines: {node: '>=14.18'}
 
-  '@sentry/node@8.38.0':
-    resolution: {integrity: sha512-nwW0XqZFQseXYn0i6i6nKPkbjgHMBEFSF9TnK6mHHqJHHObHIZ6qu5CfvGKgxATia8JPIg9NN8XcyYOnQMi07w==}
+  '@sentry/node@8.50.0':
+    resolution: {integrity: sha512-I9eGIdcoWKVy4O8a1f2t0jGVTdN1z9McxbGW8aWwDE5Vd9gpuNjFh9qGapmBEPzysWBX8rjsemDdSa3TcijJMw==}
     engines: {node: '>=14.18'}
 
-  '@sentry/opentelemetry@8.38.0':
-    resolution: {integrity: sha512-AfjmIf/v7+x2WplhkX66LyGKvrzzPeSgff9uJ0cFCC2s0yd1qA2VPuIwEyr5i/FOJOP5bvFr8tu/hz3LA4+F5Q==}
+  '@sentry/opentelemetry@8.50.0':
+    resolution: {integrity: sha512-uAZjAMPAulFHL88ThK2k+XPx2QzvZ/I7e7sP1In28Tb/yLH0mi+51AUH+zcnLELIPC86m1aDYl8uwYcP6tV4dA==}
     engines: {node: '>=14.18'}
     peerDependencies:
       '@opentelemetry/api': ^1.9.0
-      '@opentelemetry/core': ^1.25.1
-      '@opentelemetry/instrumentation': ^0.54.0
-      '@opentelemetry/sdk-trace-base': ^1.26.0
-      '@opentelemetry/semantic-conventions': ^1.27.0
+      '@opentelemetry/core': ^1.29.0
+      '@opentelemetry/instrumentation': ^0.56.0
+      '@opentelemetry/sdk-trace-base': ^1.29.0
+      '@opentelemetry/semantic-conventions': ^1.28.0
 
-  '@sentry/profiling-node@8.38.0':
-    resolution: {integrity: sha512-7I+hANLQRUAciRLzz4nUehEUHYeMNKkMfu6KkBuLcD1F3x7Y/tcxyHlHl+bNKf6tyUdW7IKGb+7Pk/WKMBnZrg==}
+  '@sentry/profiling-node@8.50.0':
+    resolution: {integrity: sha512-HyjW6burG+oBoLK+O9TMT5kPRKeOzM/rwchBfqqjht1babrApMeuycz1nFlJbYC38D0AK0jufjN4kwiU+Z+pNQ==}
     engines: {node: '>=14.18'}
     hasBin: true
 
-  '@sentry/types@8.38.0':
-    resolution: {integrity: sha512-fP5H9ZX01W4Z/EYctk3mkSHi7d06cLcX2/UWqwdWbyPWI+pL2QpUPICeO/C+8SnmYx//wFj3qWDhyPCh1PdFAA==}
-    engines: {node: '>=14.18'}
+  '@shikijs/core@1.27.2':
+    resolution: {integrity: sha512-ns1dokDr0KE1lQ9mWd4rqaBkhSApk0qGCK1+lOqwnkQSkVZ08UGqXj1Ef8dAcTMZNFkN6PSNjkL5TYNX7pyPbQ==}
 
-  '@sentry/utils@8.38.0':
-    resolution: {integrity: sha512-3X7MgIKIx+2q5Al7QkhaRB4wV6DvzYsaeIwdqKUzGLuRjXmNgJrLoU87TAwQRmZ6Wr3IoEpThZZMNrzYPXxArw==}
-    engines: {node: '>=14.18'}
+  '@shikijs/engine-javascript@1.27.2':
+    resolution: {integrity: sha512-0JB7U5vJc16NShBdxv9hSSJYSKX79+32O7F4oXIxJLdYfomyFvx4B982ackUI9ftO9T3WwagkiiD3nOxOOLiGA==}
 
-  '@shikijs/core@1.22.2':
-    resolution: {integrity: sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==}
+  '@shikijs/engine-oniguruma@1.27.2':
+    resolution: {integrity: sha512-FZYKD1KN7srvpkz4lbGLOYWlyDU4Rd+2RtuKfABTkafAPOFr+J6umfIwY/TzOQqfNtWjL7SAwPAO0dcOraRLaQ==}
 
-  '@shikijs/engine-javascript@1.22.2':
-    resolution: {integrity: sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==}
+  '@shikijs/langs@1.27.2':
+    resolution: {integrity: sha512-MSrknKL0DbeXvhtSigMLIzjPOOQfvK7fsbcRv2NUUB0EvuTTomY8/U+lAkczYrXY2+dygKOapJKk8ScFYbtoNw==}
 
-  '@shikijs/engine-oniguruma@1.22.2':
-    resolution: {integrity: sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==}
+  '@shikijs/themes@1.27.2':
+    resolution: {integrity: sha512-Yw/uV7EijjWavIIZLoWneTAohcbBqEKj6XMX1bfMqO3llqTKsyXukPp1evf8qPqzUHY7ibauqEaQchhfi857mg==}
 
-  '@shikijs/types@1.22.2':
-    resolution: {integrity: sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==}
+  '@shikijs/types@1.27.2':
+    resolution: {integrity: sha512-DM9OWUyjmdYdnKDpaGB/GEn9XkToyK1tqxuqbmc5PV+5K8WjjwfygL3+cIvbkSw2v1ySwHDgqATq/+98pJ4Kyg==}
 
-  '@shikijs/vscode-textmate@9.3.0':
-    resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==}
+  '@shikijs/vscode-textmate@10.0.1':
+    resolution: {integrity: sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==}
 
   '@sideway/address@4.1.4':
     resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
@@ -3517,15 +3758,12 @@ packages:
   '@sideway/pinpoint@2.0.0':
     resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
 
-  '@simplewebauthn/server@10.0.1':
-    resolution: {integrity: sha512-djNWcRn+H+6zvihBFJSpG3fzb0NQS9c/Mw5dYOtZ9H+oDw8qn9Htqxt4cpqRvSOAfwqP7rOvE9rwqVaoGGc3hg==}
+  '@simplewebauthn/server@12.0.0':
+    resolution: {integrity: sha512-aJdTe9GikOk40U7Q5Mm/Sqkxcq4a2oPZAcLcnyqMyFqrUaOS6vdsZW8/H3Mnsw9umcr88pcgB7kozPPt+5wOBw==}
     engines: {node: '>=20.0.0'}
 
-  '@simplewebauthn/types@10.0.0':
-    resolution: {integrity: sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==}
-
-  '@simplewebauthn/types@11.0.0':
-    resolution: {integrity: sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA==}
+  '@simplewebauthn/types@12.0.0':
+    resolution: {integrity: sha512-q6y8MkoV8V8jB4zzp18Uyj2I7oFp2/ONL8c3j8uT06AOWu3cIChc1au71QYHrP2b+xDapkGTiv+9lX7xkTlAsA==}
 
   '@sinclair/typebox@0.27.8':
     resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@@ -3534,10 +3772,6 @@ packages:
     resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
     engines: {node: '>=10'}
 
-  '@sindresorhus/is@5.3.0':
-    resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==}
-    engines: {node: '>=14.16'}
-
   '@sindresorhus/is@7.0.1':
     resolution: {integrity: sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==}
     engines: {node: '>=18'}
@@ -3790,123 +4024,123 @@ packages:
   '@sqltools/formatter@1.2.5':
     resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
 
-  '@storybook/addon-actions@8.4.4':
-    resolution: {integrity: sha512-+Dd6alcieS6UN7IKhXLuhyQYQMu9HG/Tdr790a4EOQKpJM1NxIMuPuUH3fAoKfa9VhtI1BxTBr7zNtzg9Akqhg==}
+  '@storybook/addon-actions@8.5.0':
+    resolution: {integrity: sha512-6CW9+17rk5eNx6I8EKqCxRKtsJFTR/lHL+xiJ6/iBWApIm8sg63vhXvUTJ58UixmIkT5oLh0+ESNPh+x10D8fw==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-backgrounds@8.4.4':
-    resolution: {integrity: sha512-asaGD4ruIPFthyhpByQSJagvtNN7EGKdHj5yMnsMvkSXnN0r1uVkI2/Z37hmLt02Qbzf6OQiBPW5TDL+X+EEBg==}
+  '@storybook/addon-backgrounds@8.5.0':
+    resolution: {integrity: sha512-lzyFLs7niNsqlhH5kdUrp7htLiMIcjY50VLWe0PaeJ6T6GZ7X9qhQzROAUV6cGqzyd8A6y/LzIUntDPMVEm/6g==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-controls@8.4.4':
-    resolution: {integrity: sha512-FbZRbwJQggLz6M3zB6scCp1SDGwQ5zdiD6sjBilZzgGO5rBFqG0A8PoOyr4iPrLU2y/NZBdRrJBD+6MkaJ+yzw==}
+  '@storybook/addon-controls@8.5.0':
+    resolution: {integrity: sha512-1fivx77A/ahObrPl0L66o9i9MUNfqXxsrpekne5gjMNXw9XJFIRNUe/ddL4CMmwu7SgVbj2QV+q5E5mlnZNTJw==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-docs@8.4.4':
-    resolution: {integrity: sha512-wuHaStfpd2rkAN5Lf0qmvE3JKTHTEDbnAMTvfs9inzGBL0iAwBLjW48/ll7lLkJ2E3k/FQtaevNpuc7C52u1Bw==}
+  '@storybook/addon-docs@8.5.0':
+    resolution: {integrity: sha512-REwLSr1VgOVNJZwP3y3mldhOjBHlM5fqTvq/tC8NaYpAzx9O4rZdoUSZxW3tYtoNoYrHpB8kzRTeZl8WSdKllw==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-essentials@8.4.4':
-    resolution: {integrity: sha512-0ObUQ98zZkeWqP2k3Un5jny3WxT3THgUKZUGD+mR8eq6CuTmJ3bUXWzDHreuDxQwgr8s5f04XD8IcRvjZ9IRgA==}
+  '@storybook/addon-essentials@8.5.0':
+    resolution: {integrity: sha512-RrHRdaw2j3ugZiYQ6OHt3Ff08ID4hwAvipqULEsbEnEw3VlXOaW/MT5e2M7kW3MHskQ3iJ6XAD1Y1rNm432Pzw==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-highlight@8.4.4':
-    resolution: {integrity: sha512-k7EUxiMe8RCasmgfa6ZKx7UG6kU9RooTYGwqY5TG5xAQOzDwKn4qom+OYkT/9/6lORhJrUe2GgQLCrq/WGpS1A==}
+  '@storybook/addon-highlight@8.5.0':
+    resolution: {integrity: sha512-/JxYzMK5aJSYs0K/0eAEFyER2dMoxqwM891MdnkNwLFdyrM58lzHee00F9oEX6zeQoRUNQPRepq0ui2PvbTMGw==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-interactions@8.4.4':
-    resolution: {integrity: sha512-izqcc6tY0BiKW7DYrEnoXUEH9FYDPWNfQnqqE0nVBv3BS2DoNmm8M9SB8fZx7pPfw53cMJBGt3vrlY0Wtxy1+Q==}
+  '@storybook/addon-interactions@8.5.0':
+    resolution: {integrity: sha512-vX1a8qS7o/W3kEzfL/CqOj/Rr6UlGLT/n0KXMpfIhx63tzxe1a1qGpFLL0h0zqAVPHZIOu9humWMKri5Iny6oA==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-links@8.4.4':
-    resolution: {integrity: sha512-hqTv06fPq9k5GUZD8JR49ANw5sBg8EYAsuCNoSd9OwVSBO/3y53HrMA0NCILUK8hnupPvtBuKXXoHmHes9R+1g==}
+  '@storybook/addon-links@8.5.0':
+    resolution: {integrity: sha512-Y11GIByAYqn0TibI/xsy0vCe+ZxJS9PVAAoHngLxkf9J4WodAXcJABr8ZPlWDNdaEhSS/FF7UQUmNag0UC2/pw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.4.4
+      storybook: ^8.5.0
     peerDependenciesMeta:
       react:
         optional: true
 
-  '@storybook/addon-mdx-gfm@8.4.4':
-    resolution: {integrity: sha512-dj98NGWowhSwWYn2LUaLMxHNvBY+73n9CFsELrttg24nOxmeRfku0uh2hp5epMmRMX3Fej7nCkKNJaU1fihZ+Q==}
+  '@storybook/addon-mdx-gfm@8.5.0':
+    resolution: {integrity: sha512-UDKKftEWrhUiBRR6Ew+1lDJMV+TU0gZ0v4aLN6erOKswHI5ilm+L1xmixf8ZHkgaUIJ6JDHOMMfAhj1CP62LTw==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-measure@8.4.4':
-    resolution: {integrity: sha512-KsjrwrXwrI+z7hKKfjyY1w1b0gLSLZmp15vIRJMELybWV0+4bZFLJGwMBOQFx+aWBED8yZrRV9OjTmoczawsZg==}
+  '@storybook/addon-measure@8.5.0':
+    resolution: {integrity: sha512-e8pJy2sICyj0Ff0W1PFc6HPE6PqcjnnHtfuDaO3M9uSKJLYkpTWJ8i1VSP178f8seq44r5/PdQCHqs5q5l3zgw==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-outline@8.4.4':
-    resolution: {integrity: sha512-CVS1dm6BNUWKGrJj9E1ThBp5Khe6Yw+Hhz6OFxrPZfoTr6RstwoTmvSpKjDUCn8zj6ujoORdiQUh1FsHOxAPBg==}
+  '@storybook/addon-outline@8.5.0':
+    resolution: {integrity: sha512-r12sk1b38Ph6NroWAOTfjbJ/V+gDobm7tKQQlbSDf6fgX7cqyPHmKjfNDCOCQpXouZm/Jm+41zd758PW+Yt4ng==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-storysource@8.4.4':
-    resolution: {integrity: sha512-BuMQMQvYqiaosbGkUxDPU2nfZtI2E/zxpNaubpUAH2j+bx4zdXRXyW1P71wj5GZC84bszoyXhdd++9A0knmaYA==}
+  '@storybook/addon-storysource@8.5.0':
+    resolution: {integrity: sha512-AvnWIJk1CNHStvLHZp4AK/MqU4IWLt0O6CsfCpH868EgfHcnQ4kbELTVSbMCMraBfcvOtbXidEWTUb+/Pc2KWg==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-toolbars@8.4.4':
-    resolution: {integrity: sha512-ENPshJMDpfzOJ4Tgm1hSzQoaEmgDxCtP6C8LKk4MOd3X92MJ7p6kfb3y3R1BLg4E/g90qp6lKPFdcohS2tKCgQ==}
+  '@storybook/addon-toolbars@8.5.0':
+    resolution: {integrity: sha512-q3yYYO2WX8K2DYNM++FzixGDjzYaeREincgsl2WXYXrcuGb5hkOoOgRiAQL8Nz9NQ1Eo+B/yZxrhG/5VoVhUUQ==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/addon-viewport@8.4.4':
-    resolution: {integrity: sha512-SRHJlLhf3tu7+sYNfVIYTeMegn6aiv4HGX97ZLvL76NWWBU8BntQ1LKMki7475mWiZNUFMoYYPsHlG+HU9FAtg==}
+  '@storybook/addon-viewport@8.5.0':
+    resolution: {integrity: sha512-MlhVELImk9YzjEgGR2ciLC8d5tUSGcO7my4kWIClN0VyTRcvG4ZfwrsEC+jN3/l52nrgjLmKrDX5UAGZm6w5mQ==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/blocks@8.4.4':
-    resolution: {integrity: sha512-LwM3guL7uWpYR1a/SY0KZjCUskTKEaS22eF7GK8iXAV5BY4KpKr6ArW4O9orK29KtFwKhDZQLcMcECsOJBVk/A==}
+  '@storybook/blocks@8.5.0':
+    resolution: {integrity: sha512-2sTOgjH/JFOgWnpqkKjpKVvKAgUaC9ZBjH1gnCoA5dne/SDafYaCAYfv6yZn7g2Xm1sTxWCAmMIUkYSALeWr+w==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.4.4
+      storybook: ^8.5.0
     peerDependenciesMeta:
       react:
         optional: true
       react-dom:
         optional: true
 
-  '@storybook/builder-vite@8.4.4':
-    resolution: {integrity: sha512-UfPzE0p2xvBK7sA853N3VN+Plfw6/DIVppwbgsaRdzie52QXZQrl60u0igD47DHi6+xbqCBWDz7up4h3k00Z5A==}
+  '@storybook/builder-vite@8.5.0':
+    resolution: {integrity: sha512-GVJFjAxX/mL3bmXX6N619ShuYprkh6Ix08JU6QGNf/tTkG92BxjgCqQdfovBrviDhFyO2bhkdlEp6ujMo5CbZA==}
     peerDependencies:
-      storybook: ^8.4.4
-      vite: ^4.0.0 || ^5.0.0
+      storybook: ^8.5.0
+      vite: ^4.0.0 || ^5.0.0 || ^6.0.0
 
-  '@storybook/components@8.4.4':
-    resolution: {integrity: sha512-0BSZVmsk23C0BSRKx3liZSVQFXtoF86XQFdNQxjrXIwdHIEN7TcL3DwcxeVUU5ilGp7HeDgAydGNIPGgTeEe6g==}
+  '@storybook/components@8.5.0':
+    resolution: {integrity: sha512-DhaHtwfEcfWYj3ih/5RBSDHe3Idxyf+oHw2/DmaLKJX6MluhdK3ZqigjRcTmA9Gj/SbR4CkHEEtDzAvBlW0BYw==}
     peerDependencies:
       storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
 
-  '@storybook/core-events@8.4.4':
-    resolution: {integrity: sha512-pkwr0UU95WSJtn9Q7q5ip0x8WxerLf5z4CWonvymGu9Z0bZyMXeA+GOEt/YQIJgqI4fbTK8Jqi+suC6ibUu9oQ==}
+  '@storybook/core-events@8.5.0':
+    resolution: {integrity: sha512-vT0nEG+I3pM03CWZtUc/zt8TGOJeC9KvustclOAR6u8jU9f181EcLEJkLy3TOrN/NhyqPm7O9M7LR3ayUPThMA==}
     peerDependencies:
       storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
 
-  '@storybook/core@8.4.4':
-    resolution: {integrity: sha512-WjTmJpsHsFCd7tQ/8jFpDWjhntauXcWYYTcEZk56Pq4miyNrrXhV0S80Gxv3Uvzk0jocgtT2AKf8rQuH2UkQEg==}
+  '@storybook/core@8.5.0':
+    resolution: {integrity: sha512-apborO6ynns7SeydBSqE9o0zT6JSU+VY4gLFPJROGcconvSW4bS5xtJCsgjlulceyWVxepFHGXl4jEZw+SktXA==}
     peerDependencies:
       prettier: ^2 || ^3
     peerDependenciesMeta:
       prettier:
         optional: true
 
-  '@storybook/csf-plugin@8.4.4':
-    resolution: {integrity: sha512-4+6SUhp5sEJN9BY5RuxcFKvJbOqCzIUp9oHSSz36hkP07a4QH+SwxfEd0U7JRfmPpB63L+izywTzWhdADiAMOQ==}
+  '@storybook/csf-plugin@8.5.0':
+    resolution: {integrity: sha512-cs6ogviNyLG1h9J8Sb47U3DqIrQmn2EHm4ta3fpCeV3ABbrMgbzYyxtmybz4g/AwlDgjAZAt6PPcXkfCJ6p2CQ==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/csf@0.1.11':
-    resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==}
+  '@storybook/csf@0.1.12':
+    resolution: {integrity: sha512-9/exVhabisyIVL0VxTCxo01Tdm8wefIXKXfltAPTSr8cbLn5JAxGQ6QV3mjdecLGEOucfoVhAKtJfVHxEK1iqw==}
 
   '@storybook/global@5.0.0':
     resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
@@ -3918,45 +4152,49 @@ packages:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
 
-  '@storybook/instrumenter@8.4.4':
-    resolution: {integrity: sha512-mq/YVEZrB8jyyio2Of01rQixsQ72z8ssAhJS9ldIlK+cvERQi0VBCpH3pejPmjOB40yiKBJHNqH4HIANVhibgw==}
+  '@storybook/instrumenter@8.5.0':
+    resolution: {integrity: sha512-eZ/UY6w4U2vay+wX7QVwKiRoyMzZscuv6v4k4r8BlmHPFWbhiZDO9S2GsG16UkyKnrQrYk432he70n7hn1Xvmg==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/manager-api@8.4.4':
-    resolution: {integrity: sha512-rmNPcbEyzakEHoaecUbhkW7WWOkyZ0z7ywH4d5/s0ZuQS57Px2N+ZLVgRJwYK+YNHiJYqDf1BTln9YJ/Mt1L6Q==}
+  '@storybook/manager-api@8.5.0':
+    resolution: {integrity: sha512-Ildriueo3eif4M+gMlMxu/mrBIbAnz8+oesmQJKdzZfe/U9eQTI9OUqJsxx/IVBmdzQ3ySsgNmzj5VweRkse4A==}
     peerDependencies:
       storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
 
-  '@storybook/preview-api@8.4.4':
-    resolution: {integrity: sha512-iZrWQcjRBqBHFdDXVxGpw6mHBZMCMYqhWXdyJ0d1S2y3PwcfOjkcXlQ1UiAenFHlA6dKrcYw8luKUQTL9bKReA==}
+  '@storybook/preview-api@8.5.0':
+    resolution: {integrity: sha512-g0XbD54zMUkl6bpuA7qEBCE9rW1QV6KKmwkO4bkxMOJcMke3x9l00JTaYn7Un8wItjXiS3BIG15B6mnfBG7fng==}
     peerDependencies:
       storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
 
-  '@storybook/react-dom-shim@8.4.4':
-    resolution: {integrity: sha512-kufv2FDK3kjADBo+/aKHsUn9T5E4p9IBAmCoIvXBGRDumPRds7Pt3MB4ODKlg+IumR7LMEq0jTJkn27ZRTuUmw==}
+  '@storybook/react-dom-shim@8.5.0':
+    resolution: {integrity: sha512-7P8xg4FiuFpM6kQOzZynno+0zyLVs8NgsmRK58t3JRZXbda1tzlxTXzvqx4hUevvbPJGjmrB0F3xTFH+8Otnvw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/react-vite@8.4.4':
-    resolution: {integrity: sha512-NbTAY4R526hJ+gz7BFLS1HpGx3BikQDbq1BuEcaWsf/rJnygwlzeQmdPyfrfNC8R0ufIKRWUiPrPmMvrf8ZI6A==}
+  '@storybook/react-vite@8.5.0':
+    resolution: {integrity: sha512-4f5AM8aPs2aTBeiycotinaDIPJg/YRtPb0F1dDquS097eUOeImS73+NSSCwrIjmSiapG/KWVkPgFnadEumFkAA==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
+      '@storybook/test': 8.5.0
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.4.4
-      vite: ^4.0.0 || ^5.0.0
+      storybook: ^8.5.0
+      vite: ^4.0.0 || ^5.0.0 || ^6.0.0
+    peerDependenciesMeta:
+      '@storybook/test':
+        optional: true
 
-  '@storybook/react@8.4.4':
-    resolution: {integrity: sha512-92lGnRcAI2qW6zH8GMBScyXmOS1ANI8ZuSP4ExQj+lGsCrAr7PBr0wuHy3wIn1YyAvQGPUn/mpYrmMz08c2HfA==}
+  '@storybook/react@8.5.0':
+    resolution: {integrity: sha512-/jbkmGGc95N7KduIennL/k8grNTP5ye/YBnkcS4TbF7uDWBtKy3/Wqvx5BIlFXq3qeUnZJ8YtZc0lPIYeCY8XQ==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      '@storybook/test': 8.4.4
+      '@storybook/test': 8.5.0
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
-      storybook: ^8.4.4
+      storybook: ^8.5.0
       typescript: '>= 4.2.x'
     peerDependenciesMeta:
       '@storybook/test':
@@ -3964,40 +4202,46 @@ packages:
       typescript:
         optional: true
 
-  '@storybook/source-loader@8.4.4':
-    resolution: {integrity: sha512-xaC23ljSEpHSMdp/VdqKd1o4Dr7x5lA2897RR6SKFRFDgkKD5Mp1UXsrcwqSZNSeXETTmVWXf8rHrz14VKkK6w==}
+  '@storybook/source-loader@8.5.0':
+    resolution: {integrity: sha512-XsXeYakkjZ2TjvLBfr/vH1G/NK3ZVrU/asI7gqEyzd725YiM12sLp7zlnIpVtGJTCRCzWn69jqzmc4WifFon/g==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/test@8.4.4':
-    resolution: {integrity: sha512-tmJd+lxl3MC0Xdu1KW/69V8tibv98OvdopxGqfVR0x5dkRHM3sFK/tv1ZJAUeronlvRyhGySOu1tHUrMjcNqyA==}
+  '@storybook/test@8.5.0':
+    resolution: {integrity: sha512-M/DdPlI6gwL7NGkK5o7GYjdEBp95AsFEUtW29zQfnVIAngYugzi3nIuM/XkQHunidVdAZCYjw2s2Yhhsx/m9sw==}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
 
-  '@storybook/theming@8.4.4':
-    resolution: {integrity: sha512-iq4yt3Fx35ZV5owNC//E6G+QPV19xHHVN2Ugi3p7KOSFK3chuXX9mxZ1rfir+t+U30a5EPOEnlsY3/1LXn7aTw==}
+  '@storybook/theming@8.5.0':
+    resolution: {integrity: sha512-591LbOj/HMmHYUfLgrMerxhF1A9mY61HWKxcRpB6xxalc1Xw1kRtQ49DcwuTXnUu9ktBB3nuOzPNPQPFSh/7PQ==}
     peerDependencies:
       storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
 
-  '@storybook/types@8.4.4':
-    resolution: {integrity: sha512-NUeIhecJ+i2ul/u/ftV+f9gBT2cUOuLjgy1a+l0UbJd7n3wwN17vX2zrrDkrGG3dp3edr8bWMGjAN3WERJje1A==}
+  '@storybook/types@8.5.0':
+    resolution: {integrity: sha512-5orPpfLvSksGH341ibmpFnV86yqlZMbvNax+a+z1h56UKRA+4c/hgdRQl1brs7YaQzrgJ2wUX7PAlJjBJ1erUQ==}
     peerDependencies:
       storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
 
-  '@storybook/vue3-vite@8.4.4':
-    resolution: {integrity: sha512-cyhPX16KzOWuHZCcMXqJ+k11xOvelWXmML6pSvhV0OtizDHxgesCdbSa1X1P76ZszjOlt8MJfPiSaE+XNwB0UQ==}
+  '@storybook/vue3-vite@8.5.0':
+    resolution: {integrity: sha512-gcIX9UBUMI/HQsCpQC2V9XZc32RO1H4sE4/FkSFxZVOLeQHcAFLQDuUXBpIrIOLl9ouoBE3jzVyYApejvyhO1w==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      storybook: ^8.4.4
-      vite: ^4.0.0 || ^5.0.0
+      storybook: ^8.5.0
+      vite: ^4.0.0 || ^5.0.0 || ^6.0.0
 
-  '@storybook/vue3@8.4.4':
-    resolution: {integrity: sha512-HVUtE8x4nIJeCO592VNyrACMgGA6ViarRS6Faw+MWdUQXnZlwkadGusx2T++hnaalAt9VJLF5NRIcV8O7dA6Ig==}
+  '@storybook/vue3@8.5.0':
+    resolution: {integrity: sha512-bml2I00QhkSI23dCqBPhLWSCNDCFQJ23cJ/NPAVeJ6FZXrWBa3EC7wFAnXzvzPUxvTyB036jFjvsWraOWEANTA==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      storybook: ^8.4.4
+      storybook: ^8.5.0
       vue: ^3.0.0
 
+  '@stylistic/eslint-plugin@2.13.0':
+    resolution: {integrity: sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: '>=8.40.0'
+
   '@swc/cli@0.3.12':
     resolution: {integrity: sha512-h7bvxT+4+UDrLWJLFHt6V+vNAcUNii2G4aGSSotKz1ECEk4MyEh5CWxmeSscwuz5K3i+4DWTgm4+4EyMCQKn+g==}
     engines: {node: '>= 16.14.0'}
@@ -4176,12 +4420,12 @@ packages:
     resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
     engines: {node: '>=14.16'}
 
-  '@tabler/icons-webfont@https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz':
-    resolution: {tarball: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz}
-    version: 3.29.0-mi.1913
+  '@tabler/icons-webfont@https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz':
+    resolution: {tarball: https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz}
+    version: 3.30.0-mi.1932
 
-  '@tabler/icons@3.29.0':
-    resolution: {integrity: sha512-VWNINymdmhay3MDvWVREmRwuWLSrX3YiInKvs5L4AHRF4bAfJabLlEReE0BW/XFsBt22ff8/C8Eam/LXlF97mA==}
+  '@tabler/icons@3.30.0':
+    resolution: {integrity: sha512-c8OKLM48l00u9TFbh2qhSODMONIzML8ajtCyq95rW8vzkWcBrKRPM61tdkThz2j4kd5u17srPGIjqdeRUZdfdw==}
 
   '@tensorflow/tfjs-backend-cpu@4.4.0':
     resolution: {integrity: sha512-d4eln500/qNym78z9IrUUzF0ITBoJGLrxV8xd92kLVoXhg35Mm+zqUXShjFcrH8joOHOFuST0qZ0TbDDqcPzPA==}
@@ -4352,12 +4596,12 @@ packages:
   '@types/fluent-ffmpeg@2.1.27':
     resolution: {integrity: sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A==}
 
-  '@types/glob@7.2.0':
-    resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
-
   '@types/graceful-fs@4.1.6':
     resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==}
 
+  '@types/hammerjs@2.0.46':
+    resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==}
+
   '@types/hast@3.0.4':
     resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
 
@@ -4388,9 +4632,6 @@ packages:
   '@types/jsdom@21.1.7':
     resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
 
-  '@types/json-schema@7.0.12':
-    resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
-
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
@@ -4400,8 +4641,8 @@ packages:
   '@types/jsonld@1.5.15':
     resolution: {integrity: sha512-PlAFPZjL+AuGYmwlqwKEL0IMP8M8RexH0NIPGfCVWSQ041H2rR/8OlyZSD7KsCVoN8vCfWdtWDBxX8yBVP+xow==}
 
-  '@types/jsrsasign@10.5.14':
-    resolution: {integrity: sha512-lppSlfK6etu+cuKs40K4rg8As79PH6hzIB+v55zSqImbSH3SE6Fm8MBHCiI91cWlAP3Z4igtJK1VL3fSN09blQ==}
+  '@types/jsrsasign@10.5.15':
+    resolution: {integrity: sha512-3stUTaSRtN09PPzVWR6aySD9gNnuymz+WviNHoTb85dKu+BjaV4uBbWWGykBBJkfwPtcNZVfTn2lbX00U+yhpQ==}
 
   '@types/keyv@3.1.4':
     resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
@@ -4427,9 +4668,6 @@ packages:
   '@types/mime@3.0.1':
     resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
 
-  '@types/minimatch@5.1.2':
-    resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
-
   '@types/minimist@1.2.2':
     resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
 
@@ -4442,11 +4680,14 @@ packages:
   '@types/node-fetch@2.6.11':
     resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
 
+  '@types/node@22.10.7':
+    resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==}
+
   '@types/node@22.9.0':
     resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==}
 
-  '@types/nodemailer@6.4.16':
-    resolution: {integrity: sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==}
+  '@types/nodemailer@6.4.17':
+    resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
 
   '@types/normalize-package-data@2.4.1':
     resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
@@ -4535,9 +4776,6 @@ packages:
   '@types/serviceworker@0.0.67':
     resolution: {integrity: sha512-7TCH7iNsCSNb+aUD9M/36TekrWFSLCjNK8zw/3n5kOtRjbLtDfGYMXTrDnGhSfqXNwpqmt9Vd90w5C/ad1tX6Q==}
 
-  '@types/shimmer@1.0.5':
-    resolution: {integrity: sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==}
-
   '@types/shimmer@1.2.0':
     resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
 
@@ -4602,24 +4840,13 @@ packages:
     resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==}
 
   '@types/yargs-parser@21.0.0':
-    resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
-
-  '@types/yargs@17.0.19':
-    resolution: {integrity: sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ==}
-
-  '@types/yauzl@2.10.0':
-    resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
-
-  '@typescript-eslint/eslint-plugin@7.1.0':
-    resolution: {integrity: sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      '@typescript-eslint/parser': ^7.0.0
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
+    resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
+
+  '@types/yargs@17.0.19':
+    resolution: {integrity: sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ==}
+
+  '@types/yauzl@2.10.0':
+    resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
 
   '@typescript-eslint/eslint-plugin@7.17.0':
     resolution: {integrity: sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==}
@@ -4632,15 +4859,13 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/parser@7.1.0':
-    resolution: {integrity: sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==}
-    engines: {node: ^16.0.0 || >=18.0.0}
+  '@typescript-eslint/eslint-plugin@8.20.0':
+    resolution: {integrity: sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
+      '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.8.0'
 
   '@typescript-eslint/parser@7.17.0':
     resolution: {integrity: sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==}
@@ -4652,23 +4877,20 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/scope-manager@7.1.0':
-    resolution: {integrity: sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==}
-    engines: {node: ^16.0.0 || >=18.0.0}
+  '@typescript-eslint/parser@8.20.0':
+    resolution: {integrity: sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.8.0'
 
   '@typescript-eslint/scope-manager@7.17.0':
     resolution: {integrity: sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==}
     engines: {node: ^18.18.0 || >=20.0.0}
 
-  '@typescript-eslint/type-utils@7.1.0':
-    resolution: {integrity: sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      eslint: ^8.56.0
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
+  '@typescript-eslint/scope-manager@8.20.0':
+    resolution: {integrity: sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@typescript-eslint/type-utils@7.17.0':
     resolution: {integrity: sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==}
@@ -4680,22 +4902,20 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/types@7.1.0':
-    resolution: {integrity: sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==}
-    engines: {node: ^16.0.0 || >=18.0.0}
+  '@typescript-eslint/type-utils@8.20.0':
+    resolution: {integrity: sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.8.0'
 
   '@typescript-eslint/types@7.17.0':
     resolution: {integrity: sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==}
     engines: {node: ^18.18.0 || >=20.0.0}
 
-  '@typescript-eslint/typescript-estree@7.1.0':
-    resolution: {integrity: sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==}
-    engines: {node: ^16.0.0 || >=18.0.0}
-    peerDependencies:
-      typescript: '*'
-    peerDependenciesMeta:
-      typescript:
-        optional: true
+  '@typescript-eslint/types@8.20.0':
+    resolution: {integrity: sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@typescript-eslint/typescript-estree@7.17.0':
     resolution: {integrity: sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==}
@@ -4706,11 +4926,11 @@ packages:
       typescript:
         optional: true
 
-  '@typescript-eslint/utils@7.1.0':
-    resolution: {integrity: sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==}
-    engines: {node: ^16.0.0 || >=18.0.0}
+  '@typescript-eslint/typescript-estree@8.20.0':
+    resolution: {integrity: sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      eslint: ^8.56.0
+      typescript: '>=4.8.4 <5.8.0'
 
   '@typescript-eslint/utils@7.17.0':
     resolution: {integrity: sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==}
@@ -4718,22 +4938,29 @@ packages:
     peerDependencies:
       eslint: ^8.56.0
 
-  '@typescript-eslint/visitor-keys@7.1.0':
-    resolution: {integrity: sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==}
-    engines: {node: ^16.0.0 || >=18.0.0}
+  '@typescript-eslint/utils@8.20.0':
+    resolution: {integrity: sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <5.8.0'
 
   '@typescript-eslint/visitor-keys@7.17.0':
     resolution: {integrity: sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==}
     engines: {node: ^18.18.0 || >=20.0.0}
 
+  '@typescript-eslint/visitor-keys@8.20.0':
+    resolution: {integrity: sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
   '@ungap/structured-clone@1.2.0':
     resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
 
-  '@vitejs/plugin-vue@5.2.0':
-    resolution: {integrity: sha512-7n7KdUEtx/7Yl7I/WVAMZ1bEb0eVvXF3ummWTeLcs/9gvo9pJhuLdouSXGjdZ/MKD1acf1I272+X0RMua4/R3g==}
+  '@vitejs/plugin-vue@5.2.1':
+    resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==}
     engines: {node: ^18.0.0 || >=20.0.0}
     peerDependencies:
-      vite: ^5.0.0
+      vite: ^5.0.0 || ^6.0.0
       vue: ^3.2.25
 
   '@vitest/coverage-v8@1.6.0':
@@ -4777,39 +5004,45 @@ packages:
   '@volar/language-core@2.2.0':
     resolution: {integrity: sha512-a8WG9+4OdeNDW4ywABZIM6S6UN7em8uIlM/BZ2pWQUYrVmX+m8sj/X+QadvO+Li/t/LjAqbWJQtVgxdpEWLALQ==}
 
-  '@volar/language-core@2.4.10':
-    resolution: {integrity: sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==}
+  '@volar/language-core@2.4.11':
+    resolution: {integrity: sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==}
 
   '@volar/source-map@2.2.0':
     resolution: {integrity: sha512-HQlPRlHOVqCCHK8wI76ZldHkEwKsjp7E6idUc36Ekni+KJDNrqgSqPvyHQixybXPHNU7CI9Uxd9/IkxO7LuNBw==}
 
-  '@volar/source-map@2.4.10':
-    resolution: {integrity: sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==}
+  '@volar/source-map@2.4.11':
+    resolution: {integrity: sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==}
 
   '@volar/typescript@2.2.0':
     resolution: {integrity: sha512-wC6l4zLiiCLxF+FGaHCbWlQYf4vMsnRxYhcI6WgvaNppOD6r1g+Ef1RKRJUApALWU46Yy/JDU/TbdV6w/X6Liw==}
 
-  '@volar/typescript@2.4.10':
-    resolution: {integrity: sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==}
-
-  '@vue/compiler-core@3.5.11':
-    resolution: {integrity: sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==}
+  '@volar/typescript@2.4.11':
+    resolution: {integrity: sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==}
 
   '@vue/compiler-core@3.5.12':
     resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==}
 
-  '@vue/compiler-dom@3.5.11':
-    resolution: {integrity: sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==}
+  '@vue/compiler-core@3.5.13':
+    resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
 
   '@vue/compiler-dom@3.5.12':
     resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==}
 
+  '@vue/compiler-dom@3.5.13':
+    resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
+
   '@vue/compiler-sfc@3.5.12':
     resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==}
 
+  '@vue/compiler-sfc@3.5.13':
+    resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
+
   '@vue/compiler-ssr@3.5.12':
     resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==}
 
+  '@vue/compiler-ssr@3.5.13':
+    resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
+
   '@vue/compiler-vue2@2.7.16':
     resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
 
@@ -4821,8 +5054,8 @@ packages:
       typescript:
         optional: true
 
-  '@vue/language-core@2.1.10':
-    resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==}
+  '@vue/language-core@2.2.0':
+    resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==}
     peerDependencies:
       typescript: '*'
     peerDependenciesMeta:
@@ -4832,23 +5065,37 @@ packages:
   '@vue/reactivity@3.5.12':
     resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==}
 
+  '@vue/reactivity@3.5.13':
+    resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
+
   '@vue/runtime-core@3.5.12':
     resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==}
 
+  '@vue/runtime-core@3.5.13':
+    resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==}
+
   '@vue/runtime-dom@3.5.12':
     resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==}
 
+  '@vue/runtime-dom@3.5.13':
+    resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==}
+
   '@vue/server-renderer@3.5.12':
     resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==}
     peerDependencies:
       vue: 3.5.12
 
-  '@vue/shared@3.5.11':
-    resolution: {integrity: sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==}
+  '@vue/server-renderer@3.5.13':
+    resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==}
+    peerDependencies:
+      vue: 3.5.13
 
   '@vue/shared@3.5.12':
     resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==}
 
+  '@vue/shared@3.5.13':
+    resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
+
   '@vue/test-utils@2.4.1':
     resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==}
     peerDependencies:
@@ -4919,6 +5166,10 @@ packages:
     resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
     engines: {node: '>= 14'}
 
+  agent-base@7.1.3:
+    resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
+    engines: {node: '>= 14'}
+
   aggregate-error@3.1.0:
     resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
     engines: {node: '>=8'}
@@ -4960,8 +5211,8 @@ packages:
   ajv@8.17.1:
     resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
 
-  alien-signals@0.2.1:
-    resolution: {integrity: sha512-FlEQrDJe9r2RI4cDlnK2zYqJezvx1uJaWEuwxsnlFqnPwvJbgitNBRumWrLDv8lA+7cCikpMxfJD2TTHiaTklA==}
+  alien-signals@0.4.14:
+    resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==}
 
   ansi-colors@4.1.3:
     resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
@@ -5167,8 +5418,8 @@ packages:
   axios@0.24.0:
     resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
 
-  axios@1.7.7:
-    resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
+  axios@1.7.9:
+    resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==}
 
   b4a@1.6.4:
     resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
@@ -5279,13 +5530,13 @@ packages:
   browser-assert@1.2.1:
     resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==}
 
-  browserslist@4.22.2:
-    resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==}
+  browserslist@4.23.0:
+    resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
 
-  browserslist@4.23.0:
-    resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
+  browserslist@4.24.4:
+    resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
 
@@ -5322,8 +5573,8 @@ packages:
     resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
     engines: {node: '>=6.14.2'}
 
-  bullmq@5.26.1:
-    resolution: {integrity: sha512-XuxCGFlC1PQ2i1JHQiB9dqkqKQILMwQpU7ipi+cT/dzJaoXVcS0/IByUz6SsZ3xyOQY3twPt6G7J2d5GrsJuEA==}
+  bullmq@5.34.10:
+    resolution: {integrity: sha512-ia6EzpQm1ZPq6GUBSLyfvzJrhdBTd1f3Gn2g9pFtLX4hBOob6QHmcmBzGgPlSCyr/i2Qfe4OdjS21bRd02srbw==}
 
   buraha@0.0.1:
     resolution: {integrity: sha512-G563A0mTbzknm2jDaNxfZuNKIdeArs8T+XQN6t+KbmgnOoevXSXhKDkyf8Md/36Jrx99ikwbCag37VGe3myExQ==}
@@ -5352,10 +5603,6 @@ packages:
     resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==}
     engines: {node: '>=14.16'}
 
-  cacheable-request@10.2.14:
-    resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==}
-    engines: {node: '>=14.16'}
-
   cacheable-request@12.0.1:
     resolution: {integrity: sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==}
     engines: {node: '>=18'}
@@ -5397,12 +5644,12 @@ packages:
   caniuse-api@3.0.0:
     resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
 
-  caniuse-lite@1.0.30001566:
-    resolution: {integrity: sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==}
-
   caniuse-lite@1.0.30001591:
     resolution: {integrity: sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==}
 
+  caniuse-lite@1.0.30001695:
+    resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==}
+
   canonicalize@1.0.8:
     resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==}
 
@@ -5443,8 +5690,8 @@ packages:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
 
-  chalk@5.3.0:
-    resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
+  chalk@5.4.1:
+    resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
     engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
 
   char-regex@1.0.2:
@@ -5463,8 +5710,8 @@ packages:
   character-parser@2.2.0:
     resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==}
 
-  chart.js@4.4.6:
-    resolution: {integrity: sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==}
+  chart.js@4.4.7:
+    resolution: {integrity: sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==}
     engines: {pnpm: '>=8'}
 
   chartjs-adapter-date-fns@3.0.0:
@@ -5483,8 +5730,8 @@ packages:
     peerDependencies:
       chart.js: '>=2.6.0'
 
-  chartjs-plugin-zoom@2.0.1:
-    resolution: {integrity: sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==}
+  chartjs-plugin-zoom@2.2.0:
+    resolution: {integrity: sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==}
     peerDependencies:
       chart.js: '>=3.2.0'
 
@@ -5506,10 +5753,6 @@ packages:
     resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==}
     engines: {node: '>=18.17'}
 
-  cheerio@1.0.0-rc.12:
-    resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
-    engines: {node: '>= 6'}
-
   chokidar@3.5.3:
     resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
     engines: {node: '>= 8.10.0'}
@@ -5521,8 +5764,8 @@ packages:
     resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
     engines: {node: '>=10'}
 
-  chromatic@11.18.1:
-    resolution: {integrity: sha512-hkNT9vA6K9+PnE/khhZYBnRCOm8NonaQDs7RZ8YHFo7/lh1b/x/uFMkTjWjaj/mkM6QOR/evu5VcZMtcaauSlw==}
+  chromatic@11.25.0:
+    resolution: {integrity: sha512-P2BVe0rRLS9WM+eSG3u1SRg0Mi2vopsdPs2FiXwUiPqZ6hs9fe66d3Pnt7CfQ22v2jThuPEXYjYEeuL75a16Bw==}
     hasBin: true
     peerDependencies:
       '@chromatic-com/cypress': ^0.*.* || ^1.0.0
@@ -5689,8 +5932,9 @@ packages:
   config-chain@1.1.13:
     resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
 
-  consola@2.15.3:
-    resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
+  consola@3.4.0:
+    resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==}
+    engines: {node: ^14.18.0 || >=16.10.0}
 
   console-control-strings@1.1.0:
     resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
@@ -5755,8 +5999,8 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
 
-  cron-parser@4.8.1:
-    resolution: {integrity: sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==}
+  cron-parser@4.9.0:
+    resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
     engines: {node: '>=12.0.0'}
 
   cropperjs@2.0.0-rc.2:
@@ -5770,8 +6014,8 @@ packages:
   cross-fetch@3.1.6:
     resolution: {integrity: sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==}
 
-  cross-fetch@4.0.0:
-    resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
+  cross-fetch@4.1.0:
+    resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
 
   cross-spawn@5.1.0:
     resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
@@ -5780,6 +6024,10 @@ packages:
     resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
     engines: {node: '>= 8'}
 
+  cross-spawn@7.0.6:
+    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+    engines: {node: '>= 8'}
+
   css-declaration-sorter@7.2.0:
     resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==}
     engines: {node: ^14 || ^16 || >=18}
@@ -5809,21 +6057,21 @@ packages:
     engines: {node: '>=4'}
     hasBin: true
 
-  cssnano-preset-default@6.1.2:
-    resolution: {integrity: sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  cssnano-preset-default@7.0.6:
+    resolution: {integrity: sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  cssnano-utils@4.0.2:
-    resolution: {integrity: sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  cssnano-utils@5.0.0:
+    resolution: {integrity: sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  cssnano@6.1.2:
-    resolution: {integrity: sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  cssnano@7.0.6:
+    resolution: {integrity: sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
@@ -5831,16 +6079,16 @@ packages:
     resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
     engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
 
-  cssstyle@4.0.1:
-    resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==}
+  cssstyle@4.2.1:
+    resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==}
     engines: {node: '>=18'}
 
   csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
-  cypress@13.15.2:
-    resolution: {integrity: sha512-ARbnUorjcCM3XiPwgHKuqsyr5W9Qn+pIIBPaoilnoBkLdSC2oLQjV1BUpnmc7KR+b7Avah3Ly2RMFnfxr96E/A==}
-    engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
+  cypress@14.0.0:
+    resolution: {integrity: sha512-kEGqQr23so5IpKeg/dp6GVi7RlHx1NmW66o2a2Q4wk9gRaAblLZQSiZJuDI8UMC4LlG5OJ7Q6joAiqTrfRNbTw==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
 
   dashdash@1.14.1:
@@ -5920,6 +6168,15 @@ packages:
       supports-color:
         optional: true
 
+  debug@4.4.0:
+    resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
   decamelize-keys@1.1.1:
     resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
     engines: {node: '>=0.10.0'}
@@ -6018,6 +6275,11 @@ packages:
     resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
     engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
 
+  detect-libc@1.0.3:
+    resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
+    engines: {node: '>=0.10'}
+    hasBin: true
+
   detect-libc@2.0.3:
     resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
     engines: {node: '>=8'}
@@ -6130,16 +6392,19 @@ packages:
     engines: {node: '>=0.10.0'}
     hasBin: true
 
-  electron-to-chromium@1.4.601:
-    resolution: {integrity: sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==}
-
   electron-to-chromium@1.4.686:
     resolution: {integrity: sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==}
 
+  electron-to-chromium@1.5.83:
+    resolution: {integrity: sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==}
+
   emittery@0.13.1:
     resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
     engines: {node: '>=12'}
 
+  emoji-regex-xs@1.0.0:
+    resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
+
   emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
 
@@ -6239,25 +6504,34 @@ packages:
     peerDependencies:
       esbuild: '>=0.12 <1'
 
-  esbuild@0.18.20:
-    resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
-    engines: {node: '>=12'}
-    hasBin: true
-
   esbuild@0.21.5:
     resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
     engines: {node: '>=12'}
     hasBin: true
 
+  esbuild@0.23.1:
+    resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
+    engines: {node: '>=18'}
+    hasBin: true
+
   esbuild@0.24.0:
     resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==}
     engines: {node: '>=18'}
     hasBin: true
 
+  esbuild@0.24.2:
+    resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
+    engines: {node: '>=18'}
+    hasBin: true
+
   escalade@3.1.1:
     resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
     engines: {node: '>=6'}
 
+  escalade@3.2.0:
+    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+    engines: {node: '>=6'}
+
   escape-goat@3.0.0:
     resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==}
     engines: {node: '>=10'}
@@ -6291,27 +6565,6 @@ packages:
   eslint-import-resolver-node@0.3.9:
     resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
 
-  eslint-module-utils@2.11.0:
-    resolution: {integrity: sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==}
-    engines: {node: '>=4'}
-    peerDependencies:
-      '@typescript-eslint/parser': '*'
-      eslint: '*'
-      eslint-import-resolver-node: '*'
-      eslint-import-resolver-typescript: '*'
-      eslint-import-resolver-webpack: '*'
-    peerDependenciesMeta:
-      '@typescript-eslint/parser':
-        optional: true
-      eslint:
-        optional: true
-      eslint-import-resolver-node:
-        optional: true
-      eslint-import-resolver-typescript:
-        optional: true
-      eslint-import-resolver-webpack:
-        optional: true
-
   eslint-module-utils@2.12.0:
     resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==}
     engines: {node: '>=4'}
@@ -6333,16 +6586,6 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
 
-  eslint-plugin-import@2.30.0:
-    resolution: {integrity: sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==}
-    engines: {node: '>=4'}
-    peerDependencies:
-      '@typescript-eslint/parser': '*'
-      eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
-    peerDependenciesMeta:
-      '@typescript-eslint/parser':
-        optional: true
-
   eslint-plugin-import@2.31.0:
     resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
     engines: {node: '>=4'}
@@ -6359,6 +6602,12 @@ packages:
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
 
+  eslint-plugin-vue@9.32.0:
+    resolution: {integrity: sha512-b/Y05HYmnB/32wqVcjxjHZzNpwxj1onBOvqW89W+V+XNG1dRuaFbNd3vT9CLbr2LXjEoq+3vn8DanWf7XU22Ug==}
+    engines: {node: ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
+
   eslint-rule-docs@1.1.235:
     resolution: {integrity: sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==}
 
@@ -6378,8 +6627,8 @@ packages:
     resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  eslint@9.14.0:
-    resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==}
+  eslint@9.18.0:
+    resolution: {integrity: sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     hasBin: true
     peerDependencies:
@@ -6482,10 +6731,6 @@ packages:
   exponential-backoff@3.1.1:
     resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
 
-  express@4.21.0:
-    resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==}
-    engines: {node: '>= 0.10.0'}
-
   express@4.21.1:
     resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==}
     engines: {node: '>= 0.10.0'}
@@ -6522,8 +6767,8 @@ packages:
   fast-fifo@1.3.0:
     resolution: {integrity: sha512-IgfweLvEpwyA4WgiQe9Nx6VV2QkML2NkvZnk1oKnIzXgXdWxuhF7zw4DvLTPZJn6PIUneiAXPF24QmoEqHTjyw==}
 
-  fast-glob@3.3.2:
-    resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+  fast-glob@3.3.3:
+    resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
     engines: {node: '>=8.6.0'}
 
   fast-json-stable-stringify@2.1.0:
@@ -6569,8 +6814,8 @@ packages:
     resolution: {integrity: sha512-2qfoaQ3BQDhZ1gtbkKZd6n0kKxJISJGM6u/skD9ljdWItAscjXrtZ1lnjr7PavmXX9j4EyCPmBDiIsLn07d5vA==}
     engines: {node: '>= 10'}
 
-  fastify@5.0.0:
-    resolution: {integrity: sha512-Qe4dU+zGOzg7vXjw4EvcuyIbNnMwTmcuOhlOrOJsgwzvjEZmsM/IeHulgJk+r46STjdJS/ZJbxO8N70ODXDMEQ==}
+  fastify@5.2.1:
+    resolution: {integrity: sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==}
 
   fastq@1.17.1:
     resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
@@ -6690,10 +6935,6 @@ packages:
   forever-agent@0.6.1:
     resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
 
-  form-data-encoder@2.1.4:
-    resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==}
-    engines: {node: '>= 14.17'}
-
   form-data-encoder@4.0.2:
     resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==}
     engines: {node: '>= 18'}
@@ -6706,6 +6947,9 @@ packages:
     resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
     engines: {node: '>=12.20.0'}
 
+  forwarded-parse@2.1.2:
+    resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
+
   forwarded@0.2.0:
     resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
     engines: {node: '>= 0.6'}
@@ -6817,8 +7061,8 @@ packages:
     resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
     engines: {node: '>= 0.4'}
 
-  get-tsconfig@4.7.2:
-    resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==}
+  get-tsconfig@4.9.0:
+    resolution: {integrity: sha512-52n24W52sIueosRe0XZ8Ex5Yle+WbhfCKnV/gWXpbVR8FXNTfqdKEKUSypKso66VRHTvvcQxL44UTZbJRlCTnw==}
 
   getos@3.2.1:
     resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==}
@@ -6834,19 +7078,13 @@ packages:
     resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
     engines: {node: '>=10.13.0'}
 
-  glob-promise@4.2.2:
-    resolution: {integrity: sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==}
-    engines: {node: '>=12'}
-    peerDependencies:
-      glob: ^7.1.6
-
   glob@10.3.10:
     resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
     engines: {node: '>=16 || 14 >=14.17'}
     hasBin: true
 
-  glob@11.0.0:
-    resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
+  glob@11.0.1:
+    resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==}
     engines: {node: 20 || >=22}
     hasBin: true
 
@@ -6875,8 +7113,8 @@ packages:
     resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
     engines: {node: '>=18'}
 
-  globals@15.12.0:
-    resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==}
+  globals@15.14.0:
+    resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==}
     engines: {node: '>=18'}
 
   globalthis@1.0.3:
@@ -6897,12 +7135,8 @@ packages:
     resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==}
     engines: {node: '>=10.19.0'}
 
-  got@12.6.1:
-    resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==}
-    engines: {node: '>=14.16'}
-
-  got@14.4.4:
-    resolution: {integrity: sha512-tqiF7eSgTBwQkxb1LxsEpva8TaMYVisbhplrFVmw9GQE3855Z+MH/mnsXLLOkDxR6hZJRFMj5VTAZ8lmTF8ZOA==}
+  got@14.4.5:
+    resolution: {integrity: sha512-sq+uET8TnNKRNnjEOPJzMcxeI0irT8BBNmf+GtZcJpmhYsQM1DSKmCROUjPWKsXZ5HzwD5Cf5/RV+QD9BSTxJg==}
     engines: {node: '>=20'}
 
   graceful-fs@4.2.11:
@@ -6922,11 +7156,8 @@ packages:
     resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==}
     engines: {node: '>=0.8.0'}
 
-  happy-dom@10.0.3:
-    resolution: {integrity: sha512-WkCP+Z5fX6U5PY+yHP3ElV5D9PoxRAHRWPFq3pG9rg/6Hjf5ak7dozAgSCywsTRUq2qfa8vV8OQvUy5pRXy8EQ==}
-
-  happy-dom@15.11.4:
-    resolution: {integrity: sha512-AU6tzh3ADd28vSmXahgLsGyGGihXPGeKH0owDn9lhHolB6vIwEhag//T+TBzDoAcHhmVEwlxwSgtW1KZep+1MA==}
+  happy-dom@16.6.0:
+    resolution: {integrity: sha512-Zz5S9sog8a3p8XYZbO+eI1QMOAvCNnIoyrH8A8MLX+X2mJrzADTy+kdETmc4q+uD9AGAvQYGn96qBAn2RAciKw==}
     engines: {node: '>=18.0.0'}
 
   hard-rejection@2.1.0:
@@ -6988,8 +7219,8 @@ packages:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
 
-  hast-util-to-html@9.0.3:
-    resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==}
+  hast-util-to-html@9.0.4:
+    resolution: {integrity: sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==}
 
   hast-util-whitespace@3.0.0:
     resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
@@ -7023,8 +7254,8 @@ packages:
     resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
     engines: {node: '>=18'}
 
-  html-entities@2.3.2:
-    resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==}
+  html-entities@2.5.2:
+    resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==}
 
   html-escaper@2.0.2:
     resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
@@ -7092,6 +7323,10 @@ packages:
     resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
     engines: {node: '>= 14'}
 
+  https-proxy-agent@7.0.6:
+    resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+    engines: {node: '>= 14'}
+
   human-signals@1.1.1:
     resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==}
     engines: {node: '>=8.12.0'}
@@ -7137,8 +7372,8 @@ packages:
     resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
     engines: {node: '>= 4'}
 
-  immutable@4.2.2:
-    resolution: {integrity: sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==}
+  immutable@5.0.3:
+    resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==}
 
   import-fresh@3.3.0:
     resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
@@ -7200,8 +7435,8 @@ packages:
   intersection-observer@0.12.2:
     resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
 
-  ioredis@5.4.1:
-    resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==}
+  ioredis@5.4.2:
+    resolution: {integrity: sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==}
     engines: {node: '>=12.22.0'}
 
   ip-address@9.0.5:
@@ -7212,9 +7447,9 @@ packages:
     resolution: {integrity: sha512-KifhLKBjdS/hB3TD4UUOalVp1BpzPFvRpgJvXcP0Ya98tuSQTUQ71iI7EW7CKddkBJTYB3GfTWl5eJwpLOXj2A==}
     engines: {node: '>=16.14.0'}
 
-  ip-regex@4.3.0:
-    resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
-    engines: {node: '>=8'}
+  ip-regex@5.0.0:
+    resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
 
   ip@2.0.1:
     resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==}
@@ -7313,10 +7548,6 @@ packages:
     resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
     engines: {node: '>=10'}
 
-  is-ip@3.1.0:
-    resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==}
-    engines: {node: '>=8'}
-
   is-lambda@1.0.1:
     resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==}
 
@@ -7670,19 +7901,19 @@ packages:
   jsbn@1.1.0:
     resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
 
-  jschardet@3.0.0:
-    resolution: {integrity: sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==}
+  jschardet@3.1.4:
+    resolution: {integrity: sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==}
     engines: {node: '>=0.1.90'}
 
   jsdoc-type-pratt-parser@4.1.0:
     resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
     engines: {node: '>=12.0.0'}
 
-  jsdom@24.1.1:
-    resolution: {integrity: sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==}
+  jsdom@26.0.0:
+    resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==}
     engines: {node: '>=18'}
     peerDependencies:
-      canvas: ^2.11.2
+      canvas: ^3.0.0
     peerDependenciesMeta:
       canvas:
         optional: true
@@ -7741,8 +7972,8 @@ packages:
   jsonfile@6.1.0:
     resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
 
-  jsonld@8.3.2:
-    resolution: {integrity: sha512-MwBbq95szLwt8eVQ1Bcfwmgju/Y5P2GdtlHE2ncyfuYjIdEhluUVyj1eudacf1mOkWIoS9GpDBTECqhmq7EOaA==}
+  jsonld@8.3.3:
+    resolution: {integrity: sha512-9YcilrF+dLfg9NTEof/mJLMtbdX1RJ8dbWtJgE00cMOIohb1lIyJl710vFiTaiHTl6ZYODJuBd32xFvUhmv3kg==}
     engines: {node: '>=14'}
 
   jsonpointer@5.0.1:
@@ -7821,8 +8052,8 @@ packages:
   light-my-request@6.0.0:
     resolution: {integrity: sha512-kFkFXrmKCL0EEeOmJybMH5amWFd+AFvlvMlvFTRxCUwbhfapZqDmeLMPoWihntnYY6JpoQDE9k+vOzObF1fDqg==}
 
-  lilconfig@3.1.1:
-    resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==}
+  lilconfig@3.1.3:
+    resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
     engines: {node: '>=14'}
 
   lines-and-columns@1.2.4:
@@ -7905,14 +8136,13 @@ packages:
     resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
 
-  lru-cache@10.0.2:
-    resolution: {integrity: sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==}
-    engines: {node: 14 || >=16.14}
-
   lru-cache@10.2.2:
     resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
     engines: {node: 14 || >=16.14}
 
+  lru-cache@10.4.3:
+    resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
   lru-cache@11.0.0:
     resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==}
     engines: {node: 20 || >=22}
@@ -8036,8 +8266,8 @@ packages:
     resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
     engines: {node: '>= 0.6'}
 
-  meilisearch@0.45.0:
-    resolution: {integrity: sha512-+zCzEqE+CumY4icB0Vox180adZqaNtnr60hJWGiEdmol5eWmksfY8rYsTcz87styXC2ZOg+2yF56gdH6oyIBTA==}
+  meilisearch@0.48.2:
+    resolution: {integrity: sha512-auDB6grs3f/+dVzzPiAOOFnrVN/L87/+SWZfy56TpimDinWjU4u6Jc2o6lgeLluotpbMOFY1WpTTbqtUdMduQw==}
 
   memoizerific@1.11.3:
     resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
@@ -8339,8 +8569,8 @@ packages:
     peerDependencies:
       msw: ^2.0.0
 
-  msw@2.6.4:
-    resolution: {integrity: sha512-Pm4LmWQeytDsNCR+A7gt39XAdtH6zQb6jnIKRig0FlvYOn8eksn3s1nXxUfz5KYUjbckof7Z4p2ewzgffPoCbg==}
+  msw@2.7.0:
+    resolution: {integrity: sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==}
     engines: {node: '>=18'}
     hasBin: true
     peerDependencies:
@@ -8370,13 +8600,13 @@ packages:
   nan@2.20.0:
     resolution: {integrity: sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==}
 
-  nanoid@3.3.7:
-    resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
+  nanoid@3.3.8:
+    resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
-  nanoid@5.0.8:
-    resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==}
+  nanoid@5.0.9:
+    resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==}
     engines: {node: ^18 || >=20}
     hasBin: true
 
@@ -8420,6 +8650,9 @@ packages:
   node-addon-api@3.2.1:
     resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==}
 
+  node-addon-api@7.1.1:
+    resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
   node-domexception@1.0.0:
     resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
     engines: {node: '>=10.5.0'}
@@ -8465,6 +8698,9 @@ packages:
   node-releases@2.0.14:
     resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
 
+  node-releases@2.0.19:
+    resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+
   nodemailer@6.9.16:
     resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==}
     engines: {node: '>=6.0.0'}
@@ -8474,6 +8710,11 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
+  nodemon@3.1.9:
+    resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==}
+    engines: {node: '>=10'}
+    hasBin: true
+
   nofilter@3.1.0:
     resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==}
     engines: {node: '>=12.19'}
@@ -8541,8 +8782,8 @@ packages:
   nth-check@2.1.1:
     resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
 
-  nwsapi@2.2.12:
-    resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==}
+  nwsapi@2.2.16:
+    resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
 
   oauth2orize-pkce@0.1.2:
     resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==}
@@ -8621,8 +8862,8 @@ packages:
     resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
     engines: {node: '>=12'}
 
-  oniguruma-to-js@0.4.3:
-    resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==}
+  oniguruma-to-es@2.2.0:
+    resolution: {integrity: sha512-EEsso27ri0sf+t4uRFEj5C5gvXQj0d0w1Y2qq06b+hDLBnvzO1rWTwEW4C7ytan6nhg4WPwE26eLoiPhHUbvKg==}
 
   open@8.4.2:
     resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
@@ -8631,8 +8872,8 @@ packages:
   openapi-types@12.1.3:
     resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
 
-  openapi-typescript@6.7.3:
-    resolution: {integrity: sha512-es3mGcDXV6TKPo6n3aohzHm0qxhLyR39MhF6mkD1FwFGjhxnqMqfSIgM0eCpInZvqatve4CxmXcMZw3jnnsaXw==}
+  openapi-typescript@6.7.6:
+    resolution: {integrity: sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==}
     hasBin: true
 
   optionator@0.9.4:
@@ -8649,8 +8890,8 @@ packages:
   ospath@1.2.2:
     resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
 
-  otpauth@9.3.4:
-    resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
+  otpauth@9.3.6:
+    resolution: {integrity: sha512-eIcCvuEvcAAPHxUKC9Q4uCe0Fh/yRc5jv9z+f/kvyIF2LPrhgAOuLB7J9CssGYhND/BL8M9hlHBTFmffpoQlMQ==}
 
   outvariant@1.4.3:
     resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
@@ -8659,10 +8900,6 @@ packages:
     resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
     engines: {node: '>=8'}
 
-  p-cancelable@3.0.0:
-    resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
-    engines: {node: '>=12.20'}
-
   p-cancelable@4.0.1:
     resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==}
     engines: {node: '>=14.16'}
@@ -8783,12 +9020,13 @@ packages:
   path-to-regexp@1.8.0:
     resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==}
 
-  path-to-regexp@3.3.0:
-    resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==}
-
   path-to-regexp@6.3.0:
     resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
 
+  path-to-regexp@8.2.0:
+    resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
+    engines: {node: '>=16'}
+
   path-type@4.0.0:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
@@ -8875,9 +9113,6 @@ packages:
   picocolors@1.0.1:
     resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
 
-  picocolors@1.1.0:
-    resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==}
-
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
@@ -8944,153 +9179,153 @@ packages:
     resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
     engines: {node: '>= 0.4'}
 
-  postcss-calc@9.0.1:
-    resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-calc@10.1.0:
+    resolution: {integrity: sha512-uQ/LDGsf3mgsSUEXmAt3VsCSHR3aKqtEIkmB+4PhzYwRYOW5MZs/GhCCFpsOtJJkP6EC6uGipbrnaTjqaJZcJw==}
+    engines: {node: ^18.12 || ^20.9 || >=22.0}
     peerDependencies:
-      postcss: ^8.2.2
+      postcss: ^8.4.38
 
-  postcss-colormin@6.1.0:
-    resolution: {integrity: sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-colormin@7.0.2:
+    resolution: {integrity: sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-convert-values@6.1.0:
-    resolution: {integrity: sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-convert-values@7.0.4:
+    resolution: {integrity: sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-discard-comments@6.0.2:
-    resolution: {integrity: sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-discard-comments@7.0.3:
+    resolution: {integrity: sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-discard-duplicates@6.0.3:
-    resolution: {integrity: sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-discard-duplicates@7.0.1:
+    resolution: {integrity: sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-discard-empty@6.0.3:
-    resolution: {integrity: sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-discard-empty@7.0.0:
+    resolution: {integrity: sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-discard-overridden@6.0.2:
-    resolution: {integrity: sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-discard-overridden@7.0.0:
+    resolution: {integrity: sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-merge-longhand@6.0.5:
-    resolution: {integrity: sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-merge-longhand@7.0.4:
+    resolution: {integrity: sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-merge-rules@6.1.1:
-    resolution: {integrity: sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-merge-rules@7.0.4:
+    resolution: {integrity: sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-minify-font-values@6.1.0:
-    resolution: {integrity: sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-minify-font-values@7.0.0:
+    resolution: {integrity: sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-minify-gradients@6.0.3:
-    resolution: {integrity: sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-minify-gradients@7.0.0:
+    resolution: {integrity: sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-minify-params@6.1.0:
-    resolution: {integrity: sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-minify-params@7.0.2:
+    resolution: {integrity: sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-minify-selectors@6.0.4:
-    resolution: {integrity: sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-minify-selectors@7.0.4:
+    resolution: {integrity: sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-charset@6.0.2:
-    resolution: {integrity: sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-charset@7.0.0:
+    resolution: {integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-display-values@6.0.2:
-    resolution: {integrity: sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-display-values@7.0.0:
+    resolution: {integrity: sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-positions@6.0.2:
-    resolution: {integrity: sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-positions@7.0.0:
+    resolution: {integrity: sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-repeat-style@6.0.2:
-    resolution: {integrity: sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-repeat-style@7.0.0:
+    resolution: {integrity: sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-string@6.0.2:
-    resolution: {integrity: sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-string@7.0.0:
+    resolution: {integrity: sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-timing-functions@6.0.2:
-    resolution: {integrity: sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-timing-functions@7.0.0:
+    resolution: {integrity: sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-unicode@6.1.0:
-    resolution: {integrity: sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-unicode@7.0.2:
+    resolution: {integrity: sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-url@6.0.2:
-    resolution: {integrity: sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-url@7.0.0:
+    resolution: {integrity: sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-normalize-whitespace@6.0.2:
-    resolution: {integrity: sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-normalize-whitespace@7.0.0:
+    resolution: {integrity: sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-ordered-values@6.0.2:
-    resolution: {integrity: sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-ordered-values@7.0.1:
+    resolution: {integrity: sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-reduce-initial@6.1.0:
-    resolution: {integrity: sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-reduce-initial@7.0.2:
+    resolution: {integrity: sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-reduce-transforms@6.0.2:
-    resolution: {integrity: sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-reduce-transforms@7.0.0:
+    resolution: {integrity: sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
@@ -9098,27 +9333,31 @@ packages:
     resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==}
     engines: {node: '>=4'}
 
-  postcss-svgo@6.0.3:
-    resolution: {integrity: sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==}
-    engines: {node: ^14 || ^16 || >= 18}
+  postcss-selector-parser@6.1.2:
+    resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
+    engines: {node: '>=4'}
+
+  postcss-selector-parser@7.0.0:
+    resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==}
+    engines: {node: '>=4'}
+
+  postcss-svgo@7.0.1:
+    resolution: {integrity: sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >= 18}
     peerDependencies:
       postcss: ^8.4.31
 
-  postcss-unique-selectors@6.0.4:
-    resolution: {integrity: sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  postcss-unique-selectors@7.0.3:
+    resolution: {integrity: sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
   postcss-value-parser@4.2.0:
     resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
 
-  postcss@8.4.47:
-    resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
-    engines: {node: ^10 || ^12 || >=14}
-
-  postcss@8.4.49:
-    resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
+  postcss@8.5.1:
+    resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
     engines: {node: ^10 || ^12 || >=14}
 
   postgres-array@2.0.0:
@@ -9160,8 +9399,8 @@ packages:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
     engines: {node: '>= 0.8.0'}
 
-  prettier@3.3.3:
-    resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
+  prettier@3.4.2:
+    resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==}
     engines: {node: '>=14'}
     hasBin: true
 
@@ -9177,8 +9416,9 @@ packages:
     resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
-  private-ip@2.3.3:
-    resolution: {integrity: sha512-5zyFfekIVUOTVbL92hc8LJOtE/gyGHeREHkJ2yTyByP8Q2YZVoBqLg3EfYLeF0oVvGqtaEX2t2Qovja0/gStXw==}
+  private-ip@3.0.2:
+    resolution: {integrity: sha512-2pkOVPGYD/4QyAg95c6E/4bLYXPthT5Xw4ocXYzIIsMBhskOMn6IwkWXmg6ZiA6K58+O6VD/n02r1hDhk7vDPw==}
+    engines: {node: '>=14.16'}
 
   probe-image-size@7.2.3:
     resolution: {integrity: sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==}
@@ -9470,8 +9710,14 @@ packages:
   regenerator-runtime@0.14.0:
     resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
 
-  regex@4.3.3:
-    resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==}
+  regex-recursion@5.1.1:
+    resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==}
+
+  regex-utilities@2.3.0:
+    resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+  regex@5.1.1:
+    resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==}
 
   regexp.prototype.flags@1.5.0:
     resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
@@ -9576,16 +9822,13 @@ packages:
     deprecated: Rimraf versions prior to v4 are no longer supported
     hasBin: true
 
-  rollup@4.26.0:
-    resolution: {integrity: sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==}
+  rollup@4.31.0:
+    resolution: {integrity: sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
-  rrweb-cssom@0.6.0:
-    resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
-
-  rrweb-cssom@0.7.1:
-    resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==}
+  rrweb-cssom@0.8.0:
+    resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
 
   rss-parser@3.13.0:
     resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
@@ -9627,16 +9870,11 @@ packages:
   safer-buffer@2.1.2:
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
 
-  sanitize-html@2.13.1:
-    resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==}
-
-  sass@1.79.3:
-    resolution: {integrity: sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==}
-    engines: {node: '>=14.0.0'}
-    hasBin: true
+  sanitize-html@2.14.0:
+    resolution: {integrity: sha512-CafX+IUPxZshXqqRaG9ZClSlfPVjSxI0td7n07hk8QO2oO+9JDnlcL8iM8TWeOXOIBFgIOx6zioTzM53AOMn3g==}
 
-  sass@1.79.4:
-    resolution: {integrity: sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==}
+  sass@1.83.4:
+    resolution: {integrity: sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==}
     engines: {node: '>=14.0.0'}
     hasBin: true
 
@@ -9653,8 +9891,8 @@ packages:
   secure-json-parse@2.7.0:
     resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
 
-  secure-json-parse@3.0.0:
-    resolution: {integrity: sha512-YO+gVWyp97H+nCG/qdC8X819iKx5g+BpnO9nYT4uFq4uyI0rSxwtx5qD9rGfScg7FGLYu/YBf8uOtwQKv+gq8g==}
+  secure-json-parse@3.0.2:
+    resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==}
 
   seedrandom@3.0.5:
     resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
@@ -9742,8 +9980,8 @@ packages:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
 
-  shiki@1.22.2:
-    resolution: {integrity: sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==}
+  shiki@1.27.2:
+    resolution: {integrity: sha512-QtA1C41oEVixKog+V8I3ia7jjGls7oCZ8Yul8vdHrVBga5uPoyTtMvFF4lMMXIyAZo5A5QbXq91bot2vA6Q+eQ==}
 
   shimmer@1.2.1:
     resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==}
@@ -9977,8 +10215,8 @@ packages:
   standard-as-callback@2.1.0:
     resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
 
-  start-server-and-test@2.0.8:
-    resolution: {integrity: sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==}
+  start-server-and-test@2.0.10:
+    resolution: {integrity: sha512-nZphcfcqGqwk74lbZkqSwClkYz+M5ZPGOMgWxNVJrdztPKN96qe6HooRu6L3TpwITn0lKJJdKACqHbJtqythOQ==}
     engines: {node: '>=16'}
     hasBin: true
 
@@ -10012,8 +10250,8 @@ packages:
       react-dom:
         optional: true
 
-  storybook@8.4.4:
-    resolution: {integrity: sha512-xBOq3q/MuUUg3zM0imMMaK5ziKq3TO388jsnaiemJ4Uf0ZGwcHjM8HDBCDt0s5/CfsOQ49zo1ouZ3aNlu0qsUg==}
+  storybook@8.5.0:
+    resolution: {integrity: sha512-cEx42OlCetManF+cONVJVYP7SYsnI2K922DfWKmZhebP0it0n6TUof4y5/XzJ8YUruwPgyclGLdX8TvdRuNSfw==}
     hasBin: true
     peerDependencies:
       prettier: ^2 || ^3
@@ -10150,9 +10388,9 @@ packages:
     resolution: {integrity: sha512-ERPW+XkvX9W2A+ov07iy+ZFJpVdik04GhDA4eVogiG9hpC97Kem2iucyzhFxbFRvQ5o2UckFtKZdp1hkGvnrEw==}
     engines: {node: '>=16'}
 
-  stylehacks@6.1.1:
-    resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==}
-    engines: {node: ^14 || ^16 || >=18.0}
+  stylehacks@7.0.4:
+    resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==}
+    engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
     peerDependencies:
       postcss: ^8.4.31
 
@@ -10180,16 +10418,16 @@ packages:
     resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
     engines: {node: '>= 0.4'}
 
-  svgo@3.2.0:
-    resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==}
+  svgo@3.3.2:
+    resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
     engines: {node: '>=14.0.0'}
     hasBin: true
 
   symbol-tree@3.2.4:
     resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
 
-  systeminformation@5.23.5:
-    resolution: {integrity: sha512-PEpJwhRYxZgBCAlWZhWIgfMTjXLqfcaZ1pJsJn9snWNfBW/Z1YQg1mbIUSWrEV3ErAHF7l/OoVLQeaZDlPzkpA==}
+  systeminformation@5.25.11:
+    resolution: {integrity: sha512-jI01fn/t47rrLTQB0FTlMCC+5dYx8o0RRF+R4BPiUNsvg5OdY0s9DKMFmJGrx5SwMZQ4cag0Gl6v8oycso9b/g==}
     engines: {node: '>=8.0.0'}
     os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
     hasBin: true
@@ -10209,8 +10447,8 @@ packages:
     resolution: {integrity: sha512-+HRtZ40Vc+6YfCDWCeAsixwxJgMbPY4HHuTgzPYH3JXvqHWUlsCfy+ylXlAKhFNcuLp4xVeWeFBUhDk+7KYUvQ==}
     engines: {node: '>=14.16'}
 
-  terser@5.36.0:
-    resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==}
+  terser@5.37.0:
+    resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==}
     engines: {node: '>=10'}
     hasBin: true
 
@@ -10218,9 +10456,6 @@ packages:
     resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
     engines: {node: '>=8'}
 
-  text-table@0.2.0:
-    resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
-
   textarea-caret@3.1.0:
     resolution: {integrity: sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==}
 
@@ -10234,8 +10469,8 @@ packages:
   thread-stream@3.1.0:
     resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
 
-  three@0.169.0:
-    resolution: {integrity: sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==}
+  three@0.172.0:
+    resolution: {integrity: sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==}
 
   throttle-debounce@5.0.2:
     resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
@@ -10335,9 +10570,6 @@ packages:
     resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
     engines: {node: '>=18'}
 
-  trace-redirect@1.0.6:
-    resolution: {integrity: sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==}
-
   tree-kill@1.2.2:
     resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
     hasBin: true
@@ -10356,18 +10588,18 @@ packages:
   trough@2.2.0:
     resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
 
-  ts-api-utils@1.0.1:
-    resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==}
-    engines: {node: '>=16.13.0'}
-    peerDependencies:
-      typescript: '>=4.2.0'
-
   ts-api-utils@1.3.0:
     resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==}
     engines: {node: '>=16'}
     peerDependencies:
       typescript: '>=4.2.0'
 
+  ts-api-utils@2.0.0:
+    resolution: {integrity: sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==}
+    engines: {node: '>=18.12'}
+    peerDependencies:
+      typescript: '>=4.8.4'
+
   ts-case-convert@2.1.0:
     resolution: {integrity: sha512-Ye79el/pHYXfoew6kqhMwCoxp4NWjKNcm2kBzpmEMIU9dd9aBmHNNFtZ+WTm0rz1ngyDmfqDXDlyUnBXayiD0w==}
 
@@ -10403,8 +10635,11 @@ packages:
   tslib@2.7.0:
     resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
 
-  tsx@4.4.0:
-    resolution: {integrity: sha512-4fwcEjRUxW20ciSaMB8zkpGwCPxuRGnadDuj/pBk5S9uT29zvWz15PK36GrKJo45mSJomDxVejZ73c6lr3811Q==}
+  tslib@2.8.1:
+    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+  tsx@4.19.2:
+    resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==}
     engines: {node: '>=18.0.0'}
     hasBin: true
 
@@ -10546,13 +10781,18 @@ packages:
       typeorm-aurora-data-api-driver:
         optional: true
 
-  typescript@5.4.2:
-    resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==}
+  typescript@5.6.3:
+    resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
     engines: {node: '>=14.17'}
     hasBin: true
 
-  typescript@5.6.3:
-    resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
+  typescript@5.7.2:
+    resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  typescript@5.7.3:
+    resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
     engines: {node: '>=14.17'}
     hasBin: true
 
@@ -10583,8 +10823,11 @@ packages:
   undici-types@6.19.8:
     resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
 
-  undici@5.28.2:
-    resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==}
+  undici-types@6.20.0:
+    resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
+
+  undici@5.28.5:
+    resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==}
     engines: {node: '>=14.0'}
 
   undici@6.19.8:
@@ -10649,6 +10892,12 @@ packages:
     peerDependencies:
       browserslist: '>= 4.21.0'
 
+  update-browserslist-db@1.1.2:
+    resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==}
+    hasBin: true
+    peerDependencies:
+      browserslist: '>= 4.21.0'
+
   uri-js@4.4.1:
     resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
 
@@ -10677,6 +10926,10 @@ packages:
     resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
     hasBin: true
 
+  uuid@11.0.5:
+    resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==}
+    hasBin: true
+
   uuid@8.3.2:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
@@ -10758,6 +11011,46 @@ packages:
       terser:
         optional: true
 
+  vite@6.0.7:
+    resolution: {integrity: sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+      jiti: '>=1.21.0'
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      sass-embedded: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.16.0
+      tsx: ^4.8.1
+      yaml: ^2.4.2
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      jiti:
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+      tsx:
+        optional: true
+      yaml:
+        optional: true
+
   vitest-fetch-mock@0.2.2:
     resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==}
     engines: {node: '>=14.14.0'}
@@ -10831,9 +11124,6 @@ packages:
   vue-component-type-helpers@2.0.16:
     resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==}
 
-  vue-component-type-helpers@2.1.10:
-    resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==}
-
   vue-component-type-helpers@2.2.0:
     resolution: {integrity: sha512-cYrAnv2me7bPDcg9kIcGwjJiSB6Qyi08+jLDo9yuvoFQjzHiPTzML7RnkJB1+3P6KMsX/KbCD4QE3Tv/knEllw==}
 
@@ -10867,8 +11157,8 @@ packages:
   vue-template-compiler@2.7.14:
     resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==}
 
-  vue-tsc@2.1.10:
-    resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==}
+  vue-tsc@2.2.0:
+    resolution: {integrity: sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==}
     hasBin: true
     peerDependencies:
       typescript: '>=5.0.0'
@@ -10881,6 +11171,14 @@ packages:
       typescript:
         optional: true
 
+  vue@3.5.13:
+    resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   vuedraggable@4.1.0:
     resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
     peerDependencies:
@@ -10890,8 +11188,8 @@ packages:
     resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
     engines: {node: '>=18'}
 
-  wait-on@8.0.1:
-    resolution: {integrity: sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==}
+  wait-on@8.0.2:
+    resolution: {integrity: sha512-qHlU6AawrgAIHlueGQHQ+ETcPLAauXbnoTKl3RKq20W0T8x0DKVAo5xWIYjHSyvHxQlcYbFdR0jp4T9bDVITFA==}
     engines: {node: '>=12.0.0'}
     hasBin: true
 
@@ -10929,10 +11227,6 @@ packages:
   webpack-virtual-modules@0.5.0:
     resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
 
-  whatwg-encoding@2.0.0:
-    resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
-    engines: {node: '>=12'}
-
   whatwg-encoding@3.1.1:
     resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
     engines: {node: '>=18'}
@@ -10945,8 +11239,8 @@ packages:
     resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
     engines: {node: '>=18'}
 
-  whatwg-url@14.0.0:
-    resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==}
+  whatwg-url@14.1.0:
+    resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==}
     engines: {node: '>=18'}
 
   whatwg-url@5.0.0:
@@ -11141,17 +11435,25 @@ snapshots:
 
   '@apidevtools/swagger-methods@3.0.2': {}
 
+  '@asamuzakjp/css-color@2.8.3':
+    dependencies:
+      '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
+      '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
+      '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
+      '@csstools/css-tokenizer': 3.0.3
+      lru-cache: 10.4.3
+
   '@aws-crypto/crc32@5.2.0':
     dependencies:
       '@aws-crypto/util': 5.2.0
       '@aws-sdk/types': 3.609.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-crypto/crc32c@5.2.0':
     dependencies:
       '@aws-crypto/util': 5.2.0
       '@aws-sdk/types': 3.609.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-crypto/sha1-browser@5.2.0':
     dependencies:
@@ -11160,7 +11462,7 @@ snapshots:
       '@aws-sdk/types': 3.609.0
       '@aws-sdk/util-locate-window': 3.208.0
       '@smithy/util-utf8': 2.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-crypto/sha256-browser@5.2.0':
     dependencies:
@@ -11170,23 +11472,23 @@ snapshots:
       '@aws-sdk/types': 3.609.0
       '@aws-sdk/util-locate-window': 3.208.0
       '@smithy/util-utf8': 2.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-crypto/sha256-js@5.2.0':
     dependencies:
       '@aws-crypto/util': 5.2.0
       '@aws-sdk/types': 3.609.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-crypto/supports-web-crypto@5.2.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-crypto/util@5.2.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/util-utf8': 2.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/client-s3@3.620.0':
     dependencies:
@@ -11292,7 +11594,7 @@ snapshots:
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-retry': 3.0.3
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
     transitivePeerDependencies:
       - aws-crt
 
@@ -11335,7 +11637,7 @@ snapshots:
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-retry': 3.0.3
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
     transitivePeerDependencies:
       - aws-crt
 
@@ -11380,7 +11682,7 @@ snapshots:
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-retry': 3.0.3
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
     transitivePeerDependencies:
       - aws-crt
 
@@ -11392,14 +11694,14 @@ snapshots:
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
       fast-xml-parser: 4.2.5
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/credential-provider-env@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/property-provider': 3.1.3
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/credential-provider-http@3.620.0':
     dependencies:
@@ -11411,7 +11713,7 @@ snapshots:
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
       '@smithy/util-stream': 3.1.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/credential-provider-ini@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)':
     dependencies:
@@ -11426,7 +11728,7 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
     transitivePeerDependencies:
       - '@aws-sdk/client-sso-oidc'
       - aws-crt
@@ -11444,7 +11746,7 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
     transitivePeerDependencies:
       - '@aws-sdk/client-sso-oidc'
       - '@aws-sdk/client-sts'
@@ -11456,7 +11758,7 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/credential-provider-sso@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))':
     dependencies:
@@ -11466,7 +11768,7 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
     transitivePeerDependencies:
       - '@aws-sdk/client-sso-oidc'
       - aws-crt
@@ -11477,7 +11779,7 @@ snapshots:
       '@aws-sdk/types': 3.609.0
       '@smithy/property-provider': 3.1.3
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/lib-storage@3.620.0(@aws-sdk/client-s3@3.620.0)':
     dependencies:
@@ -11498,14 +11800,14 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
       '@smithy/util-config-provider': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-expect-continue@3.620.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-flexible-checksums@3.620.0':
     dependencies:
@@ -11516,33 +11818,33 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-host-header@3.620.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-location-constraint@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-logger@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-recursion-detection@3.620.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-sdk-s3@3.620.0':
     dependencies:
@@ -11556,7 +11858,7 @@ snapshots:
       '@smithy/util-config-provider': 3.0.0
       '@smithy/util-stream': 3.1.3
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-signing@3.620.0':
     dependencies:
@@ -11566,13 +11868,13 @@ snapshots:
       '@smithy/signature-v4': 4.1.0
       '@smithy/types': 3.3.0
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-ssec@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/middleware-user-agent@3.620.0':
     dependencies:
@@ -11580,7 +11882,7 @@ snapshots:
       '@aws-sdk/util-endpoints': 3.614.0
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/region-config-resolver@3.614.0':
     dependencies:
@@ -11589,7 +11891,7 @@ snapshots:
       '@smithy/types': 3.3.0
       '@smithy/util-config-provider': 3.0.0
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/signature-v4-multi-region@3.620.0':
     dependencies:
@@ -11598,7 +11900,7 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/signature-v4': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/token-providers@3.614.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))':
     dependencies:
@@ -11607,46 +11909,46 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/types@3.609.0':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/util-arn-parser@3.568.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/util-endpoints@3.614.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
       '@smithy/util-endpoints': 2.0.5
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/util-locate-window@3.208.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/util-user-agent-browser@3.609.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/types': 3.3.0
       bowser: 2.11.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/util-user-agent-node@3.614.0':
     dependencies:
       '@aws-sdk/types': 3.609.0
       '@smithy/node-config-provider': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@aws-sdk/xml-builder@3.609.0':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@babel/code-frame@7.23.5':
     dependencies:
@@ -11656,7 +11958,7 @@ snapshots:
   '@babel/code-frame@7.24.7':
     dependencies:
       '@babel/highlight': 7.24.7
-      picocolors: 1.0.1
+      picocolors: 1.1.1
 
   '@babel/compat-data@7.23.5': {}
 
@@ -11665,15 +11967,15 @@ snapshots:
   '@babel/core@7.23.5':
     dependencies:
       '@ampproject/remapping': 2.2.1
-      '@babel/code-frame': 7.23.5
+      '@babel/code-frame': 7.24.7
       '@babel/generator': 7.23.5
       '@babel/helper-compilation-targets': 7.22.15
       '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5)
       '@babel/helpers': 7.23.5
-      '@babel/parser': 7.24.7
+      '@babel/parser': 7.25.6
       '@babel/template': 7.22.15
-      '@babel/traverse': 7.23.5
-      '@babel/types': 7.24.7
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.25.6
       convert-source-map: 2.0.0
       debug: 4.3.7(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
@@ -11690,10 +11992,10 @@ snapshots:
       '@babel/helper-compilation-targets': 7.24.7
       '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7)
       '@babel/helpers': 7.24.7
-      '@babel/parser': 7.24.7
+      '@babel/parser': 7.25.6
       '@babel/template': 7.24.7
       '@babel/traverse': 7.24.7
-      '@babel/types': 7.24.7
+      '@babel/types': 7.25.6
       convert-source-map: 2.0.0
       debug: 4.3.7(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
@@ -11704,9 +12006,9 @@ snapshots:
 
   '@babel/generator@7.23.5':
     dependencies:
-      '@babel/types': 7.24.7
-      '@jridgewell/gen-mapping': 0.3.2
-      '@jridgewell/trace-mapping': 0.3.18
+      '@babel/types': 7.25.6
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
       jsesc: 2.5.2
 
   '@babel/generator@7.24.7':
@@ -11720,7 +12022,7 @@ snapshots:
     dependencies:
       '@babel/compat-data': 7.23.5
       '@babel/helper-validator-option': 7.23.5
-      browserslist: 4.22.2
+      browserslist: 4.23.0
       lru-cache: 5.1.1
       semver: 6.3.1
 
@@ -11732,33 +12034,22 @@ snapshots:
       lru-cache: 5.1.1
       semver: 6.3.1
 
-  '@babel/helper-environment-visitor@7.22.20': {}
-
   '@babel/helper-environment-visitor@7.24.7':
     dependencies:
       '@babel/types': 7.25.6
 
-  '@babel/helper-function-name@7.23.0':
-    dependencies:
-      '@babel/template': 7.24.7
-      '@babel/types': 7.25.6
-
   '@babel/helper-function-name@7.24.7':
     dependencies:
       '@babel/template': 7.24.7
       '@babel/types': 7.25.6
 
-  '@babel/helper-hoist-variables@7.22.5':
-    dependencies:
-      '@babel/types': 7.25.6
-
   '@babel/helper-hoist-variables@7.24.7':
     dependencies:
       '@babel/types': 7.25.6
 
   '@babel/helper-module-imports@7.22.15':
     dependencies:
-      '@babel/types': 7.24.7
+      '@babel/types': 7.25.6
 
   '@babel/helper-module-imports@7.24.7':
     dependencies:
@@ -11770,10 +12061,10 @@ snapshots:
   '@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5)':
     dependencies:
       '@babel/core': 7.23.5
-      '@babel/helper-environment-visitor': 7.22.20
+      '@babel/helper-environment-visitor': 7.24.7
       '@babel/helper-module-imports': 7.22.15
       '@babel/helper-simple-access': 7.22.5
-      '@babel/helper-split-export-declaration': 7.22.6
+      '@babel/helper-split-export-declaration': 7.24.7
       '@babel/helper-validator-identifier': 7.24.7
 
   '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)':
@@ -11791,7 +12082,7 @@ snapshots:
 
   '@babel/helper-simple-access@7.22.5':
     dependencies:
-      '@babel/types': 7.24.7
+      '@babel/types': 7.25.6
 
   '@babel/helper-simple-access@7.24.7':
     dependencies:
@@ -11800,10 +12091,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/helper-split-export-declaration@7.22.6':
-    dependencies:
-      '@babel/types': 7.24.7
-
   '@babel/helper-split-export-declaration@7.24.7':
     dependencies:
       '@babel/types': 7.25.6
@@ -11821,8 +12108,8 @@ snapshots:
   '@babel/helpers@7.23.5':
     dependencies:
       '@babel/template': 7.22.15
-      '@babel/traverse': 7.23.5
-      '@babel/types': 7.24.7
+      '@babel/traverse': 7.24.7
+      '@babel/types': 7.25.6
     transitivePeerDependencies:
       - supports-color
 
@@ -11842,7 +12129,7 @@ snapshots:
       '@babel/helper-validator-identifier': 7.24.7
       chalk: 2.4.2
       js-tokens: 4.0.0
-      picocolors: 1.0.1
+      picocolors: 1.1.1
 
   '@babel/parser@7.24.7':
     dependencies:
@@ -11930,7 +12217,7 @@ snapshots:
     dependencies:
       '@babel/code-frame': 7.24.7
       '@babel/parser': 7.25.6
-      '@babel/types': 7.24.7
+      '@babel/types': 7.25.6
 
   '@babel/template@7.24.0':
     dependencies:
@@ -11944,21 +12231,6 @@ snapshots:
       '@babel/parser': 7.25.6
       '@babel/types': 7.25.6
 
-  '@babel/traverse@7.23.5':
-    dependencies:
-      '@babel/code-frame': 7.24.7
-      '@babel/generator': 7.23.5
-      '@babel/helper-environment-visitor': 7.22.20
-      '@babel/helper-function-name': 7.23.0
-      '@babel/helper-hoist-variables': 7.22.5
-      '@babel/helper-split-export-declaration': 7.22.6
-      '@babel/parser': 7.25.6
-      '@babel/types': 7.24.7
-      debug: 4.3.7(supports-color@8.1.1)
-      globals: 11.12.0
-    transitivePeerDependencies:
-      - supports-color
-
   '@babel/traverse@7.24.7':
     dependencies:
       '@babel/code-frame': 7.24.7
@@ -11988,22 +12260,22 @@ snapshots:
 
   '@bcoe/v8-coverage@0.2.3': {}
 
-  '@bull-board/api@6.5.0(@bull-board/ui@6.5.0)':
+  '@bull-board/api@6.7.0(@bull-board/ui@6.7.0)':
     dependencies:
-      '@bull-board/ui': 6.5.0
+      '@bull-board/ui': 6.7.0
       redis-info: 3.1.0
 
-  '@bull-board/fastify@6.5.0':
+  '@bull-board/fastify@6.7.0':
     dependencies:
-      '@bull-board/api': 6.5.0(@bull-board/ui@6.5.0)
-      '@bull-board/ui': 6.5.0
-      '@fastify/static': 8.0.2
-      '@fastify/view': 10.0.1
+      '@bull-board/api': 6.7.0(@bull-board/ui@6.7.0)
+      '@bull-board/ui': 6.7.0
+      '@fastify/static': 8.0.4
+      '@fastify/view': 10.0.2
       ejs: 3.1.10
 
-  '@bull-board/ui@6.5.0':
+  '@bull-board/ui@6.7.0':
     dependencies:
-      '@bull-board/api': 6.5.0(@bull-board/ui@6.5.0)
+      '@bull-board/api': 6.7.0(@bull-board/ui@6.7.0)
 
   '@bundled-es-modules/cookie@2.0.1':
     dependencies:
@@ -12020,6 +12292,8 @@ snapshots:
 
   '@canvas/image-data@1.0.0': {}
 
+  '@chainsafe/is-ip@2.1.0': {}
+
   '@colors/colors@1.5.0':
     optional: true
 
@@ -12089,6 +12363,26 @@ snapshots:
 
   '@cropper/utils@2.0.0-rc.2': {}
 
+  '@csstools/color-helpers@5.0.1': {}
+
+  '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
+    dependencies:
+      '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
+      '@csstools/css-tokenizer': 3.0.3
+
+  '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
+    dependencies:
+      '@csstools/color-helpers': 5.0.1
+      '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
+      '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
+      '@csstools/css-tokenizer': 3.0.3
+
+  '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)':
+    dependencies:
+      '@csstools/css-tokenizer': 3.0.3
+
+  '@csstools/css-tokenizer@3.0.3': {}
+
   '@cypress/request@3.0.6':
     dependencies:
       aws-sign2: 0.7.0
@@ -12121,7 +12415,7 @@ snapshots:
     dependencies:
       ky: 0.33.3
       ky-universal: 0.11.0(ky@0.33.3)(web-streams-polyfill@4.0.0)
-      undici: 5.28.2
+      undici: 5.28.5
     transitivePeerDependencies:
       - web-streams-polyfill
 
@@ -12134,240 +12428,321 @@ snapshots:
 
   '@emnapi/runtime@1.2.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
     optional: true
 
   '@esbuild/aix-ppc64@0.21.5':
     optional: true
 
+  '@esbuild/aix-ppc64@0.23.1':
+    optional: true
+
   '@esbuild/aix-ppc64@0.24.0':
     optional: true
 
-  '@esbuild/android-arm64@0.18.20':
+  '@esbuild/aix-ppc64@0.24.2':
     optional: true
 
   '@esbuild/android-arm64@0.21.5':
     optional: true
 
+  '@esbuild/android-arm64@0.23.1':
+    optional: true
+
   '@esbuild/android-arm64@0.24.0':
     optional: true
 
-  '@esbuild/android-arm@0.18.20':
+  '@esbuild/android-arm64@0.24.2':
     optional: true
 
   '@esbuild/android-arm@0.21.5':
     optional: true
 
+  '@esbuild/android-arm@0.23.1':
+    optional: true
+
   '@esbuild/android-arm@0.24.0':
     optional: true
 
-  '@esbuild/android-x64@0.18.20':
+  '@esbuild/android-arm@0.24.2':
     optional: true
 
   '@esbuild/android-x64@0.21.5':
     optional: true
 
+  '@esbuild/android-x64@0.23.1':
+    optional: true
+
   '@esbuild/android-x64@0.24.0':
     optional: true
 
-  '@esbuild/darwin-arm64@0.18.20':
+  '@esbuild/android-x64@0.24.2':
     optional: true
 
   '@esbuild/darwin-arm64@0.21.5':
     optional: true
 
+  '@esbuild/darwin-arm64@0.23.1':
+    optional: true
+
   '@esbuild/darwin-arm64@0.24.0':
     optional: true
 
-  '@esbuild/darwin-x64@0.18.20':
+  '@esbuild/darwin-arm64@0.24.2':
     optional: true
 
   '@esbuild/darwin-x64@0.21.5':
     optional: true
 
+  '@esbuild/darwin-x64@0.23.1':
+    optional: true
+
   '@esbuild/darwin-x64@0.24.0':
     optional: true
 
-  '@esbuild/freebsd-arm64@0.18.20':
+  '@esbuild/darwin-x64@0.24.2':
     optional: true
 
   '@esbuild/freebsd-arm64@0.21.5':
     optional: true
 
+  '@esbuild/freebsd-arm64@0.23.1':
+    optional: true
+
   '@esbuild/freebsd-arm64@0.24.0':
     optional: true
 
-  '@esbuild/freebsd-x64@0.18.20':
+  '@esbuild/freebsd-arm64@0.24.2':
     optional: true
 
   '@esbuild/freebsd-x64@0.21.5':
     optional: true
 
+  '@esbuild/freebsd-x64@0.23.1':
+    optional: true
+
   '@esbuild/freebsd-x64@0.24.0':
     optional: true
 
-  '@esbuild/linux-arm64@0.18.20':
+  '@esbuild/freebsd-x64@0.24.2':
     optional: true
 
   '@esbuild/linux-arm64@0.21.5':
     optional: true
 
+  '@esbuild/linux-arm64@0.23.1':
+    optional: true
+
   '@esbuild/linux-arm64@0.24.0':
     optional: true
 
-  '@esbuild/linux-arm@0.18.20':
+  '@esbuild/linux-arm64@0.24.2':
     optional: true
 
   '@esbuild/linux-arm@0.21.5':
     optional: true
 
+  '@esbuild/linux-arm@0.23.1':
+    optional: true
+
   '@esbuild/linux-arm@0.24.0':
     optional: true
 
-  '@esbuild/linux-ia32@0.18.20':
+  '@esbuild/linux-arm@0.24.2':
     optional: true
 
   '@esbuild/linux-ia32@0.21.5':
     optional: true
 
+  '@esbuild/linux-ia32@0.23.1':
+    optional: true
+
   '@esbuild/linux-ia32@0.24.0':
     optional: true
 
-  '@esbuild/linux-loong64@0.18.20':
+  '@esbuild/linux-ia32@0.24.2':
     optional: true
 
   '@esbuild/linux-loong64@0.21.5':
     optional: true
 
+  '@esbuild/linux-loong64@0.23.1':
+    optional: true
+
   '@esbuild/linux-loong64@0.24.0':
     optional: true
 
-  '@esbuild/linux-mips64el@0.18.20':
+  '@esbuild/linux-loong64@0.24.2':
     optional: true
 
   '@esbuild/linux-mips64el@0.21.5':
     optional: true
 
+  '@esbuild/linux-mips64el@0.23.1':
+    optional: true
+
   '@esbuild/linux-mips64el@0.24.0':
     optional: true
 
-  '@esbuild/linux-ppc64@0.18.20':
+  '@esbuild/linux-mips64el@0.24.2':
     optional: true
 
   '@esbuild/linux-ppc64@0.21.5':
     optional: true
 
+  '@esbuild/linux-ppc64@0.23.1':
+    optional: true
+
   '@esbuild/linux-ppc64@0.24.0':
     optional: true
 
-  '@esbuild/linux-riscv64@0.18.20':
+  '@esbuild/linux-ppc64@0.24.2':
     optional: true
 
   '@esbuild/linux-riscv64@0.21.5':
     optional: true
 
+  '@esbuild/linux-riscv64@0.23.1':
+    optional: true
+
   '@esbuild/linux-riscv64@0.24.0':
     optional: true
 
-  '@esbuild/linux-s390x@0.18.20':
+  '@esbuild/linux-riscv64@0.24.2':
     optional: true
 
   '@esbuild/linux-s390x@0.21.5':
     optional: true
 
+  '@esbuild/linux-s390x@0.23.1':
+    optional: true
+
   '@esbuild/linux-s390x@0.24.0':
     optional: true
 
-  '@esbuild/linux-x64@0.18.20':
+  '@esbuild/linux-s390x@0.24.2':
     optional: true
 
   '@esbuild/linux-x64@0.21.5':
     optional: true
 
+  '@esbuild/linux-x64@0.23.1':
+    optional: true
+
   '@esbuild/linux-x64@0.24.0':
     optional: true
 
-  '@esbuild/netbsd-x64@0.18.20':
+  '@esbuild/linux-x64@0.24.2':
+    optional: true
+
+  '@esbuild/netbsd-arm64@0.24.2':
     optional: true
 
   '@esbuild/netbsd-x64@0.21.5':
     optional: true
 
+  '@esbuild/netbsd-x64@0.23.1':
+    optional: true
+
   '@esbuild/netbsd-x64@0.24.0':
     optional: true
 
+  '@esbuild/netbsd-x64@0.24.2':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.23.1':
+    optional: true
+
   '@esbuild/openbsd-arm64@0.24.0':
     optional: true
 
-  '@esbuild/openbsd-x64@0.18.20':
+  '@esbuild/openbsd-arm64@0.24.2':
     optional: true
 
   '@esbuild/openbsd-x64@0.21.5':
     optional: true
 
+  '@esbuild/openbsd-x64@0.23.1':
+    optional: true
+
   '@esbuild/openbsd-x64@0.24.0':
     optional: true
 
-  '@esbuild/sunos-x64@0.18.20':
+  '@esbuild/openbsd-x64@0.24.2':
     optional: true
 
   '@esbuild/sunos-x64@0.21.5':
     optional: true
 
+  '@esbuild/sunos-x64@0.23.1':
+    optional: true
+
   '@esbuild/sunos-x64@0.24.0':
     optional: true
 
-  '@esbuild/win32-arm64@0.18.20':
+  '@esbuild/sunos-x64@0.24.2':
     optional: true
 
   '@esbuild/win32-arm64@0.21.5':
     optional: true
 
+  '@esbuild/win32-arm64@0.23.1':
+    optional: true
+
   '@esbuild/win32-arm64@0.24.0':
     optional: true
 
-  '@esbuild/win32-ia32@0.18.20':
+  '@esbuild/win32-arm64@0.24.2':
     optional: true
 
   '@esbuild/win32-ia32@0.21.5':
     optional: true
 
+  '@esbuild/win32-ia32@0.23.1':
+    optional: true
+
   '@esbuild/win32-ia32@0.24.0':
     optional: true
 
-  '@esbuild/win32-x64@0.18.20':
+  '@esbuild/win32-ia32@0.24.2':
     optional: true
 
   '@esbuild/win32-x64@0.21.5':
     optional: true
 
+  '@esbuild/win32-x64@0.23.1':
+    optional: true
+
   '@esbuild/win32-x64@0.24.0':
     optional: true
 
-  '@eslint-community/eslint-utils@4.4.0(eslint@9.14.0)':
+  '@esbuild/win32-x64@0.24.2':
+    optional: true
+
+  '@eslint-community/eslint-utils@4.4.0(eslint@9.18.0)':
     dependencies:
-      eslint: 9.14.0
+      eslint: 9.18.0
       eslint-visitor-keys: 3.4.3
 
   '@eslint-community/regexpp@4.11.0': {}
 
   '@eslint-community/regexpp@4.12.1': {}
 
-  '@eslint-community/regexpp@4.6.2': {}
-
   '@eslint/compat@1.1.1': {}
 
-  '@eslint/config-array@0.18.0':
+  '@eslint/config-array@0.19.1':
     dependencies:
-      '@eslint/object-schema': 2.1.4
+      '@eslint/object-schema': 2.1.5
       debug: 4.3.7(supports-color@8.1.1)
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/core@0.7.0': {}
+  '@eslint/core@0.10.0':
+    dependencies:
+      '@types/json-schema': 7.0.15
 
-  '@eslint/eslintrc@3.1.0':
+  '@eslint/eslintrc@3.2.0':
     dependencies:
       ajv: 6.12.6
       debug: 4.3.7(supports-color@8.1.1)
@@ -12381,17 +12756,18 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/js@9.14.0': {}
+  '@eslint/js@9.18.0': {}
 
-  '@eslint/object-schema@2.1.4': {}
+  '@eslint/object-schema@2.1.5': {}
 
-  '@eslint/plugin-kit@0.2.0':
+  '@eslint/plugin-kit@0.2.5':
     dependencies:
+      '@eslint/core': 0.10.0
       levn: 0.4.1
 
   '@fastify/accept-negotiator@2.0.0': {}
 
-  '@fastify/accepts@5.0.1':
+  '@fastify/accepts@5.0.2':
     dependencies:
       accepts: 1.3.8
       fastify-plugin: 5.0.0
@@ -12406,12 +12782,12 @@ snapshots:
 
   '@fastify/busboy@3.0.0': {}
 
-  '@fastify/cookie@11.0.1':
+  '@fastify/cookie@11.0.2':
     dependencies:
       cookie: 1.0.1
       fastify-plugin: 5.0.0
 
-  '@fastify/cors@10.0.1':
+  '@fastify/cors@10.0.2':
     dependencies:
       fastify-plugin: 5.0.0
       mnemonist: 0.39.8
@@ -12420,9 +12796,9 @@ snapshots:
 
   '@fastify/error@4.0.0': {}
 
-  '@fastify/express@4.0.1':
+  '@fastify/express@4.0.2':
     dependencies:
-      express: 4.21.0
+      express: 4.21.1
       fastify-plugin: 5.0.0
     transitivePeerDependencies:
       - supports-color
@@ -12431,6 +12807,8 @@ snapshots:
     dependencies:
       fast-json-stringify: 6.0.0
 
+  '@fastify/forwarded@3.0.0': {}
+
   '@fastify/http-proxy@10.0.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)':
     dependencies:
       '@fastify/reply-from': 11.0.0
@@ -12445,13 +12823,18 @@ snapshots:
     dependencies:
       fast-deep-equal: 3.1.3
 
-  '@fastify/multipart@9.0.1':
+  '@fastify/multipart@9.0.2':
     dependencies:
       '@fastify/busboy': 3.0.0
       '@fastify/deepmerge': 2.0.0
       '@fastify/error': 4.0.0
       fastify-plugin: 5.0.0
-      secure-json-parse: 3.0.0
+      secure-json-parse: 3.0.2
+
+  '@fastify/proxy-addr@5.0.0':
+    dependencies:
+      '@fastify/forwarded': 3.0.0
+      ipaddr.js: 2.2.0
 
   '@fastify/reply-from@11.0.0':
     dependencies:
@@ -12463,7 +12846,7 @@ snapshots:
       toad-cache: 3.7.0
       undici: 6.19.8
 
-  '@fastify/send@3.1.1':
+  '@fastify/send@3.3.1':
     dependencies:
       '@lukeed/ms': 2.0.2
       escape-html: 1.0.3
@@ -12471,16 +12854,16 @@ snapshots:
       http-errors: 2.0.0
       mime: 3.0.0
 
-  '@fastify/static@8.0.2':
+  '@fastify/static@8.0.4':
     dependencies:
       '@fastify/accept-negotiator': 2.0.0
-      '@fastify/send': 3.1.1
+      '@fastify/send': 3.3.1
       content-disposition: 0.5.4
       fastify-plugin: 5.0.0
       fastq: 1.17.1
-      glob: 11.0.0
+      glob: 11.0.1
 
-  '@fastify/view@10.0.1':
+  '@fastify/view@10.0.2':
     dependencies:
       fastify-plugin: 5.0.0
       toad-cache: 3.7.0
@@ -12599,16 +12982,16 @@ snapshots:
   '@img/sharp-win32-x64@0.33.5':
     optional: true
 
-  '@inquirer/confirm@5.0.2(@types/node@22.9.0)':
+  '@inquirer/confirm@5.0.2(@types/node@22.10.7)':
     dependencies:
-      '@inquirer/core': 10.1.0(@types/node@22.9.0)
-      '@inquirer/type': 3.0.1(@types/node@22.9.0)
-      '@types/node': 22.9.0
+      '@inquirer/core': 10.1.0(@types/node@22.10.7)
+      '@inquirer/type': 3.0.1(@types/node@22.10.7)
+      '@types/node': 22.10.7
 
-  '@inquirer/core@10.1.0(@types/node@22.9.0)':
+  '@inquirer/core@10.1.0(@types/node@22.10.7)':
     dependencies:
       '@inquirer/figures': 1.0.8
-      '@inquirer/type': 3.0.1(@types/node@22.9.0)
+      '@inquirer/type': 3.0.1(@types/node@22.10.7)
       ansi-escapes: 4.3.2
       cli-width: 4.1.0
       mute-stream: 2.0.0
@@ -12621,9 +13004,9 @@ snapshots:
 
   '@inquirer/figures@1.0.8': {}
 
-  '@inquirer/type@3.0.1(@types/node@22.9.0)':
+  '@inquirer/type@3.0.1(@types/node@22.10.7)':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@ioredis/commands@1.2.0': {}
 
@@ -12649,7 +13032,7 @@ snapshots:
   '@jest/console@29.7.0':
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       chalk: 4.1.2
       jest-message-util: 29.7.0
       jest-util: 29.7.0
@@ -12662,14 +13045,14 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       ci-info: 3.7.1
       exit: 0.1.2
       graceful-fs: 4.2.11
       jest-changed-files: 29.7.0
-      jest-config: 29.7.0(@types/node@22.9.0)
+      jest-config: 29.7.0(@types/node@22.10.7)
       jest-haste-map: 29.7.0
       jest-message-util: 29.7.0
       jest-regex-util: 29.6.3
@@ -12698,7 +13081,7 @@ snapshots:
     dependencies:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       jest-mock: 29.7.0
 
   '@jest/expect-utils@29.7.0':
@@ -12716,7 +13099,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@sinonjs/fake-timers': 10.3.0
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       jest-message-util: 29.7.0
       jest-mock: 29.7.0
       jest-util: 29.7.0
@@ -12738,7 +13121,7 @@ snapshots:
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.25
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       chalk: 4.1.2
       collect-v8-coverage: 1.0.1
       exit: 0.1.2
@@ -12808,25 +13191,17 @@ snapshots:
       '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       '@types/yargs': 17.0.19
       chalk: 4.1.2
 
-  '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))':
+  '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.3)(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))':
     dependencies:
-      glob: 7.2.3
-      glob-promise: 4.2.2(glob@7.2.3)
       magic-string: 0.27.0
-      react-docgen-typescript: 2.2.2(typescript@5.6.3)
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
+      react-docgen-typescript: 2.2.2(typescript@5.7.3)
+      vite: 6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2)
     optionalDependencies:
-      typescript: 5.6.3
-
-  '@jridgewell/gen-mapping@0.3.2':
-    dependencies:
-      '@jridgewell/set-array': 1.1.2
-      '@jridgewell/sourcemap-codec': 1.4.15
-      '@jridgewell/trace-mapping': 0.3.18
+      typescript: 5.7.3
 
   '@jridgewell/gen-mapping@0.3.5':
     dependencies:
@@ -12836,8 +13211,6 @@ snapshots:
 
   '@jridgewell/resolve-uri@3.1.0': {}
 
-  '@jridgewell/set-array@1.1.2': {}
-
   '@jridgewell/set-array@1.2.1': {}
 
   '@jridgewell/source-map@0.3.6':
@@ -12845,17 +13218,10 @@ snapshots:
       '@jridgewell/gen-mapping': 0.3.5
       '@jridgewell/trace-mapping': 0.3.25
 
-  '@jridgewell/sourcemap-codec@1.4.14': {}
-
   '@jridgewell/sourcemap-codec@1.4.15': {}
 
   '@jridgewell/sourcemap-codec@1.5.0': {}
 
-  '@jridgewell/trace-mapping@0.3.18':
-    dependencies:
-      '@jridgewell/resolve-uri': 3.1.0
-      '@jridgewell/sourcemap-codec': 1.4.14
-
   '@jridgewell/trace-mapping@0.3.25':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.0
@@ -12899,51 +13265,52 @@ snapshots:
       '@types/react': 18.0.28
       react: 18.3.1
 
-  '@microsoft/api-extractor-model@7.29.8(@types/node@22.9.0)':
+  '@microsoft/api-extractor-model@7.30.2(@types/node@22.10.7)':
     dependencies:
-      '@microsoft/tsdoc': 0.15.0
-      '@microsoft/tsdoc-config': 0.17.0
-      '@rushstack/node-core-library': 5.9.0(@types/node@22.9.0)
+      '@microsoft/tsdoc': 0.15.1
+      '@microsoft/tsdoc-config': 0.17.1
+      '@rushstack/node-core-library': 5.10.2(@types/node@22.10.7)
     transitivePeerDependencies:
       - '@types/node'
 
-  '@microsoft/api-extractor@7.47.11(@types/node@22.9.0)':
+  '@microsoft/api-extractor@7.49.1(@types/node@22.10.7)':
     dependencies:
-      '@microsoft/api-extractor-model': 7.29.8(@types/node@22.9.0)
-      '@microsoft/tsdoc': 0.15.0
-      '@microsoft/tsdoc-config': 0.17.0
-      '@rushstack/node-core-library': 5.9.0(@types/node@22.9.0)
+      '@microsoft/api-extractor-model': 7.30.2(@types/node@22.10.7)
+      '@microsoft/tsdoc': 0.15.1
+      '@microsoft/tsdoc-config': 0.17.1
+      '@rushstack/node-core-library': 5.10.2(@types/node@22.10.7)
       '@rushstack/rig-package': 0.5.3
-      '@rushstack/terminal': 0.14.2(@types/node@22.9.0)
-      '@rushstack/ts-command-line': 4.23.0(@types/node@22.9.0)
+      '@rushstack/terminal': 0.14.5(@types/node@22.10.7)
+      '@rushstack/ts-command-line': 4.23.3(@types/node@22.10.7)
       lodash: 4.17.21
       minimatch: 3.0.8
       resolve: 1.22.8
       semver: 7.5.4
       source-map: 0.6.1
-      typescript: 5.4.2
+      typescript: 5.7.2
     transitivePeerDependencies:
       - '@types/node'
 
-  '@microsoft/tsdoc-config@0.17.0':
+  '@microsoft/tsdoc-config@0.17.1':
     dependencies:
-      '@microsoft/tsdoc': 0.15.0
+      '@microsoft/tsdoc': 0.15.1
       ajv: 8.12.0
       jju: 1.4.0
       resolve: 1.22.8
 
-  '@microsoft/tsdoc@0.15.0': {}
+  '@microsoft/tsdoc@0.15.1': {}
 
   '@misskey-dev/browser-image-resizer@2024.1.0': {}
 
-  '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3))(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0))(eslint@9.14.0)(globals@15.12.0)':
+  '@misskey-dev/eslint-plugin@2.1.0(@eslint/compat@1.1.1)(@stylistic/eslint-plugin@2.13.0(eslint@9.18.0)(typescript@5.7.3))(@typescript-eslint/eslint-plugin@8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3))(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0))(eslint@9.18.0)(globals@15.14.0)':
     dependencies:
       '@eslint/compat': 1.1.1
-      '@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
-      '@typescript-eslint/parser': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
-      eslint: 9.14.0
-      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)
-      globals: 15.12.0
+      '@stylistic/eslint-plugin': 2.13.0(eslint@9.18.0)(typescript@5.7.3)
+      '@typescript-eslint/eslint-plugin': 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)
+      '@typescript-eslint/parser': 8.20.0(eslint@9.18.0)(typescript@5.7.3)
+      eslint: 9.18.0
+      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)
+      globals: 15.14.0
 
   '@misskey-dev/sharp-read-bmp@1.2.0':
     dependencies:
@@ -12951,16 +13318,15 @@ snapshots:
       decode-ico: 0.4.1
       sharp: 0.33.5
 
-  '@misskey-dev/summaly@5.1.0':
+  '@misskey-dev/summaly@5.2.0':
     dependencies:
-      cheerio: 1.0.0-rc.12
+      cheerio: 1.0.0
       escape-regexp: 0.0.1
-      got: 12.6.1
-      html-entities: 2.3.2
+      got: 14.4.5
+      html-entities: 2.5.2
       iconv-lite: 0.6.3
-      jschardet: 3.0.0
-      private-ip: 2.3.3
-      trace-redirect: 1.0.6
+      jschardet: 3.1.4
+      private-ip: 3.0.2
 
   '@mole-inc/bin-wrapper@8.0.1':
     dependencies:
@@ -12991,7 +13357,7 @@ snapshots:
   '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2':
     optional: true
 
-  '@mswjs/interceptors@0.36.10':
+  '@mswjs/interceptors@0.37.5':
     dependencies:
       '@open-draft/deferred-promise': 2.2.0
       '@open-draft/logger': 0.3.0
@@ -13000,73 +13366,75 @@ snapshots:
       outvariant: 1.4.3
       strict-event-emitter: 0.5.1
 
-  '@napi-rs/canvas-android-arm64@0.1.56':
+  '@napi-rs/canvas-android-arm64@0.1.65':
+    optional: true
+
+  '@napi-rs/canvas-darwin-arm64@0.1.65':
     optional: true
 
-  '@napi-rs/canvas-darwin-arm64@0.1.56':
+  '@napi-rs/canvas-darwin-x64@0.1.65':
     optional: true
 
-  '@napi-rs/canvas-darwin-x64@0.1.56':
+  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.65':
     optional: true
 
-  '@napi-rs/canvas-linux-arm-gnueabihf@0.1.56':
+  '@napi-rs/canvas-linux-arm64-gnu@0.1.65':
     optional: true
 
-  '@napi-rs/canvas-linux-arm64-gnu@0.1.56':
+  '@napi-rs/canvas-linux-arm64-musl@0.1.65':
     optional: true
 
-  '@napi-rs/canvas-linux-arm64-musl@0.1.56':
+  '@napi-rs/canvas-linux-riscv64-gnu@0.1.65':
     optional: true
 
-  '@napi-rs/canvas-linux-x64-gnu@0.1.56':
+  '@napi-rs/canvas-linux-x64-gnu@0.1.65':
     optional: true
 
-  '@napi-rs/canvas-linux-x64-musl@0.1.56':
+  '@napi-rs/canvas-linux-x64-musl@0.1.65':
     optional: true
 
-  '@napi-rs/canvas-win32-x64-msvc@0.1.56':
+  '@napi-rs/canvas-win32-x64-msvc@0.1.65':
     optional: true
 
-  '@napi-rs/canvas@0.1.56':
+  '@napi-rs/canvas@0.1.65':
     optionalDependencies:
-      '@napi-rs/canvas-android-arm64': 0.1.56
-      '@napi-rs/canvas-darwin-arm64': 0.1.56
-      '@napi-rs/canvas-darwin-x64': 0.1.56
-      '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.56
-      '@napi-rs/canvas-linux-arm64-gnu': 0.1.56
-      '@napi-rs/canvas-linux-arm64-musl': 0.1.56
-      '@napi-rs/canvas-linux-x64-gnu': 0.1.56
-      '@napi-rs/canvas-linux-x64-musl': 0.1.56
-      '@napi-rs/canvas-win32-x64-msvc': 0.1.56
-
-  '@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+      '@napi-rs/canvas-android-arm64': 0.1.65
+      '@napi-rs/canvas-darwin-arm64': 0.1.65
+      '@napi-rs/canvas-darwin-x64': 0.1.65
+      '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.65
+      '@napi-rs/canvas-linux-arm64-gnu': 0.1.65
+      '@napi-rs/canvas-linux-arm64-musl': 0.1.65
+      '@napi-rs/canvas-linux-riscv64-gnu': 0.1.65
+      '@napi-rs/canvas-linux-x64-gnu': 0.1.65
+      '@napi-rs/canvas-linux-x64-musl': 0.1.65
+      '@napi-rs/canvas-win32-x64-msvc': 0.1.65
+
+  '@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1)':
     dependencies:
       iterare: 1.2.1
       reflect-metadata: 0.2.2
       rxjs: 7.8.1
-      tslib: 2.7.0
+      tslib: 2.8.1
       uid: 2.0.2
 
-  '@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+  '@nestjs/core@11.0.1(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
     dependencies:
-      '@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13)
+      '@nestjs/common': 11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nuxt/opencollective': 0.4.1
       fast-safe-stringify: 2.1.1
       iterare: 1.2.1
-      path-to-regexp: 3.3.0
+      path-to-regexp: 8.2.0
       reflect-metadata: 0.2.2
       rxjs: 7.8.1
-      tslib: 2.7.0
+      tslib: 2.8.1
       uid: 2.0.2
     optionalDependencies:
-      '@nestjs/platform-express': 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)
-    transitivePeerDependencies:
-      - encoding
+      '@nestjs/platform-express': 10.4.7(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.1)
 
-  '@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)':
+  '@nestjs/platform-express@10.4.7(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.1)':
     dependencies:
-      '@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/common': 11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/core': 11.0.1(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)
       body-parser: 1.20.3
       cors: 2.8.5
       express: 4.21.1
@@ -13075,15 +13443,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)':
+  '@nestjs/testing@11.0.1(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.1)(@nestjs/platform-express@10.4.7)':
     dependencies:
-      '@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
-      tslib: 2.7.0
+      '@nestjs/common': 11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      '@nestjs/core': 11.0.1(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+      tslib: 2.8.1
     optionalDependencies:
-      '@nestjs/platform-express': 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)
+      '@nestjs/platform-express': 10.4.7(@nestjs/common@11.0.1(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.1)
 
-  '@noble/hashes@1.5.0': {}
+  '@noble/hashes@1.6.1': {}
 
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
@@ -13111,13 +13479,9 @@ snapshots:
     dependencies:
       semver: 7.6.3
 
-  '@nuxtjs/opencollective@0.3.2(encoding@0.1.13)':
+  '@nuxt/opencollective@0.4.1':
     dependencies:
-      chalk: 4.1.2
-      consola: 2.15.3
-      node-fetch: 2.7.0(encoding@0.1.13)
-    transitivePeerDependencies:
-      - encoding
+      consola: 3.4.0
 
   '@one-ini/wasm@0.1.1': {}
 
@@ -13130,212 +13494,205 @@ snapshots:
 
   '@open-draft/until@2.1.0': {}
 
-  '@opentelemetry/api-logs@0.52.1':
-    dependencies:
-      '@opentelemetry/api': 1.9.0
-
   '@opentelemetry/api-logs@0.53.0':
     dependencies:
       '@opentelemetry/api': 1.9.0
 
-  '@opentelemetry/api-logs@0.54.2':
+  '@opentelemetry/api-logs@0.56.0':
     dependencies:
       '@opentelemetry/api': 1.9.0
 
   '@opentelemetry/api@1.9.0': {}
 
-  '@opentelemetry/context-async-hooks@1.25.1(@opentelemetry/api@1.9.0)':
-    dependencies:
-      '@opentelemetry/api': 1.9.0
-
-  '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/semantic-conventions': 1.25.1
 
-  '@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/core@1.29.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/semantic-conventions': 1.28.0
 
-  '@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/semantic-conventions': 1.28.0
 
-  '@opentelemetry/instrumentation-amqplib@0.43.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-amqplib@0.45.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-connect@0.40.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-connect@0.42.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
       '@types/connect': 3.4.36
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-dataloader@0.12.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-dataloader@0.15.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-express@0.44.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-express@0.46.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-fastify@0.41.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-fastify@0.43.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-fs@0.16.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-fs@0.18.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-generic-pool@0.39.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-generic-pool@0.42.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-graphql@0.44.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-graphql@0.46.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-hapi@0.41.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-hapi@0.44.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-http@0.53.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-http@0.56.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
+      forwarded-parse: 2.1.2
       semver: 7.6.3
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-ioredis@0.43.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-ioredis@0.46.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
       '@opentelemetry/redis-common': 0.36.2
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-kafkajs@0.4.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-kafkajs@0.6.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-knex@0.41.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-knex@0.43.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-koa@0.43.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-koa@0.46.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-lru-memoizer@0.40.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-lru-memoizer@0.43.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-mongodb@0.48.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-mongodb@0.50.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-mongoose@0.42.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-mongoose@0.45.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-mysql2@0.41.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-mysql2@0.44.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
       '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-mysql@0.41.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-mysql@0.44.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
       '@types/mysql': 2.15.26
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-nestjs-core@0.40.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-nestjs-core@0.43.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-pg@0.44.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-pg@0.49.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
       '@opentelemetry/semantic-conventions': 1.27.0
       '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0)
       '@types/pg': 8.6.1
@@ -13343,41 +13700,29 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-redis-4@0.42.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-redis-4@0.45.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
       '@opentelemetry/redis-common': 0.36.2
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/semantic-conventions': 1.28.0
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-tedious@0.15.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-tedious@0.17.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
       '@types/tedious': 4.0.14
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation-undici@0.6.0(@opentelemetry/api@1.9.0)':
-    dependencies:
-      '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
-    transitivePeerDependencies:
-      - supports-color
-
-  '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation-undici@0.9.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/api-logs': 0.52.1
-      '@types/shimmer': 1.0.5
-      import-in-the-middle: 1.11.2
-      require-in-the-middle: 7.3.0
-      semver: 7.6.3
-      shimmer: 1.2.1
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -13393,10 +13738,10 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/api-logs': 0.54.2
+      '@opentelemetry/api-logs': 0.56.0
       '@types/shimmer': 1.2.0
       import-in-the-middle: 1.11.2
       require-in-the-middle: 7.3.0
@@ -13407,53 +13752,114 @@ snapshots:
 
   '@opentelemetry/redis-common@0.36.2': {}
 
-  '@opentelemetry/resources@1.27.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
 
-  '@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0)':
+  '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
-
-  '@opentelemetry/semantic-conventions@1.25.1': {}
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
 
   '@opentelemetry/semantic-conventions@1.27.0': {}
 
+  '@opentelemetry/semantic-conventions@1.28.0': {}
+
   '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+
+  '@parcel/watcher-android-arm64@2.5.0':
+    optional: true
+
+  '@parcel/watcher-darwin-arm64@2.5.0':
+    optional: true
+
+  '@parcel/watcher-darwin-x64@2.5.0':
+    optional: true
+
+  '@parcel/watcher-freebsd-x64@2.5.0':
+    optional: true
+
+  '@parcel/watcher-linux-arm-glibc@2.5.0':
+    optional: true
+
+  '@parcel/watcher-linux-arm-musl@2.5.0':
+    optional: true
+
+  '@parcel/watcher-linux-arm64-glibc@2.5.0':
+    optional: true
+
+  '@parcel/watcher-linux-arm64-musl@2.5.0':
+    optional: true
+
+  '@parcel/watcher-linux-x64-glibc@2.5.0':
+    optional: true
+
+  '@parcel/watcher-linux-x64-musl@2.5.0':
+    optional: true
+
+  '@parcel/watcher-win32-arm64@2.5.0':
+    optional: true
+
+  '@parcel/watcher-win32-ia32@2.5.0':
+    optional: true
+
+  '@parcel/watcher-win32-x64@2.5.0':
+    optional: true
+
+  '@parcel/watcher@2.5.0':
+    dependencies:
+      detect-libc: 1.0.3
+      is-glob: 4.0.3
+      micromatch: 4.0.8
+      node-addon-api: 7.1.1
+    optionalDependencies:
+      '@parcel/watcher-android-arm64': 2.5.0
+      '@parcel/watcher-darwin-arm64': 2.5.0
+      '@parcel/watcher-darwin-x64': 2.5.0
+      '@parcel/watcher-freebsd-x64': 2.5.0
+      '@parcel/watcher-linux-arm-glibc': 2.5.0
+      '@parcel/watcher-linux-arm-musl': 2.5.0
+      '@parcel/watcher-linux-arm64-glibc': 2.5.0
+      '@parcel/watcher-linux-arm64-musl': 2.5.0
+      '@parcel/watcher-linux-x64-glibc': 2.5.0
+      '@parcel/watcher-linux-x64-musl': 2.5.0
+      '@parcel/watcher-win32-arm64': 2.5.0
+      '@parcel/watcher-win32-ia32': 2.5.0
+      '@parcel/watcher-win32-x64': 2.5.0
+    optional: true
 
   '@peculiar/asn1-android@2.3.10':
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@peculiar/asn1-ecc@2.3.8':
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       '@peculiar/asn1-x509': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@peculiar/asn1-rsa@2.3.8':
     dependencies:
       '@peculiar/asn1-schema': 2.3.8
       '@peculiar/asn1-x509': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@peculiar/asn1-schema@2.3.8':
     dependencies:
       asn1js: 3.0.5
       pvtsutils: 1.3.5
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@peculiar/asn1-x509@2.3.8':
     dependencies:
@@ -13461,7 +13867,7 @@ snapshots:
       asn1js: 3.0.5
       ipaddr.js: 2.2.0
       pvtsutils: 1.3.5
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@peertube/http-signature@1.7.0':
     dependencies:
@@ -13472,11 +13878,11 @@ snapshots:
   '@pkgjs/parseargs@0.11.0':
     optional: true
 
-  '@prisma/instrumentation@5.19.1':
+  '@prisma/instrumentation@5.22.0':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -13512,84 +13918,87 @@ snapshots:
 
   '@readme/openapi-schemas@3.1.0': {}
 
-  '@rollup/plugin-json@6.1.0(rollup@4.26.0)':
+  '@rollup/plugin-json@6.1.0(rollup@4.31.0)':
     dependencies:
-      '@rollup/pluginutils': 5.1.3(rollup@4.26.0)
+      '@rollup/pluginutils': 5.1.4(rollup@4.31.0)
     optionalDependencies:
-      rollup: 4.26.0
+      rollup: 4.31.0
 
-  '@rollup/plugin-replace@5.0.7(rollup@4.26.0)':
+  '@rollup/plugin-replace@5.0.7(rollup@4.31.0)':
     dependencies:
-      '@rollup/pluginutils': 5.1.3(rollup@4.26.0)
+      '@rollup/pluginutils': 5.1.4(rollup@4.31.0)
       magic-string: 0.30.10
     optionalDependencies:
-      rollup: 4.26.0
+      rollup: 4.31.0
 
-  '@rollup/pluginutils@5.1.3(rollup@4.26.0)':
+  '@rollup/pluginutils@5.1.4(rollup@4.31.0)':
     dependencies:
       '@types/estree': 1.0.6
       estree-walker: 2.0.2
       picomatch: 4.0.2
     optionalDependencies:
-      rollup: 4.26.0
+      rollup: 4.31.0
 
-  '@rollup/rollup-android-arm-eabi@4.26.0':
+  '@rollup/rollup-android-arm-eabi@4.31.0':
     optional: true
 
-  '@rollup/rollup-android-arm64@4.26.0':
+  '@rollup/rollup-android-arm64@4.31.0':
     optional: true
 
-  '@rollup/rollup-darwin-arm64@4.26.0':
+  '@rollup/rollup-darwin-arm64@4.31.0':
     optional: true
 
-  '@rollup/rollup-darwin-x64@4.26.0':
+  '@rollup/rollup-darwin-x64@4.31.0':
     optional: true
 
-  '@rollup/rollup-freebsd-arm64@4.26.0':
+  '@rollup/rollup-freebsd-arm64@4.31.0':
     optional: true
 
-  '@rollup/rollup-freebsd-x64@4.26.0':
+  '@rollup/rollup-freebsd-x64@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.26.0':
+  '@rollup/rollup-linux-arm-gnueabihf@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-arm-musleabihf@4.26.0':
+  '@rollup/rollup-linux-arm-musleabihf@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-arm64-gnu@4.26.0':
+  '@rollup/rollup-linux-arm64-gnu@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-arm64-musl@4.26.0':
+  '@rollup/rollup-linux-arm64-musl@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.26.0':
+  '@rollup/rollup-linux-loongarch64-gnu@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-gnu@4.26.0':
+  '@rollup/rollup-linux-powerpc64le-gnu@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-s390x-gnu@4.26.0':
+  '@rollup/rollup-linux-riscv64-gnu@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-x64-gnu@4.26.0':
+  '@rollup/rollup-linux-s390x-gnu@4.31.0':
     optional: true
 
-  '@rollup/rollup-linux-x64-musl@4.26.0':
+  '@rollup/rollup-linux-x64-gnu@4.31.0':
     optional: true
 
-  '@rollup/rollup-win32-arm64-msvc@4.26.0':
+  '@rollup/rollup-linux-x64-musl@4.31.0':
     optional: true
 
-  '@rollup/rollup-win32-ia32-msvc@4.26.0':
+  '@rollup/rollup-win32-arm64-msvc@4.31.0':
     optional: true
 
-  '@rollup/rollup-win32-x64-msvc@4.26.0':
+  '@rollup/rollup-win32-ia32-msvc@4.31.0':
+    optional: true
+
+  '@rollup/rollup-win32-x64-msvc@4.31.0':
     optional: true
 
   '@rtsao/scc@1.1.0': {}
 
-  '@rushstack/node-core-library@5.9.0(@types/node@22.9.0)':
+  '@rushstack/node-core-library@5.10.2(@types/node@22.10.7)':
     dependencies:
       ajv: 8.13.0
       ajv-draft-04: 1.0.0(ajv@8.13.0)
@@ -13600,23 +14009,23 @@ snapshots:
       resolve: 1.22.8
       semver: 7.5.4
     optionalDependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@rushstack/rig-package@0.5.3':
     dependencies:
       resolve: 1.22.8
       strip-json-comments: 3.1.1
 
-  '@rushstack/terminal@0.14.2(@types/node@22.9.0)':
+  '@rushstack/terminal@0.14.5(@types/node@22.10.7)':
     dependencies:
-      '@rushstack/node-core-library': 5.9.0(@types/node@22.9.0)
+      '@rushstack/node-core-library': 5.10.2(@types/node@22.10.7)
       supports-color: 8.1.1
     optionalDependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
-  '@rushstack/ts-command-line@4.23.0(@types/node@22.9.0)':
+  '@rushstack/ts-command-line@4.23.3(@types/node@22.10.7)':
     dependencies:
-      '@rushstack/terminal': 0.14.2(@types/node@22.9.0)
+      '@rushstack/terminal': 0.14.5(@types/node@22.10.7)
       '@types/argparse': 1.0.38
       argparse: 1.0.10
       string-argv: 0.3.1
@@ -13625,107 +14034,100 @@ snapshots:
 
   '@sec-ant/readable-stream@0.4.1': {}
 
-  '@sentry/core@8.38.0':
-    dependencies:
-      '@sentry/types': 8.38.0
-      '@sentry/utils': 8.38.0
+  '@sentry/core@8.50.0': {}
 
-  '@sentry/node@8.38.0':
+  '@sentry/node@8.50.0':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-amqplib': 0.43.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-connect': 0.40.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-dataloader': 0.12.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-express': 0.44.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-fastify': 0.41.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-fs': 0.16.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-generic-pool': 0.39.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-graphql': 0.44.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-hapi': 0.41.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-ioredis': 0.43.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-kafkajs': 0.4.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-knex': 0.41.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-koa': 0.43.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-lru-memoizer': 0.40.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-mongodb': 0.48.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-mongoose': 0.42.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-mysql': 0.41.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-mysql2': 0.41.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-nestjs-core': 0.40.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-pg': 0.44.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-redis-4': 0.42.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-tedious': 0.15.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation-undici': 0.6.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
-      '@prisma/instrumentation': 5.19.1
-      '@sentry/core': 8.38.0
-      '@sentry/opentelemetry': 8.38.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.27.0)
-      '@sentry/types': 8.38.0
-      '@sentry/utils': 8.38.0
+      '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-amqplib': 0.45.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-connect': 0.42.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-dataloader': 0.15.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-express': 0.46.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-fastify': 0.43.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-fs': 0.18.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-generic-pool': 0.42.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-graphql': 0.46.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-hapi': 0.44.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-http': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-ioredis': 0.46.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-kafkajs': 0.6.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-knex': 0.43.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-koa': 0.46.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-lru-memoizer': 0.43.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-mongodb': 0.50.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-mongoose': 0.45.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-mysql': 0.44.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-mysql2': 0.44.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-nestjs-core': 0.43.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-pg': 0.49.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-redis-4': 0.45.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-tedious': 0.17.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation-undici': 0.9.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
+      '@prisma/instrumentation': 5.22.0
+      '@sentry/core': 8.50.0
+      '@sentry/opentelemetry': 8.50.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0)
       import-in-the-middle: 1.11.2
     transitivePeerDependencies:
       - supports-color
 
-  '@sentry/opentelemetry@8.38.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.27.0)':
+  '@sentry/opentelemetry@8.50.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0)':
     dependencies:
       '@opentelemetry/api': 1.9.0
-      '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0)
-      '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0)
-      '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0)
-      '@opentelemetry/semantic-conventions': 1.27.0
-      '@sentry/core': 8.38.0
-      '@sentry/types': 8.38.0
-      '@sentry/utils': 8.38.0
+      '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/instrumentation': 0.56.0(@opentelemetry/api@1.9.0)
+      '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
+      '@opentelemetry/semantic-conventions': 1.28.0
+      '@sentry/core': 8.50.0
 
-  '@sentry/profiling-node@8.38.0':
+  '@sentry/profiling-node@8.50.0':
     dependencies:
-      '@sentry/core': 8.38.0
-      '@sentry/node': 8.38.0
-      '@sentry/types': 8.38.0
-      '@sentry/utils': 8.38.0
+      '@sentry/core': 8.50.0
+      '@sentry/node': 8.50.0
       detect-libc: 2.0.3
       node-abi: 3.62.0
     transitivePeerDependencies:
       - supports-color
 
-  '@sentry/types@8.38.0': {}
+  '@shikijs/core@1.27.2':
+    dependencies:
+      '@shikijs/engine-javascript': 1.27.2
+      '@shikijs/engine-oniguruma': 1.27.2
+      '@shikijs/types': 1.27.2
+      '@shikijs/vscode-textmate': 10.0.1
+      '@types/hast': 3.0.4
+      hast-util-to-html: 9.0.4
 
-  '@sentry/utils@8.38.0':
+  '@shikijs/engine-javascript@1.27.2':
     dependencies:
-      '@sentry/types': 8.38.0
+      '@shikijs/types': 1.27.2
+      '@shikijs/vscode-textmate': 10.0.1
+      oniguruma-to-es: 2.2.0
 
-  '@shikijs/core@1.22.2':
+  '@shikijs/engine-oniguruma@1.27.2':
     dependencies:
-      '@shikijs/engine-javascript': 1.22.2
-      '@shikijs/engine-oniguruma': 1.22.2
-      '@shikijs/types': 1.22.2
-      '@shikijs/vscode-textmate': 9.3.0
-      '@types/hast': 3.0.4
-      hast-util-to-html: 9.0.3
+      '@shikijs/types': 1.27.2
+      '@shikijs/vscode-textmate': 10.0.1
 
-  '@shikijs/engine-javascript@1.22.2':
+  '@shikijs/langs@1.27.2':
     dependencies:
-      '@shikijs/types': 1.22.2
-      '@shikijs/vscode-textmate': 9.3.0
-      oniguruma-to-js: 0.4.3
+      '@shikijs/types': 1.27.2
 
-  '@shikijs/engine-oniguruma@1.22.2':
+  '@shikijs/themes@1.27.2':
     dependencies:
-      '@shikijs/types': 1.22.2
-      '@shikijs/vscode-textmate': 9.3.0
+      '@shikijs/types': 1.27.2
 
-  '@shikijs/types@1.22.2':
+  '@shikijs/types@1.27.2':
     dependencies:
-      '@shikijs/vscode-textmate': 9.3.0
+      '@shikijs/vscode-textmate': 10.0.1
       '@types/hast': 3.0.4
 
-  '@shikijs/vscode-textmate@9.3.0': {}
+  '@shikijs/vscode-textmate@10.0.1': {}
 
   '@sideway/address@4.1.4':
     dependencies:
@@ -13739,7 +14141,7 @@ snapshots:
 
   '@sideway/pinpoint@2.0.0': {}
 
-  '@simplewebauthn/server@10.0.1(encoding@0.1.13)':
+  '@simplewebauthn/server@12.0.0(encoding@0.1.13)':
     dependencies:
       '@hexagon/base64': 1.1.27
       '@levischuck/tiny-cbor': 0.2.2
@@ -13748,21 +14150,17 @@ snapshots:
       '@peculiar/asn1-rsa': 2.3.8
       '@peculiar/asn1-schema': 2.3.8
       '@peculiar/asn1-x509': 2.3.8
-      '@simplewebauthn/types': 10.0.0
-      cross-fetch: 4.0.0(encoding@0.1.13)
+      '@simplewebauthn/types': 12.0.0
+      cross-fetch: 4.1.0(encoding@0.1.13)
     transitivePeerDependencies:
       - encoding
 
-  '@simplewebauthn/types@10.0.0': {}
-
-  '@simplewebauthn/types@11.0.0': {}
+  '@simplewebauthn/types@12.0.0': {}
 
   '@sinclair/typebox@0.27.8': {}
 
   '@sindresorhus/is@4.6.0': {}
 
-  '@sindresorhus/is@5.3.0': {}
-
   '@sindresorhus/is@7.0.1': {}
 
   '@sinonjs/commons@2.0.0':
@@ -13792,21 +14190,21 @@ snapshots:
   '@smithy/abort-controller@2.2.0':
     dependencies:
       '@smithy/types': 2.12.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/abort-controller@3.1.1':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/chunked-blob-reader-native@3.0.0':
     dependencies:
       '@smithy/util-base64': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/chunked-blob-reader@3.0.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/config-resolver@3.0.5':
     dependencies:
@@ -13814,7 +14212,7 @@ snapshots:
       '@smithy/types': 3.3.0
       '@smithy/util-config-provider': 3.0.0
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/core@2.3.1':
     dependencies:
@@ -13825,7 +14223,7 @@ snapshots:
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/credential-provider-imds@3.2.0':
     dependencies:
@@ -13833,37 +14231,37 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/types': 3.3.0
       '@smithy/url-parser': 3.0.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/eventstream-codec@3.1.2':
     dependencies:
       '@aws-crypto/crc32': 5.2.0
       '@smithy/types': 3.3.0
       '@smithy/util-hex-encoding': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/eventstream-serde-browser@3.0.5':
     dependencies:
       '@smithy/eventstream-serde-universal': 3.0.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/eventstream-serde-config-resolver@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/eventstream-serde-node@3.0.4':
     dependencies:
       '@smithy/eventstream-serde-universal': 3.0.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/eventstream-serde-universal@3.0.4':
     dependencies:
       '@smithy/eventstream-codec': 3.1.2
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/fetch-http-handler@3.2.4':
     dependencies:
@@ -13871,52 +14269,52 @@ snapshots:
       '@smithy/querystring-builder': 3.0.3
       '@smithy/types': 3.3.0
       '@smithy/util-base64': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/hash-blob-browser@3.1.2':
     dependencies:
       '@smithy/chunked-blob-reader': 3.0.0
       '@smithy/chunked-blob-reader-native': 3.0.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/hash-node@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
       '@smithy/util-buffer-from': 3.0.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/hash-stream-node@3.1.2':
     dependencies:
       '@smithy/types': 3.3.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/invalid-dependency@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/is-array-buffer@2.0.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/is-array-buffer@3.0.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/md5-js@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/middleware-content-length@3.0.5':
     dependencies:
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/middleware-endpoint@3.1.0':
     dependencies:
@@ -13926,7 +14324,7 @@ snapshots:
       '@smithy/types': 3.3.0
       '@smithy/url-parser': 3.0.3
       '@smithy/util-middleware': 3.0.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/middleware-retry@3.0.13':
     dependencies:
@@ -13937,25 +14335,25 @@ snapshots:
       '@smithy/types': 3.3.0
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-retry': 3.0.3
-      tslib: 2.7.0
+      tslib: 2.8.1
       uuid: 9.0.1
 
   '@smithy/middleware-serde@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/middleware-stack@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/node-config-provider@3.1.4':
     dependencies:
       '@smithy/property-provider': 3.1.3
       '@smithy/shared-ini-file-loader': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/node-http-handler@2.5.0':
     dependencies:
@@ -13971,39 +14369,39 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/querystring-builder': 3.0.3
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/property-provider@3.1.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/protocol-http@3.3.0':
     dependencies:
       '@smithy/types': 2.12.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/protocol-http@4.1.0':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/querystring-builder@2.2.0':
     dependencies:
       '@smithy/types': 2.12.0
       '@smithy/util-uri-escape': 2.2.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/querystring-builder@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
       '@smithy/util-uri-escape': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/querystring-parser@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/service-error-classification@3.0.3':
     dependencies:
@@ -14012,7 +14410,7 @@ snapshots:
   '@smithy/shared-ini-file-loader@3.1.4':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/signature-v4@4.1.0':
     dependencies:
@@ -14023,7 +14421,7 @@ snapshots:
       '@smithy/util-middleware': 3.0.3
       '@smithy/util-uri-escape': 3.0.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/smithy-client@3.1.11':
     dependencies:
@@ -14032,49 +14430,49 @@ snapshots:
       '@smithy/protocol-http': 4.1.0
       '@smithy/types': 3.3.0
       '@smithy/util-stream': 3.1.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/types@2.12.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/types@3.3.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/url-parser@3.0.3':
     dependencies:
       '@smithy/querystring-parser': 3.0.3
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-base64@3.0.0':
     dependencies:
       '@smithy/util-buffer-from': 3.0.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-body-length-browser@3.0.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-body-length-node@3.0.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-buffer-from@2.0.0':
     dependencies:
       '@smithy/is-array-buffer': 2.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-buffer-from@3.0.0':
     dependencies:
       '@smithy/is-array-buffer': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-config-provider@3.0.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-defaults-mode-browser@3.0.13':
     dependencies:
@@ -14082,7 +14480,7 @@ snapshots:
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
       bowser: 2.11.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-defaults-mode-node@3.0.13':
     dependencies:
@@ -14092,28 +14490,28 @@ snapshots:
       '@smithy/property-provider': 3.1.3
       '@smithy/smithy-client': 3.1.11
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-endpoints@2.0.5':
     dependencies:
       '@smithy/node-config-provider': 3.1.4
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-hex-encoding@3.0.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-middleware@3.0.3':
     dependencies:
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-retry@3.0.3':
     dependencies:
       '@smithy/service-error-classification': 3.0.3
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-stream@3.1.3':
     dependencies:
@@ -14124,178 +14522,178 @@ snapshots:
       '@smithy/util-buffer-from': 3.0.0
       '@smithy/util-hex-encoding': 3.0.0
       '@smithy/util-utf8': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-uri-escape@2.2.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-uri-escape@3.0.0':
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-utf8@2.0.0':
     dependencies:
       '@smithy/util-buffer-from': 2.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-utf8@3.0.0':
     dependencies:
       '@smithy/util-buffer-from': 3.0.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@smithy/util-waiter@3.1.2':
     dependencies:
       '@smithy/abort-controller': 3.1.1
       '@smithy/types': 3.3.0
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   '@sqltools/formatter@1.2.5': {}
 
-  '@storybook/addon-actions@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-actions@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       '@types/uuid': 9.0.8
       dequal: 2.0.3
       polished: 4.2.2
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       uuid: 9.0.1
 
-  '@storybook/addon-backgrounds@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-backgrounds@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       memoizerific: 1.11.3
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-controls@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-controls@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       dequal: 2.0.3
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-docs@8.4.4(@types/react@18.0.28)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-docs@8.5.0(@types/react@18.0.28)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@mdx-js/react': 3.0.1(@types/react@18.0.28)(react@18.3.1)
-      '@storybook/blocks': 8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/csf-plugin': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/react-dom-shim': 8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/blocks': 8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/csf-plugin': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/react-dom-shim': 8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     transitivePeerDependencies:
       - '@types/react'
 
-  '@storybook/addon-essentials@8.4.4(@types/react@18.0.28)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
-    dependencies:
-      '@storybook/addon-actions': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/addon-backgrounds': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/addon-controls': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/addon-docs': 8.4.4(@types/react@18.0.28)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/addon-highlight': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/addon-measure': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/addon-outline': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/addon-toolbars': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/addon-viewport': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+  '@storybook/addon-essentials@8.5.0(@types/react@18.0.28)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
+    dependencies:
+      '@storybook/addon-actions': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/addon-backgrounds': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/addon-controls': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/addon-docs': 8.5.0(@types/react@18.0.28)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/addon-highlight': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/addon-measure': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/addon-outline': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/addon-toolbars': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/addon-viewport': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     transitivePeerDependencies:
       - '@types/react'
 
-  '@storybook/addon-highlight@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-highlight@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/addon-interactions@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-interactions@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      '@storybook/instrumenter': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/test': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/instrumenter': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/test': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       polished: 4.2.2
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-links@8.4.4(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-links@8.5.0(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/csf': 0.1.11
+      '@storybook/csf': 0.1.12
       '@storybook/global': 5.0.0
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     optionalDependencies:
       react: 18.3.1
 
-  '@storybook/addon-mdx-gfm@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-mdx-gfm@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       remark-gfm: 4.0.0
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     transitivePeerDependencies:
       - supports-color
 
-  '@storybook/addon-measure@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-measure@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       tiny-invariant: 1.3.3
 
-  '@storybook/addon-outline@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-outline@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
 
-  '@storybook/addon-storysource@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-storysource@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/source-loader': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/source-loader': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       estraverse: 5.3.0
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       tiny-invariant: 1.3.3
 
-  '@storybook/addon-toolbars@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-toolbars@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/addon-viewport@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/addon-viewport@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       memoizerific: 1.11.3
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/blocks@8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/blocks@8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/csf': 0.1.11
+      '@storybook/csf': 0.1.12
       '@storybook/icons': 1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
     optionalDependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  '@storybook/builder-vite@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))':
+  '@storybook/builder-vite@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))':
     dependencies:
-      '@storybook/csf-plugin': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/csf-plugin': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       browser-assert: 1.2.1
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
+      vite: 6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2)
 
-  '@storybook/components@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/components@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/core-events@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/core-events@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/core@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)':
+  '@storybook/core@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)':
     dependencies:
-      '@storybook/csf': 0.1.11
+      '@storybook/csf': 0.1.12
       better-opn: 3.0.2
       browser-assert: 1.2.1
-      esbuild: 0.24.0
-      esbuild-register: 3.5.0(esbuild@0.24.0)
+      esbuild: 0.24.2
+      esbuild-register: 3.5.0(esbuild@0.24.2)
       jsdoc-type-pratt-parser: 4.1.0
       process: 0.11.10
       recast: 0.23.6
@@ -14303,18 +14701,18 @@ snapshots:
       util: 0.12.5
       ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     optionalDependencies:
-      prettier: 3.3.3
+      prettier: 3.4.2
     transitivePeerDependencies:
       - bufferutil
       - supports-color
       - utf-8-validate
 
-  '@storybook/csf-plugin@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/csf-plugin@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       unplugin: 1.4.0
 
-  '@storybook/csf@0.1.11':
+  '@storybook/csf@0.1.12':
     dependencies:
       type-fest: 2.19.0
 
@@ -14325,125 +14723,138 @@ snapshots:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  '@storybook/instrumenter@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/instrumenter@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       '@storybook/global': 5.0.0
       '@vitest/utils': 2.1.1
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/manager-api@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/manager-api@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/preview-api@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/preview-api@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/react-dom-shim@8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/react-dom-shim@8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/react-vite@8.4.4(@storybook/test@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.26.0)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))':
+  '@storybook/react-vite@8.5.0(@storybook/test@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.31.0)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(typescript@5.7.3)(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))':
     dependencies:
-      '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))
-      '@rollup/pluginutils': 5.1.3(rollup@4.26.0)
-      '@storybook/builder-vite': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))
-      '@storybook/react': 8.4.4(@storybook/test@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(typescript@5.6.3)
+      '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.3)(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))
+      '@rollup/pluginutils': 5.1.4(rollup@4.31.0)
+      '@storybook/builder-vite': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))
+      '@storybook/react': 8.5.0(@storybook/test@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(typescript@5.7.3)
       find-up: 5.0.0
       magic-string: 0.30.11
       react: 18.3.1
       react-docgen: 7.0.1
       react-dom: 18.3.1(react@18.3.1)
       resolve: 1.22.8
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       tsconfig-paths: 4.2.0
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
+      vite: 6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2)
+    optionalDependencies:
+      '@storybook/test': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
     transitivePeerDependencies:
-      - '@storybook/test'
       - rollup
       - supports-color
       - typescript
 
-  '@storybook/react@8.4.4(@storybook/test@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(typescript@5.6.3)':
+  '@storybook/react@8.5.0(@storybook/test@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(typescript@5.7.3)':
     dependencies:
-      '@storybook/components': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/components': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/preview-api': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/react-dom-shim': 8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/theming': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/manager-api': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/react-dom-shim': 8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
     optionalDependencies:
-      '@storybook/test': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      typescript: 5.6.3
+      '@storybook/test': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      typescript: 5.7.3
 
-  '@storybook/source-loader@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/source-loader@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/csf': 0.1.11
+      '@storybook/csf': 0.1.12
       es-toolkit: 1.27.0
       estraverse: 5.3.0
-      prettier: 3.3.3
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      prettier: 3.4.2
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/test@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/test@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      '@storybook/csf': 0.1.11
+      '@storybook/csf': 0.1.12
       '@storybook/global': 5.0.0
-      '@storybook/instrumenter': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/instrumenter': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@testing-library/dom': 10.4.0
       '@testing-library/jest-dom': 6.5.0
       '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0)
       '@vitest/expect': 2.0.5
       '@vitest/spy': 2.0.5
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/theming@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/theming@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/types@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))':
+  '@storybook/types@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))':
     dependencies:
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
 
-  '@storybook/vue3-vite@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))':
+  '@storybook/vue3-vite@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
-      '@storybook/builder-vite': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))
-      '@storybook/vue3': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(vue@3.5.12(typescript@5.6.3))
+      '@storybook/builder-vite': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))
+      '@storybook/vue3': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(vue@3.5.13(typescript@5.7.3))
       find-package-json: 1.2.0
       magic-string: 0.30.11
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
-      typescript: 5.6.3
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
-      vue-component-meta: 2.0.16(typescript@5.6.3)
-      vue-docgen-api: 4.75.1(vue@3.5.12(typescript@5.6.3))
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
+      typescript: 5.7.3
+      vite: 6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2)
+      vue-component-meta: 2.0.16(typescript@5.7.3)
+      vue-docgen-api: 4.75.1(vue@3.5.13(typescript@5.7.3))
     transitivePeerDependencies:
       - vue
 
-  '@storybook/vue3@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))(vue@3.5.12(typescript@5.6.3))':
+  '@storybook/vue3@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
-      '@storybook/components': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/components': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/preview-api': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/theming': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/manager-api': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
       '@vue/compiler-core': 3.5.12
-      storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      storybook: 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
       ts-dedent: 2.2.0
       type-fest: 2.19.0
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.13(typescript@5.7.3)
       vue-component-type-helpers: 2.2.0
 
+  '@stylistic/eslint-plugin@2.13.0(eslint@9.18.0)(typescript@5.7.3)':
+    dependencies:
+      '@typescript-eslint/utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3)
+      eslint: 9.18.0
+      eslint-visitor-keys: 4.2.0
+      espree: 10.3.0
+      estraverse: 5.3.0
+      picomatch: 4.0.2
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
   '@swc/cli@0.3.12(@swc/core@1.9.2)(chokidar@3.5.3)':
     dependencies:
       '@mole-inc/bin-wrapper': 8.0.1
       '@swc/core': 1.9.2
       '@swc/counter': 0.1.3
       commander: 8.3.0
-      fast-glob: 3.3.2
+      fast-glob: 3.3.3
       minimatch: 9.0.3
       piscina: 4.4.0
       semver: 7.6.0
@@ -14568,11 +14979,11 @@ snapshots:
     dependencies:
       defer-to-connect: 2.0.1
 
-  '@tabler/icons-webfont@https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz':
+  '@tabler/icons-webfont@https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz':
     dependencies:
-      '@tabler/icons': 3.29.0
+      '@tabler/icons': 3.30.0
 
-  '@tabler/icons@3.29.0': {}
+  '@tabler/icons@3.30.0': {}
 
   '@tensorflow/tfjs-backend-cpu@4.4.0(@tensorflow/tfjs-core@4.4.0(encoding@0.1.13))':
     dependencies:
@@ -14689,14 +15100,14 @@ snapshots:
     dependencies:
       '@testing-library/dom': 10.4.0
 
-  '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.12)(@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))':
+  '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
       '@babel/runtime': 7.23.4
       '@testing-library/dom': 9.3.4
-      '@vue/test-utils': 2.4.1(@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))
-      vue: 3.5.12(typescript@5.6.3)
+      '@vue/test-utils': 2.4.1(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
+      vue: 3.5.13(typescript@5.7.3)
     optionalDependencies:
-      '@vue/compiler-sfc': 3.5.12
+      '@vue/compiler-sfc': 3.5.13
     transitivePeerDependencies:
       - '@vue/server-renderer'
 
@@ -14714,7 +15125,7 @@ snapshots:
 
   '@types/accepts@1.3.7':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/archiver@6.0.3':
     dependencies:
@@ -14750,7 +15161,7 @@ snapshots:
   '@types/body-parser@1.19.5':
     dependencies:
       '@types/connect': 3.4.35
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/braces@3.0.1': {}
 
@@ -14758,7 +15169,7 @@ snapshots:
     dependencies:
       '@types/http-cache-semantics': 4.0.4
       '@types/keyv': 3.1.4
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       '@types/responselike': 1.0.0
 
   '@types/canvas-confetti@1.6.4': {}
@@ -14771,11 +15182,11 @@ snapshots:
 
   '@types/connect@3.4.35':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/connect@3.4.36':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/content-disposition@0.5.8': {}
 
@@ -14798,7 +15209,7 @@ snapshots:
 
   '@types/express-serve-static-core@4.17.33':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       '@types/qs': 6.9.7
       '@types/range-parser': 1.2.4
 
@@ -14811,16 +15222,13 @@ snapshots:
 
   '@types/fluent-ffmpeg@2.1.27':
     dependencies:
-      '@types/node': 22.9.0
-
-  '@types/glob@7.2.0':
-    dependencies:
-      '@types/minimatch': 5.1.2
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/graceful-fs@4.1.6':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
+
+  '@types/hammerjs@2.0.46': {}
 
   '@types/hast@3.0.4':
     dependencies:
@@ -14832,7 +15240,7 @@ snapshots:
 
   '@types/http-link-header@1.0.7':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/istanbul-lib-coverage@2.0.4': {}
 
@@ -14853,23 +15261,21 @@ snapshots:
 
   '@types/jsdom@21.1.7':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       '@types/tough-cookie': 4.0.2
       parse5: 7.2.1
 
-  '@types/json-schema@7.0.12': {}
-
   '@types/json-schema@7.0.15': {}
 
   '@types/json5@0.0.29': {}
 
   '@types/jsonld@1.5.15': {}
 
-  '@types/jsrsasign@10.5.14': {}
+  '@types/jsrsasign@10.5.15': {}
 
   '@types/keyv@3.1.4':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/long@4.0.2': {}
 
@@ -14889,28 +15295,30 @@ snapshots:
 
   '@types/mime@3.0.1': {}
 
-  '@types/minimatch@5.1.2': {}
-
   '@types/minimist@1.2.2': {}
 
   '@types/ms@0.7.34': {}
 
   '@types/mysql@2.15.26':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/node-fetch@2.6.11':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       form-data: 4.0.1
 
+  '@types/node@22.10.7':
+    dependencies:
+      undici-types: 6.20.0
+
   '@types/node@22.9.0':
     dependencies:
       undici-types: 6.19.8
 
-  '@types/nodemailer@6.4.16':
+  '@types/nodemailer@6.4.17':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/normalize-package-data@2.4.1': {}
 
@@ -14921,11 +15329,11 @@ snapshots:
   '@types/oauth2orize@1.11.5':
     dependencies:
       '@types/express': 4.17.17
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/oauth@0.9.6':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/offscreencanvas@2019.3.0': {}
 
@@ -14937,13 +15345,13 @@ snapshots:
 
   '@types/pg@8.11.10':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       pg-protocol: 1.6.1
       pg-types: 4.0.1
 
   '@types/pg@8.6.1':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       pg-protocol: 1.7.0
       pg-types: 2.2.0
 
@@ -14955,7 +15363,7 @@ snapshots:
 
   '@types/qrcode@1.5.5':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/qs@6.9.7': {}
 
@@ -14973,7 +15381,7 @@ snapshots:
 
   '@types/readdir-glob@1.1.1':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/rename@1.0.7': {}
 
@@ -14981,7 +15389,7 @@ snapshots:
 
   '@types/responselike@1.0.0':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/sanitize-html@2.13.0':
     dependencies:
@@ -14998,12 +15406,10 @@ snapshots:
   '@types/serve-static@1.15.1':
     dependencies:
       '@types/mime': 3.0.1
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/serviceworker@0.0.67': {}
 
-  '@types/shimmer@1.0.5': {}
-
   '@types/shimmer@1.2.0': {}
 
   '@types/simple-oauth2@5.0.7': {}
@@ -15024,7 +15430,7 @@ snapshots:
 
   '@types/tedious@4.0.14':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/throttle-debounce@5.0.2': {}
 
@@ -15044,17 +15450,17 @@ snapshots:
 
   '@types/vary@1.1.3':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/web-push@3.6.4':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/webgl-ext@0.0.30': {}
 
   '@types/ws@8.5.13':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
 
   '@types/yargs-parser@21.0.0': {}
 
@@ -15064,38 +15470,18 @@ snapshots:
 
   '@types/yauzl@2.10.0':
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
     optional: true
 
-  '@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)':
-    dependencies:
-      '@eslint-community/regexpp': 4.6.2
-      '@typescript-eslint/parser': 7.1.0(eslint@9.14.0)(typescript@5.6.3)
-      '@typescript-eslint/scope-manager': 7.1.0
-      '@typescript-eslint/type-utils': 7.1.0(eslint@9.14.0)(typescript@5.6.3)
-      '@typescript-eslint/utils': 7.1.0(eslint@9.14.0)(typescript@5.6.3)
-      '@typescript-eslint/visitor-keys': 7.1.0
-      debug: 4.3.4
-      eslint: 9.14.0
-      graphemer: 1.4.0
-      ignore: 5.2.4
-      natural-compare: 1.4.0
-      semver: 7.6.0
-      ts-api-utils: 1.0.1(typescript@5.6.3)
-    optionalDependencies:
-      typescript: 5.6.3
-    transitivePeerDependencies:
-      - supports-color
-
-  '@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)':
+  '@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.18.0)(typescript@5.6.3))(eslint@9.18.0)(typescript@5.6.3)':
     dependencies:
       '@eslint-community/regexpp': 4.11.0
-      '@typescript-eslint/parser': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/parser': 7.17.0(eslint@9.18.0)(typescript@5.6.3)
       '@typescript-eslint/scope-manager': 7.17.0
-      '@typescript-eslint/type-utils': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
-      '@typescript-eslint/utils': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/type-utils': 7.17.0(eslint@9.18.0)(typescript@5.6.3)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.18.0)(typescript@5.6.3)
       '@typescript-eslint/visitor-keys': 7.17.0
-      eslint: 9.14.0
+      eslint: 9.18.0
       graphemer: 1.4.0
       ignore: 5.3.1
       natural-compare: 1.4.0
@@ -15105,85 +15491,85 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@7.1.0(eslint@9.14.0)(typescript@5.6.3)':
+  '@typescript-eslint/eslint-plugin@8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/scope-manager': 7.1.0
-      '@typescript-eslint/types': 7.1.0
-      '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.6.3)
-      '@typescript-eslint/visitor-keys': 7.1.0
-      debug: 4.3.4
-      eslint: 9.14.0
-    optionalDependencies:
-      typescript: 5.6.3
+      '@eslint-community/regexpp': 4.12.1
+      '@typescript-eslint/parser': 8.20.0(eslint@9.18.0)(typescript@5.7.3)
+      '@typescript-eslint/scope-manager': 8.20.0
+      '@typescript-eslint/type-utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3)
+      '@typescript-eslint/utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3)
+      '@typescript-eslint/visitor-keys': 8.20.0
+      eslint: 9.18.0
+      graphemer: 1.4.0
+      ignore: 5.3.1
+      natural-compare: 1.4.0
+      ts-api-utils: 2.0.0(typescript@5.7.3)
+      typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3)':
+  '@typescript-eslint/parser@7.17.0(eslint@9.18.0)(typescript@5.6.3)':
     dependencies:
       '@typescript-eslint/scope-manager': 7.17.0
       '@typescript-eslint/types': 7.17.0
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.3)
       '@typescript-eslint/visitor-keys': 7.17.0
       debug: 4.3.5
-      eslint: 9.14.0
+      eslint: 9.18.0
     optionalDependencies:
       typescript: 5.6.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/scope-manager@7.1.0':
+  '@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/types': 7.1.0
-      '@typescript-eslint/visitor-keys': 7.1.0
+      '@typescript-eslint/scope-manager': 8.20.0
+      '@typescript-eslint/types': 8.20.0
+      '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3)
+      '@typescript-eslint/visitor-keys': 8.20.0
+      debug: 4.3.7(supports-color@8.1.1)
+      eslint: 9.18.0
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
 
   '@typescript-eslint/scope-manager@7.17.0':
     dependencies:
       '@typescript-eslint/types': 7.17.0
       '@typescript-eslint/visitor-keys': 7.17.0
 
-  '@typescript-eslint/type-utils@7.1.0(eslint@9.14.0)(typescript@5.6.3)':
+  '@typescript-eslint/scope-manager@8.20.0':
     dependencies:
-      '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.6.3)
-      '@typescript-eslint/utils': 7.1.0(eslint@9.14.0)(typescript@5.6.3)
-      debug: 4.3.7(supports-color@8.1.1)
-      eslint: 9.14.0
-      ts-api-utils: 1.0.1(typescript@5.6.3)
-    optionalDependencies:
-      typescript: 5.6.3
-    transitivePeerDependencies:
-      - supports-color
+      '@typescript-eslint/types': 8.20.0
+      '@typescript-eslint/visitor-keys': 8.20.0
 
-  '@typescript-eslint/type-utils@7.17.0(eslint@9.14.0)(typescript@5.6.3)':
+  '@typescript-eslint/type-utils@7.17.0(eslint@9.18.0)(typescript@5.6.3)':
     dependencies:
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.3)
-      '@typescript-eslint/utils': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/utils': 7.17.0(eslint@9.18.0)(typescript@5.6.3)
       debug: 4.3.7(supports-color@8.1.1)
-      eslint: 9.14.0
+      eslint: 9.18.0
       ts-api-utils: 1.3.0(typescript@5.6.3)
     optionalDependencies:
       typescript: 5.6.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/types@7.1.0': {}
-
-  '@typescript-eslint/types@7.17.0': {}
-
-  '@typescript-eslint/typescript-estree@7.1.0(typescript@5.6.3)':
+  '@typescript-eslint/type-utils@8.20.0(eslint@9.18.0)(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/types': 7.1.0
-      '@typescript-eslint/visitor-keys': 7.1.0
+      '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3)
+      '@typescript-eslint/utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3)
       debug: 4.3.7(supports-color@8.1.1)
-      globby: 11.1.0
-      is-glob: 4.0.3
-      minimatch: 9.0.3
-      semver: 7.6.3
-      ts-api-utils: 1.0.1(typescript@5.6.3)
-    optionalDependencies:
-      typescript: 5.6.3
+      eslint: 9.18.0
+      ts-api-utils: 2.0.0(typescript@5.7.3)
+      typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
+  '@typescript-eslint/types@7.17.0': {}
+
+  '@typescript-eslint/types@8.20.0': {}
+
   '@typescript-eslint/typescript-estree@7.17.0(typescript@5.6.3)':
     dependencies:
       '@typescript-eslint/types': 7.17.0
@@ -15199,73 +15585,60 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@7.1.0(eslint@9.14.0)(typescript@5.6.3)':
+  '@typescript-eslint/typescript-estree@8.20.0(typescript@5.7.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.14.0)
-      '@types/json-schema': 7.0.12
-      '@types/semver': 7.5.8
-      '@typescript-eslint/scope-manager': 7.1.0
-      '@typescript-eslint/types': 7.1.0
-      '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.6.3)
-      eslint: 9.14.0
+      '@typescript-eslint/types': 8.20.0
+      '@typescript-eslint/visitor-keys': 8.20.0
+      debug: 4.3.7(supports-color@8.1.1)
+      fast-glob: 3.3.3
+      is-glob: 4.0.3
+      minimatch: 9.0.4
       semver: 7.6.3
+      ts-api-utils: 2.0.0(typescript@5.7.3)
+      typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
-      - typescript
 
-  '@typescript-eslint/utils@7.17.0(eslint@9.14.0)(typescript@5.6.3)':
+  '@typescript-eslint/utils@7.17.0(eslint@9.18.0)(typescript@5.6.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.14.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.18.0)
       '@typescript-eslint/scope-manager': 7.17.0
       '@typescript-eslint/types': 7.17.0
       '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.3)
-      eslint: 9.14.0
+      eslint: 9.18.0
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  '@typescript-eslint/visitor-keys@7.1.0':
+  '@typescript-eslint/utils@8.20.0(eslint@9.18.0)(typescript@5.7.3)':
     dependencies:
-      '@typescript-eslint/types': 7.1.0
-      eslint-visitor-keys: 3.4.3
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.18.0)
+      '@typescript-eslint/scope-manager': 8.20.0
+      '@typescript-eslint/types': 8.20.0
+      '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3)
+      eslint: 9.18.0
+      typescript: 5.7.3
+    transitivePeerDependencies:
+      - supports-color
 
   '@typescript-eslint/visitor-keys@7.17.0':
     dependencies:
       '@typescript-eslint/types': 7.17.0
       eslint-visitor-keys: 3.4.3
 
-  '@ungap/structured-clone@1.2.0': {}
-
-  '@vitejs/plugin-vue@5.2.0(vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))':
+  '@typescript-eslint/visitor-keys@8.20.0':
     dependencies:
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
-      vue: 3.5.12(typescript@5.6.3)
+      '@typescript-eslint/types': 8.20.0
+      eslint-visitor-keys: 4.2.0
 
-  '@vitejs/plugin-vue@5.2.0(vite@5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))':
-    dependencies:
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0)
-      vue: 3.5.12(typescript@5.6.3)
+  '@ungap/structured-clone@1.2.0': {}
 
-  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.36.0))':
+  '@vitejs/plugin-vue@5.2.1(vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
-      '@ampproject/remapping': 2.2.1
-      '@bcoe/v8-coverage': 0.2.3
-      debug: 4.3.5
-      istanbul-lib-coverage: 3.2.2
-      istanbul-lib-report: 3.0.1
-      istanbul-lib-source-maps: 5.0.4
-      istanbul-reports: 3.1.6
-      magic-string: 0.30.10
-      magicast: 0.3.4
-      picocolors: 1.0.1
-      std-env: 3.7.0
-      strip-literal: 2.1.0
-      test-exclude: 6.0.0
-      vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.36.0)
-    transitivePeerDependencies:
-      - supports-color
+      vite: 6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2)
+      vue: 3.5.13(typescript@5.7.3)
 
-  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))':
+  '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0))':
     dependencies:
       '@ampproject/remapping': 2.2.1
       '@bcoe/v8-coverage': 0.2.3
@@ -15280,7 +15653,7 @@ snapshots:
       std-env: 3.7.0
       strip-literal: 2.1.0
       test-exclude: 6.0.0
-      vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0)
+      vitest: 1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -15349,53 +15722,53 @@ snapshots:
     dependencies:
       '@volar/source-map': 2.2.0
 
-  '@volar/language-core@2.4.10':
+  '@volar/language-core@2.4.11':
     dependencies:
-      '@volar/source-map': 2.4.10
+      '@volar/source-map': 2.4.11
 
   '@volar/source-map@2.2.0':
     dependencies:
       muggle-string: 0.4.1
 
-  '@volar/source-map@2.4.10': {}
+  '@volar/source-map@2.4.11': {}
 
   '@volar/typescript@2.2.0':
     dependencies:
       '@volar/language-core': 2.2.0
       path-browserify: 1.0.1
 
-  '@volar/typescript@2.4.10':
+  '@volar/typescript@2.4.11':
     dependencies:
-      '@volar/language-core': 2.4.10
+      '@volar/language-core': 2.4.11
       path-browserify: 1.0.1
       vscode-uri: 3.0.8
 
-  '@vue/compiler-core@3.5.11':
+  '@vue/compiler-core@3.5.12':
     dependencies:
       '@babel/parser': 7.25.6
-      '@vue/shared': 3.5.11
+      '@vue/shared': 3.5.12
       entities: 4.5.0
       estree-walker: 2.0.2
       source-map-js: 1.2.1
 
-  '@vue/compiler-core@3.5.12':
+  '@vue/compiler-core@3.5.13':
     dependencies:
       '@babel/parser': 7.25.6
-      '@vue/shared': 3.5.12
+      '@vue/shared': 3.5.13
       entities: 4.5.0
       estree-walker: 2.0.2
       source-map-js: 1.2.1
 
-  '@vue/compiler-dom@3.5.11':
-    dependencies:
-      '@vue/compiler-core': 3.5.11
-      '@vue/shared': 3.5.11
-
   '@vue/compiler-dom@3.5.12':
     dependencies:
       '@vue/compiler-core': 3.5.12
       '@vue/shared': 3.5.12
 
+  '@vue/compiler-dom@3.5.13':
+    dependencies:
+      '@vue/compiler-core': 3.5.13
+      '@vue/shared': 3.5.13
+
   '@vue/compiler-sfc@3.5.12':
     dependencies:
       '@babel/parser': 7.25.6
@@ -15405,7 +15778,19 @@ snapshots:
       '@vue/shared': 3.5.12
       estree-walker: 2.0.2
       magic-string: 0.30.11
-      postcss: 8.4.49
+      postcss: 8.5.1
+      source-map-js: 1.2.1
+
+  '@vue/compiler-sfc@3.5.13':
+    dependencies:
+      '@babel/parser': 7.25.6
+      '@vue/compiler-core': 3.5.13
+      '@vue/compiler-dom': 3.5.13
+      '@vue/compiler-ssr': 3.5.13
+      '@vue/shared': 3.5.13
+      estree-walker: 2.0.2
+      magic-string: 0.30.11
+      postcss: 8.5.1
       source-map-js: 1.2.1
 
   '@vue/compiler-ssr@3.5.12':
@@ -15413,12 +15798,17 @@ snapshots:
       '@vue/compiler-dom': 3.5.12
       '@vue/shared': 3.5.12
 
+  '@vue/compiler-ssr@3.5.13':
+    dependencies:
+      '@vue/compiler-dom': 3.5.13
+      '@vue/shared': 3.5.13
+
   '@vue/compiler-vue2@2.7.16':
     dependencies:
       de-indent: 1.0.2
       he: 1.2.0
 
-  '@vue/language-core@2.0.16(typescript@5.6.3)':
+  '@vue/language-core@2.0.16(typescript@5.7.3)':
     dependencies:
       '@volar/language-core': 2.2.0
       '@vue/compiler-dom': 3.5.12
@@ -15428,30 +15818,39 @@ snapshots:
       path-browserify: 1.0.1
       vue-template-compiler: 2.7.14
     optionalDependencies:
-      typescript: 5.6.3
+      typescript: 5.7.3
 
-  '@vue/language-core@2.1.10(typescript@5.6.3)':
+  '@vue/language-core@2.2.0(typescript@5.7.3)':
     dependencies:
-      '@volar/language-core': 2.4.10
-      '@vue/compiler-dom': 3.5.11
+      '@volar/language-core': 2.4.11
+      '@vue/compiler-dom': 3.5.12
       '@vue/compiler-vue2': 2.7.16
-      '@vue/shared': 3.5.11
-      alien-signals: 0.2.1
+      '@vue/shared': 3.5.12
+      alien-signals: 0.4.14
       minimatch: 9.0.4
       muggle-string: 0.4.1
       path-browserify: 1.0.1
     optionalDependencies:
-      typescript: 5.6.3
+      typescript: 5.7.3
 
   '@vue/reactivity@3.5.12':
     dependencies:
       '@vue/shared': 3.5.12
 
+  '@vue/reactivity@3.5.13':
+    dependencies:
+      '@vue/shared': 3.5.13
+
   '@vue/runtime-core@3.5.12':
     dependencies:
       '@vue/reactivity': 3.5.12
       '@vue/shared': 3.5.12
 
+  '@vue/runtime-core@3.5.13':
+    dependencies:
+      '@vue/reactivity': 3.5.13
+      '@vue/shared': 3.5.13
+
   '@vue/runtime-dom@3.5.12':
     dependencies:
       '@vue/reactivity': 3.5.12
@@ -15459,23 +15858,36 @@ snapshots:
       '@vue/shared': 3.5.12
       csstype: 3.1.3
 
+  '@vue/runtime-dom@3.5.13':
+    dependencies:
+      '@vue/reactivity': 3.5.13
+      '@vue/runtime-core': 3.5.13
+      '@vue/shared': 3.5.13
+      csstype: 3.1.3
+
   '@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3))':
     dependencies:
       '@vue/compiler-ssr': 3.5.12
       '@vue/shared': 3.5.12
       vue: 3.5.12(typescript@5.6.3)
 
-  '@vue/shared@3.5.11': {}
+  '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.3))':
+    dependencies:
+      '@vue/compiler-ssr': 3.5.13
+      '@vue/shared': 3.5.13
+      vue: 3.5.13(typescript@5.7.3)
 
   '@vue/shared@3.5.12': {}
 
-  '@vue/test-utils@2.4.1(@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))':
+  '@vue/shared@3.5.13': {}
+
+  '@vue/test-utils@2.4.1(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
       js-beautify: 1.14.9
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.13(typescript@5.7.3)
       vue-component-type-helpers: 1.8.4
     optionalDependencies:
-      '@vue/server-renderer': 3.5.12(vue@3.5.12(typescript@5.6.3))
+      '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.7.3))
 
   '@webgpu/types@0.1.30': {}
 
@@ -15529,6 +15941,8 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  agent-base@7.1.3: {}
+
   aggregate-error@3.1.0:
     dependencies:
       clean-stack: 2.2.0
@@ -15588,7 +16002,7 @@ snapshots:
       json-schema-traverse: 1.0.0
       require-from-string: 2.0.2
 
-  alien-signals@0.2.1: {}
+  alien-signals@0.4.14: {}
 
   ansi-colors@4.1.3: {}
 
@@ -15755,7 +16169,7 @@ snapshots:
     dependencies:
       pvtsutils: 1.3.5
       pvutils: 1.1.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   assert-never@1.2.1: {}
 
@@ -15767,7 +16181,7 @@ snapshots:
 
   ast-types@0.16.1:
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   astral-regex@2.0.0: {}
 
@@ -15814,9 +16228,9 @@ snapshots:
     transitivePeerDependencies:
       - debug
 
-  axios@1.7.7(debug@4.3.7):
+  axios@1.7.9(debug@4.4.0):
     dependencies:
-      follow-redirects: 1.15.9(debug@4.3.7)
+      follow-redirects: 1.15.9(debug@4.4.0)
       form-data: 4.0.1
       proxy-from-env: 1.1.0
     transitivePeerDependencies:
@@ -15969,13 +16383,6 @@ snapshots:
 
   browser-assert@1.2.1: {}
 
-  browserslist@4.22.2:
-    dependencies:
-      caniuse-lite: 1.0.30001566
-      electron-to-chromium: 1.4.601
-      node-releases: 2.0.14
-      update-browserslist-db: 1.0.13(browserslist@4.22.2)
-
   browserslist@4.23.0:
     dependencies:
       caniuse-lite: 1.0.30001591
@@ -15983,6 +16390,13 @@ snapshots:
       node-releases: 2.0.14
       update-browserslist-db: 1.0.13(browserslist@4.23.0)
 
+  browserslist@4.24.4:
+    dependencies:
+      caniuse-lite: 1.0.30001695
+      electron-to-chromium: 1.5.83
+      node-releases: 2.0.19
+      update-browserslist-db: 1.1.2(browserslist@4.24.4)
+
   bser@2.1.1:
     dependencies:
       node-int64: 0.4.0
@@ -16020,10 +16434,10 @@ snapshots:
       node-gyp-build: 4.6.0
     optional: true
 
-  bullmq@5.26.1:
+  bullmq@5.34.10:
     dependencies:
-      cron-parser: 4.8.1
-      ioredis: 5.4.1
+      cron-parser: 4.9.0
+      ioredis: 5.4.2
       msgpackr: 1.11.2
       node-abort-controller: 3.1.1
       semver: 7.6.3
@@ -16061,16 +16475,6 @@ snapshots:
 
   cacheable-lookup@7.0.0: {}
 
-  cacheable-request@10.2.14:
-    dependencies:
-      '@types/http-cache-semantics': 4.0.4
-      get-stream: 6.0.1
-      http-cache-semantics: 4.1.1
-      keyv: 4.5.4
-      mimic-response: 4.0.0
-      normalize-url: 8.0.1
-      responselike: 3.0.0
-
   cacheable-request@12.0.1:
     dependencies:
       '@types/http-cache-semantics': 4.0.4
@@ -16122,15 +16526,15 @@ snapshots:
 
   caniuse-api@3.0.0:
     dependencies:
-      browserslist: 4.23.0
+      browserslist: 4.24.4
       caniuse-lite: 1.0.30001591
       lodash.memoize: 4.1.2
       lodash.uniq: 4.5.0
 
-  caniuse-lite@1.0.30001566: {}
-
   caniuse-lite@1.0.30001591: {}
 
+  caniuse-lite@1.0.30001695: {}
+
   canonicalize@1.0.8: {}
 
   canvas-confetti@1.9.3: {}
@@ -16163,7 +16567,7 @@ snapshots:
 
   chalk-template@1.1.0:
     dependencies:
-      chalk: 5.3.0
+      chalk: 5.4.1
 
   chalk@2.4.2:
     dependencies:
@@ -16181,7 +16585,7 @@ snapshots:
       ansi-styles: 4.3.0
       supports-color: 7.2.0
 
-  chalk@5.3.0: {}
+  chalk@5.4.1: {}
 
   char-regex@1.0.2: {}
 
@@ -16195,26 +16599,27 @@ snapshots:
     dependencies:
       is-regex: 1.1.4
 
-  chart.js@4.4.6:
+  chart.js@4.4.7:
     dependencies:
       '@kurkle/color': 0.3.2
 
-  chartjs-adapter-date-fns@3.0.0(chart.js@4.4.6)(date-fns@2.30.0):
+  chartjs-adapter-date-fns@3.0.0(chart.js@4.4.7)(date-fns@2.30.0):
     dependencies:
-      chart.js: 4.4.6
+      chart.js: 4.4.7
       date-fns: 2.30.0
 
-  chartjs-chart-matrix@2.0.1(chart.js@4.4.6):
+  chartjs-chart-matrix@2.0.1(chart.js@4.4.7):
     dependencies:
-      chart.js: 4.4.6
+      chart.js: 4.4.7
 
-  chartjs-plugin-gradient@0.6.1(chart.js@4.4.6):
+  chartjs-plugin-gradient@0.6.1(chart.js@4.4.7):
     dependencies:
-      chart.js: 4.4.6
+      chart.js: 4.4.7
 
-  chartjs-plugin-zoom@2.0.1(chart.js@4.4.6):
+  chartjs-plugin-zoom@2.2.0(chart.js@4.4.7):
     dependencies:
-      chart.js: 4.4.6
+      '@types/hammerjs': 2.0.46
+      chart.js: 4.4.7
       hammerjs: 2.0.8
 
   check-error@1.0.3:
@@ -16232,7 +16637,7 @@ snapshots:
       css-what: 6.1.0
       domelementtype: 2.3.0
       domhandler: 5.0.3
-      domutils: 3.0.1
+      domutils: 3.1.0
 
   cheerio@1.0.0:
     dependencies:
@@ -16248,16 +16653,6 @@ snapshots:
       undici: 6.19.8
       whatwg-mimetype: 4.0.0
 
-  cheerio@1.0.0-rc.12:
-    dependencies:
-      cheerio-select: 2.1.0
-      dom-serializer: 2.0.0
-      domhandler: 5.0.3
-      domutils: 3.0.1
-      htmlparser2: 8.0.1
-      parse5: 7.2.1
-      parse5-htmlparser2-tree-adapter: 7.0.0
-
   chokidar@3.5.3:
     dependencies:
       anymatch: 3.1.3
@@ -16275,7 +16670,7 @@ snapshots:
 
   chownr@2.0.0: {}
 
-  chromatic@11.18.1: {}
+  chromatic@11.25.0: {}
 
   ci-info@3.7.1: {}
 
@@ -16422,7 +16817,7 @@ snapshots:
       ini: 1.3.8
       proto-list: 1.2.4
 
-  consola@2.15.3: {}
+  consola@3.4.0: {}
 
   console-control-strings@1.1.0:
     optional: true
@@ -16468,13 +16863,13 @@ snapshots:
       crc-32: 1.2.2
       readable-stream: 4.3.0
 
-  create-jest@29.7.0(@types/node@22.9.0):
+  create-jest@29.7.0(@types/node@22.10.7):
     dependencies:
       '@jest/types': 29.6.3
       chalk: 4.1.2
       exit: 0.1.2
       graceful-fs: 4.2.11
-      jest-config: 29.7.0(@types/node@22.9.0)
+      jest-config: 29.7.0(@types/node@22.10.7)
       jest-util: 29.7.0
       prompts: 2.4.2
     transitivePeerDependencies:
@@ -16483,7 +16878,7 @@ snapshots:
       - supports-color
       - ts-node
 
-  cron-parser@4.8.1:
+  cron-parser@4.9.0:
     dependencies:
       luxon: 3.3.0
 
@@ -16502,7 +16897,7 @@ snapshots:
     transitivePeerDependencies:
       - encoding
 
-  cross-fetch@4.0.0(encoding@0.1.13):
+  cross-fetch@4.1.0(encoding@0.1.13):
     dependencies:
       node-fetch: 2.7.0(encoding@0.1.13)
     transitivePeerDependencies:
@@ -16520,16 +16915,22 @@ snapshots:
       shebang-command: 2.0.0
       which: 2.0.2
 
-  css-declaration-sorter@7.2.0(postcss@8.4.49):
+  cross-spawn@7.0.6:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  css-declaration-sorter@7.2.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
 
   css-select@5.1.0:
     dependencies:
       boolbase: 1.0.0
       css-what: 6.1.0
       domhandler: 5.0.3
-      domutils: 3.0.1
+      domutils: 3.1.0
       nth-check: 2.1.1
 
   css-tree@2.2.1:
@@ -16548,61 +16949,62 @@ snapshots:
 
   cssesc@3.0.0: {}
 
-  cssnano-preset-default@6.1.2(postcss@8.4.49):
-    dependencies:
-      browserslist: 4.23.0
-      css-declaration-sorter: 7.2.0(postcss@8.4.49)
-      cssnano-utils: 4.0.2(postcss@8.4.49)
-      postcss: 8.4.49
-      postcss-calc: 9.0.1(postcss@8.4.49)
-      postcss-colormin: 6.1.0(postcss@8.4.49)
-      postcss-convert-values: 6.1.0(postcss@8.4.49)
-      postcss-discard-comments: 6.0.2(postcss@8.4.49)
-      postcss-discard-duplicates: 6.0.3(postcss@8.4.49)
-      postcss-discard-empty: 6.0.3(postcss@8.4.49)
-      postcss-discard-overridden: 6.0.2(postcss@8.4.49)
-      postcss-merge-longhand: 6.0.5(postcss@8.4.49)
-      postcss-merge-rules: 6.1.1(postcss@8.4.49)
-      postcss-minify-font-values: 6.1.0(postcss@8.4.49)
-      postcss-minify-gradients: 6.0.3(postcss@8.4.49)
-      postcss-minify-params: 6.1.0(postcss@8.4.49)
-      postcss-minify-selectors: 6.0.4(postcss@8.4.49)
-      postcss-normalize-charset: 6.0.2(postcss@8.4.49)
-      postcss-normalize-display-values: 6.0.2(postcss@8.4.49)
-      postcss-normalize-positions: 6.0.2(postcss@8.4.49)
-      postcss-normalize-repeat-style: 6.0.2(postcss@8.4.49)
-      postcss-normalize-string: 6.0.2(postcss@8.4.49)
-      postcss-normalize-timing-functions: 6.0.2(postcss@8.4.49)
-      postcss-normalize-unicode: 6.1.0(postcss@8.4.49)
-      postcss-normalize-url: 6.0.2(postcss@8.4.49)
-      postcss-normalize-whitespace: 6.0.2(postcss@8.4.49)
-      postcss-ordered-values: 6.0.2(postcss@8.4.49)
-      postcss-reduce-initial: 6.1.0(postcss@8.4.49)
-      postcss-reduce-transforms: 6.0.2(postcss@8.4.49)
-      postcss-svgo: 6.0.3(postcss@8.4.49)
-      postcss-unique-selectors: 6.0.4(postcss@8.4.49)
-
-  cssnano-utils@4.0.2(postcss@8.4.49):
-    dependencies:
-      postcss: 8.4.49
-
-  cssnano@6.1.2(postcss@8.4.49):
-    dependencies:
-      cssnano-preset-default: 6.1.2(postcss@8.4.49)
-      lilconfig: 3.1.1
-      postcss: 8.4.49
+  cssnano-preset-default@7.0.6(postcss@8.5.1):
+    dependencies:
+      browserslist: 4.24.4
+      css-declaration-sorter: 7.2.0(postcss@8.5.1)
+      cssnano-utils: 5.0.0(postcss@8.5.1)
+      postcss: 8.5.1
+      postcss-calc: 10.1.0(postcss@8.5.1)
+      postcss-colormin: 7.0.2(postcss@8.5.1)
+      postcss-convert-values: 7.0.4(postcss@8.5.1)
+      postcss-discard-comments: 7.0.3(postcss@8.5.1)
+      postcss-discard-duplicates: 7.0.1(postcss@8.5.1)
+      postcss-discard-empty: 7.0.0(postcss@8.5.1)
+      postcss-discard-overridden: 7.0.0(postcss@8.5.1)
+      postcss-merge-longhand: 7.0.4(postcss@8.5.1)
+      postcss-merge-rules: 7.0.4(postcss@8.5.1)
+      postcss-minify-font-values: 7.0.0(postcss@8.5.1)
+      postcss-minify-gradients: 7.0.0(postcss@8.5.1)
+      postcss-minify-params: 7.0.2(postcss@8.5.1)
+      postcss-minify-selectors: 7.0.4(postcss@8.5.1)
+      postcss-normalize-charset: 7.0.0(postcss@8.5.1)
+      postcss-normalize-display-values: 7.0.0(postcss@8.5.1)
+      postcss-normalize-positions: 7.0.0(postcss@8.5.1)
+      postcss-normalize-repeat-style: 7.0.0(postcss@8.5.1)
+      postcss-normalize-string: 7.0.0(postcss@8.5.1)
+      postcss-normalize-timing-functions: 7.0.0(postcss@8.5.1)
+      postcss-normalize-unicode: 7.0.2(postcss@8.5.1)
+      postcss-normalize-url: 7.0.0(postcss@8.5.1)
+      postcss-normalize-whitespace: 7.0.0(postcss@8.5.1)
+      postcss-ordered-values: 7.0.1(postcss@8.5.1)
+      postcss-reduce-initial: 7.0.2(postcss@8.5.1)
+      postcss-reduce-transforms: 7.0.0(postcss@8.5.1)
+      postcss-svgo: 7.0.1(postcss@8.5.1)
+      postcss-unique-selectors: 7.0.3(postcss@8.5.1)
+
+  cssnano-utils@5.0.0(postcss@8.5.1):
+    dependencies:
+      postcss: 8.5.1
+
+  cssnano@7.0.6(postcss@8.5.1):
+    dependencies:
+      cssnano-preset-default: 7.0.6(postcss@8.5.1)
+      lilconfig: 3.1.3
+      postcss: 8.5.1
 
   csso@5.0.5:
     dependencies:
       css-tree: 2.2.1
 
-  cssstyle@4.0.1:
+  cssstyle@4.2.1:
     dependencies:
-      rrweb-cssom: 0.6.0
+      '@asamuzakjp/css-color': 2.8.3
+      rrweb-cssom: 0.8.0
 
   csstype@3.1.3: {}
 
-  cypress@13.15.2:
+  cypress@14.0.0:
     dependencies:
       '@cypress/request': 3.0.6
       '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
@@ -16657,7 +17059,7 @@ snapshots:
   data-urls@5.0.0:
     dependencies:
       whatwg-mimetype: 4.0.0
-      whatwg-url: 14.0.0
+      whatwg-url: 14.1.0
 
   data-view-buffer@1.0.1:
     dependencies:
@@ -16715,6 +17117,10 @@ snapshots:
     optionalDependencies:
       supports-color: 8.1.1
 
+  debug@4.4.0:
+    dependencies:
+      ms: 2.1.3
+
   decamelize-keys@1.1.1:
     dependencies:
       decamelize: 1.2.0
@@ -16818,6 +17224,9 @@ snapshots:
 
   destroy@1.2.0: {}
 
+  detect-libc@1.0.3:
+    optional: true
+
   detect-libc@2.0.3: {}
 
   detect-newline@3.1.0: {}
@@ -16928,12 +17337,14 @@ snapshots:
     dependencies:
       jake: 10.8.5
 
-  electron-to-chromium@1.4.601: {}
-
   electron-to-chromium@1.4.686: {}
 
+  electron-to-chromium@1.5.83: {}
+
   emittery@0.13.1: {}
 
+  emoji-regex-xs@1.0.0: {}
+
   emoji-regex@8.0.0: {}
 
   emoji-regex@9.2.2: {}
@@ -17121,38 +17532,13 @@ snapshots:
       es6-promise: 4.2.8
     optional: true
 
-  esbuild-register@3.5.0(esbuild@0.24.0):
+  esbuild-register@3.5.0(esbuild@0.24.2):
     dependencies:
       debug: 4.3.7(supports-color@8.1.1)
-      esbuild: 0.24.0
+      esbuild: 0.24.2
     transitivePeerDependencies:
       - supports-color
 
-  esbuild@0.18.20:
-    optionalDependencies:
-      '@esbuild/android-arm': 0.18.20
-      '@esbuild/android-arm64': 0.18.20
-      '@esbuild/android-x64': 0.18.20
-      '@esbuild/darwin-arm64': 0.18.20
-      '@esbuild/darwin-x64': 0.18.20
-      '@esbuild/freebsd-arm64': 0.18.20
-      '@esbuild/freebsd-x64': 0.18.20
-      '@esbuild/linux-arm': 0.18.20
-      '@esbuild/linux-arm64': 0.18.20
-      '@esbuild/linux-ia32': 0.18.20
-      '@esbuild/linux-loong64': 0.18.20
-      '@esbuild/linux-mips64el': 0.18.20
-      '@esbuild/linux-ppc64': 0.18.20
-      '@esbuild/linux-riscv64': 0.18.20
-      '@esbuild/linux-s390x': 0.18.20
-      '@esbuild/linux-x64': 0.18.20
-      '@esbuild/netbsd-x64': 0.18.20
-      '@esbuild/openbsd-x64': 0.18.20
-      '@esbuild/sunos-x64': 0.18.20
-      '@esbuild/win32-arm64': 0.18.20
-      '@esbuild/win32-ia32': 0.18.20
-      '@esbuild/win32-x64': 0.18.20
-
   esbuild@0.21.5:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.21.5
@@ -17179,6 +17565,33 @@ snapshots:
       '@esbuild/win32-ia32': 0.21.5
       '@esbuild/win32-x64': 0.21.5
 
+  esbuild@0.23.1:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.23.1
+      '@esbuild/android-arm': 0.23.1
+      '@esbuild/android-arm64': 0.23.1
+      '@esbuild/android-x64': 0.23.1
+      '@esbuild/darwin-arm64': 0.23.1
+      '@esbuild/darwin-x64': 0.23.1
+      '@esbuild/freebsd-arm64': 0.23.1
+      '@esbuild/freebsd-x64': 0.23.1
+      '@esbuild/linux-arm': 0.23.1
+      '@esbuild/linux-arm64': 0.23.1
+      '@esbuild/linux-ia32': 0.23.1
+      '@esbuild/linux-loong64': 0.23.1
+      '@esbuild/linux-mips64el': 0.23.1
+      '@esbuild/linux-ppc64': 0.23.1
+      '@esbuild/linux-riscv64': 0.23.1
+      '@esbuild/linux-s390x': 0.23.1
+      '@esbuild/linux-x64': 0.23.1
+      '@esbuild/netbsd-x64': 0.23.1
+      '@esbuild/openbsd-arm64': 0.23.1
+      '@esbuild/openbsd-x64': 0.23.1
+      '@esbuild/sunos-x64': 0.23.1
+      '@esbuild/win32-arm64': 0.23.1
+      '@esbuild/win32-ia32': 0.23.1
+      '@esbuild/win32-x64': 0.23.1
+
   esbuild@0.24.0:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.24.0
@@ -17206,8 +17619,38 @@ snapshots:
       '@esbuild/win32-ia32': 0.24.0
       '@esbuild/win32-x64': 0.24.0
 
+  esbuild@0.24.2:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.24.2
+      '@esbuild/android-arm': 0.24.2
+      '@esbuild/android-arm64': 0.24.2
+      '@esbuild/android-x64': 0.24.2
+      '@esbuild/darwin-arm64': 0.24.2
+      '@esbuild/darwin-x64': 0.24.2
+      '@esbuild/freebsd-arm64': 0.24.2
+      '@esbuild/freebsd-x64': 0.24.2
+      '@esbuild/linux-arm': 0.24.2
+      '@esbuild/linux-arm64': 0.24.2
+      '@esbuild/linux-ia32': 0.24.2
+      '@esbuild/linux-loong64': 0.24.2
+      '@esbuild/linux-mips64el': 0.24.2
+      '@esbuild/linux-ppc64': 0.24.2
+      '@esbuild/linux-riscv64': 0.24.2
+      '@esbuild/linux-s390x': 0.24.2
+      '@esbuild/linux-x64': 0.24.2
+      '@esbuild/netbsd-arm64': 0.24.2
+      '@esbuild/netbsd-x64': 0.24.2
+      '@esbuild/openbsd-arm64': 0.24.2
+      '@esbuild/openbsd-x64': 0.24.2
+      '@esbuild/sunos-x64': 0.24.2
+      '@esbuild/win32-arm64': 0.24.2
+      '@esbuild/win32-ia32': 0.24.2
+      '@esbuild/win32-x64': 0.24.2
+
   escalade@3.1.1: {}
 
+  escalade@3.2.0: {}
+
   escape-goat@3.0.0: {}
 
   escape-html@1.0.3: {}
@@ -17237,59 +17680,21 @@ snapshots:
     dependencies:
       debug: 3.2.7(supports-color@8.1.1)
       is-core-module: 2.15.1
-      resolve: 1.22.8
-    transitivePeerDependencies:
-      - supports-color
-
-  eslint-module-utils@2.11.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0):
-    dependencies:
-      debug: 3.2.7(supports-color@8.1.1)
-    optionalDependencies:
-      '@typescript-eslint/parser': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
-      eslint: 9.14.0
-      eslint-import-resolver-node: 0.3.9
-    transitivePeerDependencies:
-      - supports-color
-
-  eslint-module-utils@2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0):
-    dependencies:
-      debug: 3.2.7(supports-color@8.1.1)
-    optionalDependencies:
-      '@typescript-eslint/parser': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
-      eslint: 9.14.0
-      eslint-import-resolver-node: 0.3.9
-    transitivePeerDependencies:
-      - supports-color
-
-  eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0):
-    dependencies:
-      '@rtsao/scc': 1.1.0
-      array-includes: 3.1.8
-      array.prototype.findlastindex: 1.2.5
-      array.prototype.flat: 1.3.2
-      array.prototype.flatmap: 1.3.2
-      debug: 3.2.7(supports-color@8.1.1)
-      doctrine: 2.1.0
-      eslint: 9.14.0
-      eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.11.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0)
-      hasown: 2.0.2
-      is-core-module: 2.15.1
-      is-glob: 4.0.3
-      minimatch: 3.1.2
-      object.fromentries: 2.0.8
-      object.groupby: 1.0.3
-      object.values: 1.2.0
-      semver: 6.3.1
-      tsconfig-paths: 3.15.0
+      resolve: 1.22.8
+    transitivePeerDependencies:
+      - supports-color
+
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.18.0):
+    dependencies:
+      debug: 3.2.7(supports-color@8.1.1)
     optionalDependencies:
-      '@typescript-eslint/parser': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/parser': 8.20.0(eslint@9.18.0)(typescript@5.7.3)
+      eslint: 9.18.0
+      eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
-      - eslint-import-resolver-typescript
-      - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0):
+  eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0):
     dependencies:
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.8
@@ -17298,9 +17703,9 @@ snapshots:
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7(supports-color@8.1.1)
       doctrine: 2.1.0
-      eslint: 9.14.0
+      eslint: 9.18.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0)
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.18.0)
       hasown: 2.0.2
       is-core-module: 2.15.1
       is-glob: 4.0.3
@@ -17312,22 +17717,36 @@ snapshots:
       string.prototype.trimend: 1.0.8
       tsconfig-paths: 3.15.0
     optionalDependencies:
-      '@typescript-eslint/parser': 7.17.0(eslint@9.14.0)(typescript@5.6.3)
+      '@typescript-eslint/parser': 8.20.0(eslint@9.18.0)(typescript@5.7.3)
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-vue@9.31.0(eslint@9.14.0):
+  eslint-plugin-vue@9.31.0(eslint@9.18.0):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.14.0)
-      eslint: 9.14.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.18.0)
+      eslint: 9.18.0
       globals: 13.24.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.16
       semver: 7.6.3
-      vue-eslint-parser: 9.4.3(eslint@9.14.0)
+      vue-eslint-parser: 9.4.3(eslint@9.18.0)
+      xml-name-validator: 4.0.0
+    transitivePeerDependencies:
+      - supports-color
+
+  eslint-plugin-vue@9.32.0(eslint@9.18.0):
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.18.0)
+      eslint: 9.18.0
+      globals: 13.24.0
+      natural-compare: 1.4.0
+      nth-check: 2.1.1
+      postcss-selector-parser: 6.0.16
+      semver: 7.6.3
+      vue-eslint-parser: 9.4.3(eslint@9.18.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
@@ -17348,15 +17767,15 @@ snapshots:
 
   eslint-visitor-keys@4.2.0: {}
 
-  eslint@9.14.0:
+  eslint@9.18.0:
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@9.14.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@9.18.0)
       '@eslint-community/regexpp': 4.12.1
-      '@eslint/config-array': 0.18.0
-      '@eslint/core': 0.7.0
-      '@eslint/eslintrc': 3.1.0
-      '@eslint/js': 9.14.0
-      '@eslint/plugin-kit': 0.2.0
+      '@eslint/config-array': 0.19.1
+      '@eslint/core': 0.10.0
+      '@eslint/eslintrc': 3.2.0
+      '@eslint/js': 9.18.0
+      '@eslint/plugin-kit': 0.2.5
       '@humanfs/node': 0.16.6
       '@humanwhocodes/module-importer': 1.0.1
       '@humanwhocodes/retry': 0.4.1
@@ -17364,7 +17783,7 @@ snapshots:
       '@types/json-schema': 7.0.15
       ajv: 6.12.6
       chalk: 4.1.2
-      cross-spawn: 7.0.3
+      cross-spawn: 7.0.6
       debug: 4.3.7(supports-color@8.1.1)
       escape-string-regexp: 4.0.0
       eslint-scope: 8.2.0
@@ -17384,7 +17803,6 @@ snapshots:
       minimatch: 3.1.2
       natural-compare: 1.4.0
       optionator: 0.9.4
-      text-table: 0.2.0
     transitivePeerDependencies:
       - supports-color
 
@@ -17516,42 +17934,6 @@ snapshots:
 
   exponential-backoff@3.1.1: {}
 
-  express@4.21.0:
-    dependencies:
-      accepts: 1.3.8
-      array-flatten: 1.1.1
-      body-parser: 1.20.3
-      content-disposition: 0.5.4
-      content-type: 1.0.5
-      cookie: 0.6.0
-      cookie-signature: 1.0.6
-      debug: 2.6.9
-      depd: 2.0.0
-      encodeurl: 2.0.0
-      escape-html: 1.0.3
-      etag: 1.8.1
-      finalhandler: 1.3.1
-      fresh: 0.5.2
-      http-errors: 2.0.0
-      merge-descriptors: 1.0.3
-      methods: 1.1.2
-      on-finished: 2.4.1
-      parseurl: 1.3.3
-      path-to-regexp: 0.1.10
-      proxy-addr: 2.0.7
-      qs: 6.13.0
-      range-parser: 1.2.1
-      safe-buffer: 5.2.1
-      send: 0.19.0
-      serve-static: 1.16.2
-      setprototypeof: 1.2.0
-      statuses: 2.0.1
-      type-is: 1.6.18
-      utils-merge: 1.0.1
-      vary: 1.1.2
-    transitivePeerDependencies:
-      - supports-color
-
   express@4.21.1:
     dependencies:
       accepts: 1.3.8
@@ -17619,7 +18001,7 @@ snapshots:
 
   fast-fifo@1.3.0: {}
 
-  fast-glob@3.3.2:
+  fast-glob@3.3.3:
     dependencies:
       '@nodelib/fs.stat': 2.0.5
       '@nodelib/fs.walk': 1.2.8
@@ -17671,11 +18053,12 @@ snapshots:
       raw-body: 3.0.0
       secure-json-parse: 2.7.0
 
-  fastify@5.0.0:
+  fastify@5.2.1:
     dependencies:
       '@fastify/ajv-compiler': 4.0.0
       '@fastify/error': 4.0.0
       '@fastify/fast-json-stringify-compiler': 5.0.0
+      '@fastify/proxy-addr': 5.0.0
       abstract-logging: 2.0.1
       avvio: 9.0.0
       fast-json-stringify: 6.0.0
@@ -17683,9 +18066,8 @@ snapshots:
       light-my-request: 6.0.0
       pino: 9.2.0
       process-warning: 4.0.0
-      proxy-addr: 2.0.7
       rfdc: 1.4.1
-      secure-json-parse: 2.7.0
+      secure-json-parse: 3.0.2
       semver: 7.6.3
       toad-cache: 3.7.0
 
@@ -17808,9 +18190,9 @@ snapshots:
 
   follow-redirects@1.15.2: {}
 
-  follow-redirects@1.15.9(debug@4.3.7):
+  follow-redirects@1.15.9(debug@4.4.0):
     optionalDependencies:
-      debug: 4.3.7(supports-color@8.1.1)
+      debug: 4.4.0
 
   for-each@0.3.3:
     dependencies:
@@ -17823,8 +18205,6 @@ snapshots:
 
   forever-agent@0.6.1: {}
 
-  form-data-encoder@2.1.4: {}
-
   form-data-encoder@4.0.2: {}
 
   form-data@4.0.1:
@@ -17837,6 +18217,8 @@ snapshots:
     dependencies:
       fetch-blob: 3.2.0
 
+  forwarded-parse@2.1.2: {}
+
   forwarded@0.2.0: {}
 
   fresh@0.5.2: {}
@@ -17960,7 +18342,7 @@ snapshots:
       es-errors: 1.3.0
       get-intrinsic: 1.2.4
 
-  get-tsconfig@4.7.2:
+  get-tsconfig@4.9.0:
     dependencies:
       resolve-pkg-maps: 1.0.0
 
@@ -17980,11 +18362,6 @@ snapshots:
     dependencies:
       is-glob: 4.0.3
 
-  glob-promise@4.2.2(glob@7.2.3):
-    dependencies:
-      '@types/glob': 7.2.0
-      glob: 7.2.3
-
   glob@10.3.10:
     dependencies:
       foreground-child: 3.1.1
@@ -17993,7 +18370,7 @@ snapshots:
       minipass: 7.0.4
       path-scurry: 1.10.1
 
-  glob@11.0.0:
+  glob@11.0.1:
     dependencies:
       foreground-child: 3.1.1
       jackspeak: 4.0.1
@@ -18031,7 +18408,7 @@ snapshots:
 
   globals@14.0.0: {}
 
-  globals@15.12.0: {}
+  globals@15.14.0: {}
 
   globalthis@1.0.3:
     dependencies:
@@ -18041,7 +18418,7 @@ snapshots:
     dependencies:
       array-union: 2.1.0
       dir-glob: 3.0.1
-      fast-glob: 3.3.2
+      fast-glob: 3.3.3
       ignore: 5.2.4
       merge2: 1.4.1
       slash: 3.0.0
@@ -18067,21 +18444,7 @@ snapshots:
       p-cancelable: 2.1.1
       responselike: 2.0.1
 
-  got@12.6.1:
-    dependencies:
-      '@sindresorhus/is': 5.3.0
-      '@szmarczak/http-timer': 5.0.1
-      cacheable-lookup: 7.0.0
-      cacheable-request: 10.2.14
-      decompress-response: 6.0.0
-      form-data-encoder: 2.1.4
-      get-stream: 6.0.1
-      http2-wrapper: 2.2.1
-      lowercase-keys: 3.0.0
-      p-cancelable: 3.0.0
-      responselike: 3.0.0
-
-  got@14.4.4:
+  got@14.4.5:
     dependencies:
       '@sindresorhus/is': 7.0.1
       '@szmarczak/http-timer': 5.0.1
@@ -18105,18 +18468,8 @@ snapshots:
 
   hammerjs@2.0.8: {}
 
-  happy-dom@10.0.3:
-    dependencies:
-      css.escape: 1.5.1
-      entities: 4.5.0
-      iconv-lite: 0.6.3
-      webidl-conversions: 7.0.0
-      whatwg-encoding: 2.0.0
-      whatwg-mimetype: 3.0.0
-
-  happy-dom@15.11.4:
+  happy-dom@16.6.0:
     dependencies:
-      entities: 4.5.0
       webidl-conversions: 7.0.0
       whatwg-mimetype: 3.0.0
 
@@ -18167,7 +18520,7 @@ snapshots:
     dependencies:
       function-bind: 1.1.2
 
-  hast-util-to-html@9.0.3:
+  hast-util-to-html@9.0.4:
     dependencies:
       '@types/hast': 3.0.4
       '@types/unist': 3.0.2
@@ -18205,7 +18558,7 @@ snapshots:
     dependencies:
       whatwg-encoding: 3.1.1
 
-  html-entities@2.3.2: {}
+  html-entities@2.5.2: {}
 
   html-escaper@2.0.2: {}
 
@@ -18301,6 +18654,13 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  https-proxy-agent@7.0.6:
+    dependencies:
+      agent-base: 7.1.3
+      debug: 4.3.7(supports-color@8.1.1)
+    transitivePeerDependencies:
+      - supports-color
+
   human-signals@1.1.1: {}
 
   human-signals@2.1.0: {}
@@ -18331,7 +18691,7 @@ snapshots:
 
   ignore@5.3.1: {}
 
-  immutable@4.2.2: {}
+  immutable@5.0.3: {}
 
   import-fresh@3.3.0:
     dependencies:
@@ -18387,11 +18747,11 @@ snapshots:
 
   intersection-observer@0.12.2: {}
 
-  ioredis@5.4.1:
+  ioredis@5.4.2:
     dependencies:
       '@ioredis/commands': 1.2.0
       cluster-key-slot: 1.1.2
-      debug: 4.3.4
+      debug: 4.3.7(supports-color@8.1.1)
       denque: 2.1.0
       lodash.defaults: 4.2.0
       lodash.isarguments: 3.1.0
@@ -18410,7 +18770,7 @@ snapshots:
     dependencies:
       ip-address: 9.0.5
 
-  ip-regex@4.3.0: {}
+  ip-regex@5.0.0: {}
 
   ip@2.0.1: {}
 
@@ -18499,10 +18859,6 @@ snapshots:
       global-dirs: 3.0.1
       is-path-inside: 3.0.3
 
-  is-ip@3.1.0:
-    dependencies:
-      ip-regex: 4.3.0
-
   is-lambda@1.0.1: {}
 
   is-map@2.0.2: {}
@@ -18614,7 +18970,7 @@ snapshots:
   istanbul-lib-instrument@5.2.1:
     dependencies:
       '@babel/core': 7.24.7
-      '@babel/parser': 7.24.7
+      '@babel/parser': 7.25.6
       '@istanbuljs/schema': 0.1.3
       istanbul-lib-coverage: 3.2.2
       semver: 6.3.1
@@ -18691,7 +19047,7 @@ snapshots:
       '@jest/expect': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       chalk: 4.1.2
       co: 4.6.0
       dedent: 1.3.0
@@ -18711,16 +19067,16 @@ snapshots:
       - babel-plugin-macros
       - supports-color
 
-  jest-cli@29.7.0(@types/node@22.9.0):
+  jest-cli@29.7.0(@types/node@22.10.7):
     dependencies:
       '@jest/core': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
       chalk: 4.1.2
-      create-jest: 29.7.0(@types/node@22.9.0)
+      create-jest: 29.7.0(@types/node@22.10.7)
       exit: 0.1.2
       import-local: 3.1.0
-      jest-config: 29.7.0(@types/node@22.9.0)
+      jest-config: 29.7.0(@types/node@22.10.7)
       jest-util: 29.7.0
       jest-validate: 29.7.0
       yargs: 17.7.2
@@ -18730,7 +19086,7 @@ snapshots:
       - supports-color
       - ts-node
 
-  jest-config@29.7.0(@types/node@22.9.0):
+  jest-config@29.7.0(@types/node@22.10.7):
     dependencies:
       '@babel/core': 7.23.5
       '@jest/test-sequencer': 29.7.0
@@ -18755,7 +19111,7 @@ snapshots:
       slash: 3.0.0
       strip-json-comments: 3.1.1
     optionalDependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
     transitivePeerDependencies:
       - babel-plugin-macros
       - supports-color
@@ -18784,7 +19140,7 @@ snapshots:
       '@jest/environment': 29.7.0
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       jest-mock: 29.7.0
       jest-util: 29.7.0
 
@@ -18801,7 +19157,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@types/graceful-fs': 4.1.6
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
@@ -18840,7 +19196,7 @@ snapshots:
   jest-mock@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       jest-util: 29.7.0
 
   jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -18875,7 +19231,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -18903,7 +19259,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       chalk: 4.1.2
       cjs-module-lexer: 1.2.2
       collect-v8-coverage: 1.0.1
@@ -18949,7 +19305,7 @@ snapshots:
   jest-util@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       chalk: 4.1.2
       ci-info: 3.7.1
       graceful-fs: 4.2.11
@@ -18968,7 +19324,7 @@ snapshots:
     dependencies:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -18982,17 +19338,17 @@ snapshots:
 
   jest-worker@29.7.0:
     dependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
-  jest@29.7.0(@types/node@22.9.0):
+  jest@29.7.0(@types/node@22.10.7):
     dependencies:
       '@jest/core': 29.7.0
       '@jest/types': 29.6.3
       import-local: 3.1.0
-      jest-cli: 29.7.0(@types/node@22.9.0)
+      jest-cli: 29.7.0(@types/node@22.10.7)
     transitivePeerDependencies:
       - '@types/node'
       - babel-plugin-macros
@@ -19043,31 +19399,31 @@ snapshots:
 
   jsbn@1.1.0: {}
 
-  jschardet@3.0.0: {}
+  jschardet@3.1.4: {}
 
   jsdoc-type-pratt-parser@4.1.0: {}
 
-  jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+  jsdom@26.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
     dependencies:
-      cssstyle: 4.0.1
+      cssstyle: 4.2.1
       data-urls: 5.0.0
       decimal.js: 10.4.3
       form-data: 4.0.1
       html-encoding-sniffer: 4.0.0
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.5
+      https-proxy-agent: 7.0.6
       is-potential-custom-element-name: 1.0.1
-      nwsapi: 2.2.12
+      nwsapi: 2.2.16
       parse5: 7.2.1
-      rrweb-cssom: 0.7.1
+      rrweb-cssom: 0.8.0
       saxes: 6.0.0
       symbol-tree: 3.2.4
-      tough-cookie: 4.1.4
+      tough-cookie: 5.0.0
       w3c-xmlserializer: 5.0.0
       webidl-conversions: 7.0.0
       whatwg-encoding: 3.1.1
       whatwg-mimetype: 4.0.0
-      whatwg-url: 14.0.0
+      whatwg-url: 14.1.0
       ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       xml-name-validator: 5.0.0
     transitivePeerDependencies:
@@ -19075,27 +19431,27 @@ snapshots:
       - supports-color
       - utf-8-validate
 
-  jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+  jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4):
     dependencies:
-      cssstyle: 4.0.1
+      cssstyle: 4.2.1
       data-urls: 5.0.0
       decimal.js: 10.4.3
       form-data: 4.0.1
       html-encoding-sniffer: 4.0.0
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.5
+      https-proxy-agent: 7.0.6
       is-potential-custom-element-name: 1.0.1
-      nwsapi: 2.2.12
+      nwsapi: 2.2.16
       parse5: 7.2.1
-      rrweb-cssom: 0.7.1
+      rrweb-cssom: 0.8.0
       saxes: 6.0.0
       symbol-tree: 3.2.4
-      tough-cookie: 4.1.4
+      tough-cookie: 5.0.0
       w3c-xmlserializer: 5.0.0
       webidl-conversions: 7.0.0
       whatwg-encoding: 3.1.1
       whatwg-mimetype: 4.0.0
-      whatwg-url: 14.0.0
+      whatwg-url: 14.1.0
       ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       xml-name-validator: 5.0.0
     transitivePeerDependencies:
@@ -19153,7 +19509,7 @@ snapshots:
     optionalDependencies:
       graceful-fs: 4.2.11
 
-  jsonld@8.3.2(web-streams-polyfill@4.0.0):
+  jsonld@8.3.3(web-streams-polyfill@4.0.0):
     dependencies:
       '@digitalbazaar/http-client': 3.4.1(web-streams-polyfill@4.0.0)
       canonicalize: 1.0.8
@@ -19243,7 +19599,7 @@ snapshots:
       process-warning: 4.0.0
       set-cookie-parser: 2.6.0
 
-  lilconfig@3.1.1: {}
+  lilconfig@3.1.3: {}
 
   lines-and-columns@1.2.4: {}
 
@@ -19321,12 +19677,10 @@ snapshots:
 
   lowercase-keys@3.0.0: {}
 
-  lru-cache@10.0.2:
-    dependencies:
-      semver: 7.6.3
-
   lru-cache@10.2.2: {}
 
+  lru-cache@10.4.3: {}
+
   lru-cache@11.0.0: {}
 
   lru-cache@4.1.5:
@@ -19528,7 +19882,7 @@ snapshots:
 
   media-typer@0.3.0: {}
 
-  meilisearch@0.45.0: {}
+  meilisearch@0.48.2: {}
 
   memoizerific@1.11.3:
     dependencies:
@@ -19922,33 +20276,33 @@ snapshots:
     optionalDependencies:
       msgpackr-extract: 3.0.2
 
-  msw-storybook-addon@2.0.4(msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3)):
+  msw-storybook-addon@2.0.4(msw@2.7.0(@types/node@22.10.7)(typescript@5.7.3)):
     dependencies:
       is-node-process: 1.2.0
-      msw: 2.6.4(@types/node@22.9.0)(typescript@5.6.3)
+      msw: 2.7.0(@types/node@22.10.7)(typescript@5.7.3)
 
-  msw@2.6.4(@types/node@22.9.0)(typescript@5.6.3):
+  msw@2.7.0(@types/node@22.10.7)(typescript@5.7.3):
     dependencies:
       '@bundled-es-modules/cookie': 2.0.1
       '@bundled-es-modules/statuses': 1.0.1
       '@bundled-es-modules/tough-cookie': 0.1.6
-      '@inquirer/confirm': 5.0.2(@types/node@22.9.0)
-      '@mswjs/interceptors': 0.36.10
+      '@inquirer/confirm': 5.0.2(@types/node@22.10.7)
+      '@mswjs/interceptors': 0.37.5
       '@open-draft/deferred-promise': 2.2.0
       '@open-draft/until': 2.1.0
       '@types/cookie': 0.6.0
       '@types/statuses': 2.0.4
-      chalk: 4.1.2
       graphql: 16.8.1
       headers-polyfill: 4.0.2
       is-node-process: 1.2.0
       outvariant: 1.4.3
       path-to-regexp: 6.3.0
+      picocolors: 1.1.1
       strict-event-emitter: 0.5.1
       type-fest: 4.26.1
       yargs: 17.7.2
     optionalDependencies:
-      typescript: 5.6.3
+      typescript: 5.7.3
     transitivePeerDependencies:
       - '@types/node'
 
@@ -19976,9 +20330,9 @@ snapshots:
 
   nan@2.20.0: {}
 
-  nanoid@3.3.7: {}
+  nanoid@3.3.8: {}
 
-  nanoid@5.0.8: {}
+  nanoid@5.0.9: {}
 
   natural-compare@1.4.0: {}
 
@@ -20021,6 +20375,9 @@ snapshots:
   node-addon-api@3.2.1:
     optional: true
 
+  node-addon-api@7.1.1:
+    optional: true
+
   node-domexception@1.0.0: {}
 
   node-fetch@2.6.13(encoding@0.1.13):
@@ -20066,6 +20423,8 @@ snapshots:
 
   node-releases@2.0.14: {}
 
+  node-releases@2.0.19: {}
+
   nodemailer@6.9.16: {}
 
   nodemon@3.1.7:
@@ -20081,6 +20440,19 @@ snapshots:
       touch: 3.1.0
       undefsafe: 2.0.5
 
+  nodemon@3.1.9:
+    dependencies:
+      chokidar: 3.5.3
+      debug: 4.3.7(supports-color@5.5.0)
+      ignore-by-default: 1.0.1
+      minimatch: 3.1.2
+      pstree.remy: 1.1.8
+      semver: 7.6.3
+      simple-update-notifier: 2.0.0
+      supports-color: 5.5.0
+      touch: 3.1.0
+      undefsafe: 2.0.5
+
   nofilter@3.1.0: {}
 
   nopt@1.0.10:
@@ -20149,7 +20521,7 @@ snapshots:
     dependencies:
       boolbase: 1.0.0
 
-  nwsapi@2.2.12: {}
+  nwsapi@2.2.16: {}
 
   oauth2orize-pkce@0.1.2: {}
 
@@ -20233,9 +20605,11 @@ snapshots:
     dependencies:
       mimic-fn: 4.0.0
 
-  oniguruma-to-js@0.4.3:
+  oniguruma-to-es@2.2.0:
     dependencies:
-      regex: 4.3.3
+      emoji-regex-xs: 1.0.0
+      regex: 5.1.1
+      regex-recursion: 5.1.1
 
   open@8.4.2:
     dependencies:
@@ -20245,13 +20619,13 @@ snapshots:
 
   openapi-types@12.1.3: {}
 
-  openapi-typescript@6.7.3:
+  openapi-typescript@6.7.6:
     dependencies:
       ansi-colors: 4.1.3
-      fast-glob: 3.3.2
+      fast-glob: 3.3.3
       js-yaml: 4.1.0
       supports-color: 9.4.0
-      undici: 5.28.2
+      undici: 5.28.5
       yargs-parser: 21.1.1
 
   optionator@0.9.4:
@@ -20271,16 +20645,14 @@ snapshots:
 
   ospath@1.2.2: {}
 
-  otpauth@9.3.4:
+  otpauth@9.3.6:
     dependencies:
-      '@noble/hashes': 1.5.0
+      '@noble/hashes': 1.6.1
 
   outvariant@1.4.3: {}
 
   p-cancelable@2.1.1: {}
 
-  p-cancelable@3.0.0: {}
-
   p-cancelable@4.0.1: {}
 
   p-finally@1.0.0: {}
@@ -20374,8 +20746,8 @@ snapshots:
 
   path-scurry@1.10.1:
     dependencies:
-      lru-cache: 10.0.2
-      minipass: 7.0.4
+      lru-cache: 10.2.2
+      minipass: 7.1.2
 
   path-scurry@2.0.0:
     dependencies:
@@ -20388,10 +20760,10 @@ snapshots:
     dependencies:
       isarray: 0.0.1
 
-  path-to-regexp@3.3.0: {}
-
   path-to-regexp@6.3.0: {}
 
+  path-to-regexp@8.2.0: {}
+
   path-type@4.0.0: {}
 
   pathe@1.1.2: {}
@@ -20467,8 +20839,6 @@ snapshots:
 
   picocolors@1.0.1: {}
 
-  picocolors@1.1.0: {}
-
   picocolors@1.1.1: {}
 
   picomatch@2.3.1: {}
@@ -20536,140 +20906,142 @@ snapshots:
 
   possible-typed-array-names@1.0.0: {}
 
-  postcss-calc@9.0.1(postcss@8.4.49):
+  postcss-calc@10.1.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
-      postcss-selector-parser: 6.0.16
+      postcss: 8.5.1
+      postcss-selector-parser: 7.0.0
       postcss-value-parser: 4.2.0
 
-  postcss-colormin@6.1.0(postcss@8.4.49):
+  postcss-colormin@7.0.2(postcss@8.5.1):
     dependencies:
-      browserslist: 4.23.0
+      browserslist: 4.24.4
       caniuse-api: 3.0.0
       colord: 2.9.3
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-convert-values@6.1.0(postcss@8.4.49):
+  postcss-convert-values@7.0.4(postcss@8.5.1):
     dependencies:
-      browserslist: 4.23.0
-      postcss: 8.4.49
+      browserslist: 4.24.4
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-discard-comments@6.0.2(postcss@8.4.49):
+  postcss-discard-comments@7.0.3(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
+      postcss-selector-parser: 6.1.2
 
-  postcss-discard-duplicates@6.0.3(postcss@8.4.49):
+  postcss-discard-duplicates@7.0.1(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
 
-  postcss-discard-empty@6.0.3(postcss@8.4.49):
+  postcss-discard-empty@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
 
-  postcss-discard-overridden@6.0.2(postcss@8.4.49):
+  postcss-discard-overridden@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
 
-  postcss-merge-longhand@6.0.5(postcss@8.4.49):
+  postcss-merge-longhand@7.0.4(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
-      stylehacks: 6.1.1(postcss@8.4.49)
+      stylehacks: 7.0.4(postcss@8.5.1)
 
-  postcss-merge-rules@6.1.1(postcss@8.4.49):
+  postcss-merge-rules@7.0.4(postcss@8.5.1):
     dependencies:
-      browserslist: 4.23.0
+      browserslist: 4.24.4
       caniuse-api: 3.0.0
-      cssnano-utils: 4.0.2(postcss@8.4.49)
-      postcss: 8.4.49
-      postcss-selector-parser: 6.0.16
+      cssnano-utils: 5.0.0(postcss@8.5.1)
+      postcss: 8.5.1
+      postcss-selector-parser: 6.1.2
 
-  postcss-minify-font-values@6.1.0(postcss@8.4.49):
+  postcss-minify-font-values@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-minify-gradients@6.0.3(postcss@8.4.49):
+  postcss-minify-gradients@7.0.0(postcss@8.5.1):
     dependencies:
       colord: 2.9.3
-      cssnano-utils: 4.0.2(postcss@8.4.49)
-      postcss: 8.4.49
+      cssnano-utils: 5.0.0(postcss@8.5.1)
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-minify-params@6.1.0(postcss@8.4.49):
+  postcss-minify-params@7.0.2(postcss@8.5.1):
     dependencies:
-      browserslist: 4.23.0
-      cssnano-utils: 4.0.2(postcss@8.4.49)
-      postcss: 8.4.49
+      browserslist: 4.24.4
+      cssnano-utils: 5.0.0(postcss@8.5.1)
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-minify-selectors@6.0.4(postcss@8.4.49):
+  postcss-minify-selectors@7.0.4(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
-      postcss-selector-parser: 6.0.16
+      cssesc: 3.0.0
+      postcss: 8.5.1
+      postcss-selector-parser: 6.1.2
 
-  postcss-normalize-charset@6.0.2(postcss@8.4.49):
+  postcss-normalize-charset@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
 
-  postcss-normalize-display-values@6.0.2(postcss@8.4.49):
+  postcss-normalize-display-values@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-positions@6.0.2(postcss@8.4.49):
+  postcss-normalize-positions@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-repeat-style@6.0.2(postcss@8.4.49):
+  postcss-normalize-repeat-style@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-string@6.0.2(postcss@8.4.49):
+  postcss-normalize-string@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-timing-functions@6.0.2(postcss@8.4.49):
+  postcss-normalize-timing-functions@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-unicode@6.1.0(postcss@8.4.49):
+  postcss-normalize-unicode@7.0.2(postcss@8.5.1):
     dependencies:
-      browserslist: 4.23.0
-      postcss: 8.4.49
+      browserslist: 4.24.4
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-url@6.0.2(postcss@8.4.49):
+  postcss-normalize-url@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-normalize-whitespace@6.0.2(postcss@8.4.49):
+  postcss-normalize-whitespace@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-ordered-values@6.0.2(postcss@8.4.49):
+  postcss-ordered-values@7.0.1(postcss@8.5.1):
     dependencies:
-      cssnano-utils: 4.0.2(postcss@8.4.49)
-      postcss: 8.4.49
+      cssnano-utils: 5.0.0(postcss@8.5.1)
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
-  postcss-reduce-initial@6.1.0(postcss@8.4.49):
+  postcss-reduce-initial@7.0.2(postcss@8.5.1):
     dependencies:
-      browserslist: 4.23.0
+      browserslist: 4.24.4
       caniuse-api: 3.0.0
-      postcss: 8.4.49
+      postcss: 8.5.1
 
-  postcss-reduce-transforms@6.0.2(postcss@8.4.49):
+  postcss-reduce-transforms@7.0.0(postcss@8.5.1):
     dependencies:
-      postcss: 8.4.49
+      postcss: 8.5.1
       postcss-value-parser: 4.2.0
 
   postcss-selector-parser@6.0.16:
@@ -20677,28 +21049,32 @@ snapshots:
       cssesc: 3.0.0
       util-deprecate: 1.0.2
 
-  postcss-svgo@6.0.3(postcss@8.4.49):
+  postcss-selector-parser@6.1.2:
     dependencies:
-      postcss: 8.4.49
-      postcss-value-parser: 4.2.0
-      svgo: 3.2.0
+      cssesc: 3.0.0
+      util-deprecate: 1.0.2
 
-  postcss-unique-selectors@6.0.4(postcss@8.4.49):
+  postcss-selector-parser@7.0.0:
     dependencies:
-      postcss: 8.4.49
-      postcss-selector-parser: 6.0.16
+      cssesc: 3.0.0
+      util-deprecate: 1.0.2
 
-  postcss-value-parser@4.2.0: {}
+  postcss-svgo@7.0.1(postcss@8.5.1):
+    dependencies:
+      postcss: 8.5.1
+      postcss-value-parser: 4.2.0
+      svgo: 3.3.2
 
-  postcss@8.4.47:
+  postcss-unique-selectors@7.0.3(postcss@8.5.1):
     dependencies:
-      nanoid: 3.3.7
-      picocolors: 1.1.0
-      source-map-js: 1.2.1
+      postcss: 8.5.1
+      postcss-selector-parser: 6.1.2
+
+  postcss-value-parser@4.2.0: {}
 
-  postcss@8.4.49:
+  postcss@8.5.1:
     dependencies:
-      nanoid: 3.3.7
+      nanoid: 3.3.8
       picocolors: 1.1.1
       source-map-js: 1.2.1
 
@@ -20726,7 +21102,7 @@ snapshots:
 
   prelude-ls@1.2.1: {}
 
-  prettier@3.3.3: {}
+  prettier@3.4.2: {}
 
   pretty-bytes@5.6.0: {}
 
@@ -20742,11 +21118,11 @@ snapshots:
       ansi-styles: 5.2.0
       react-is: 18.2.0
 
-  private-ip@2.3.3:
+  private-ip@3.0.2:
     dependencies:
-      ip-regex: 4.3.0
+      '@chainsafe/is-ip': 2.1.0
+      ip-regex: 5.0.0
       ipaddr.js: 2.2.0
-      is-ip: 3.1.0
       netmask: 2.0.2
 
   probe-image-size@7.2.3:
@@ -20901,7 +21277,7 @@ snapshots:
 
   pvtsutils@1.3.5:
     dependencies:
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   pvutils@1.1.3: {}
 
@@ -20963,9 +21339,9 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  react-docgen-typescript@2.2.2(typescript@5.6.3):
+  react-docgen-typescript@2.2.2(typescript@5.7.3):
     dependencies:
-      typescript: 5.6.3
+      typescript: 5.7.3
 
   react-docgen@7.0.1:
     dependencies:
@@ -21052,7 +21428,7 @@ snapshots:
       esprima: 4.0.1
       source-map: 0.6.1
       tiny-invariant: 1.3.3
-      tslib: 2.7.0
+      tslib: 2.8.1
 
   reconnecting-websocket@4.4.0: {}
 
@@ -21079,7 +21455,16 @@ snapshots:
 
   regenerator-runtime@0.14.0: {}
 
-  regex@4.3.3: {}
+  regex-recursion@5.1.1:
+    dependencies:
+      regex: 5.1.1
+      regex-utilities: 2.3.0
+
+  regex-utilities@2.3.0: {}
+
+  regex@5.1.1:
+    dependencies:
+      regex-utilities: 2.3.0
 
   regexp.prototype.flags@1.5.0:
     dependencies:
@@ -21197,33 +21582,32 @@ snapshots:
       glob: 7.2.3
     optional: true
 
-  rollup@4.26.0:
+  rollup@4.31.0:
     dependencies:
       '@types/estree': 1.0.6
     optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.26.0
-      '@rollup/rollup-android-arm64': 4.26.0
-      '@rollup/rollup-darwin-arm64': 4.26.0
-      '@rollup/rollup-darwin-x64': 4.26.0
-      '@rollup/rollup-freebsd-arm64': 4.26.0
-      '@rollup/rollup-freebsd-x64': 4.26.0
-      '@rollup/rollup-linux-arm-gnueabihf': 4.26.0
-      '@rollup/rollup-linux-arm-musleabihf': 4.26.0
-      '@rollup/rollup-linux-arm64-gnu': 4.26.0
-      '@rollup/rollup-linux-arm64-musl': 4.26.0
-      '@rollup/rollup-linux-powerpc64le-gnu': 4.26.0
-      '@rollup/rollup-linux-riscv64-gnu': 4.26.0
-      '@rollup/rollup-linux-s390x-gnu': 4.26.0
-      '@rollup/rollup-linux-x64-gnu': 4.26.0
-      '@rollup/rollup-linux-x64-musl': 4.26.0
-      '@rollup/rollup-win32-arm64-msvc': 4.26.0
-      '@rollup/rollup-win32-ia32-msvc': 4.26.0
-      '@rollup/rollup-win32-x64-msvc': 4.26.0
+      '@rollup/rollup-android-arm-eabi': 4.31.0
+      '@rollup/rollup-android-arm64': 4.31.0
+      '@rollup/rollup-darwin-arm64': 4.31.0
+      '@rollup/rollup-darwin-x64': 4.31.0
+      '@rollup/rollup-freebsd-arm64': 4.31.0
+      '@rollup/rollup-freebsd-x64': 4.31.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.31.0
+      '@rollup/rollup-linux-arm-musleabihf': 4.31.0
+      '@rollup/rollup-linux-arm64-gnu': 4.31.0
+      '@rollup/rollup-linux-arm64-musl': 4.31.0
+      '@rollup/rollup-linux-loongarch64-gnu': 4.31.0
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.31.0
+      '@rollup/rollup-linux-riscv64-gnu': 4.31.0
+      '@rollup/rollup-linux-s390x-gnu': 4.31.0
+      '@rollup/rollup-linux-x64-gnu': 4.31.0
+      '@rollup/rollup-linux-x64-musl': 4.31.0
+      '@rollup/rollup-win32-arm64-msvc': 4.31.0
+      '@rollup/rollup-win32-ia32-msvc': 4.31.0
+      '@rollup/rollup-win32-x64-msvc': 4.31.0
       fsevents: 2.3.3
 
-  rrweb-cssom@0.6.0: {}
-
-  rrweb-cssom@0.7.1: {}
+  rrweb-cssom@0.8.0: {}
 
   rss-parser@3.13.0:
     dependencies:
@@ -21276,26 +21660,22 @@ snapshots:
 
   safer-buffer@2.1.2: {}
 
-  sanitize-html@2.13.1:
+  sanitize-html@2.14.0:
     dependencies:
       deepmerge: 4.2.2
       escape-string-regexp: 4.0.0
       htmlparser2: 8.0.1
       is-plain-object: 5.0.0
       parse-srcset: 1.0.2
-      postcss: 8.4.47
-
-  sass@1.79.3:
-    dependencies:
-      chokidar: 3.5.3
-      immutable: 4.2.2
-      source-map-js: 1.2.0
+      postcss: 8.5.1
 
-  sass@1.79.4:
+  sass@1.83.4:
     dependencies:
       chokidar: 3.5.3
-      immutable: 4.2.2
+      immutable: 5.0.3
       source-map-js: 1.2.1
+    optionalDependencies:
+      '@parcel/watcher': 2.5.0
 
   sax@1.2.4: {}
 
@@ -21309,7 +21689,7 @@ snapshots:
 
   secure-json-parse@2.7.0: {}
 
-  secure-json-parse@3.0.0: {}
+  secure-json-parse@3.0.2: {}
 
   seedrandom@3.0.5: {}
 
@@ -21427,13 +21807,15 @@ snapshots:
 
   shebang-regex@3.0.0: {}
 
-  shiki@1.22.2:
+  shiki@1.27.2:
     dependencies:
-      '@shikijs/core': 1.22.2
-      '@shikijs/engine-javascript': 1.22.2
-      '@shikijs/engine-oniguruma': 1.22.2
-      '@shikijs/types': 1.22.2
-      '@shikijs/vscode-textmate': 9.3.0
+      '@shikijs/core': 1.27.2
+      '@shikijs/engine-javascript': 1.27.2
+      '@shikijs/engine-oniguruma': 1.27.2
+      '@shikijs/langs': 1.27.2
+      '@shikijs/themes': 1.27.2
+      '@shikijs/types': 1.27.2
+      '@shikijs/vscode-textmate': 10.0.1
       '@types/hast': 3.0.4
 
   shimmer@1.2.1: {}
@@ -21665,16 +22047,16 @@ snapshots:
 
   standard-as-callback@2.1.0: {}
 
-  start-server-and-test@2.0.8:
+  start-server-and-test@2.0.10:
     dependencies:
       arg: 5.0.2
       bluebird: 3.7.2
       check-more-types: 2.24.0
-      debug: 4.3.7(supports-color@8.1.1)
+      debug: 4.4.0
       execa: 5.1.1
       lazy-ass: 1.6.0
       ps-tree: 1.2.0
-      wait-on: 8.0.1(debug@4.3.7)
+      wait-on: 8.0.2(debug@4.4.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -21686,24 +22068,24 @@ snapshots:
     dependencies:
       internal-slot: 1.0.5
 
-  storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/components@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/core-events@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/theming@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(@storybook/types@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+  storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/components@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/core-events@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/theming@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(@storybook/types@8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
     dependencies:
-      '@storybook/blocks': 8.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/components': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/core-events': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/manager-api': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/preview-api': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/theming': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
-      '@storybook/types': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))
+      '@storybook/blocks': 8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/components': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/core-events': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/manager-api': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/preview-api': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/theming': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
+      '@storybook/types': 8.5.0(storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4))
     optionalDependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
-  storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4):
+  storybook@8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4):
     dependencies:
-      '@storybook/core': 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)
+      '@storybook/core': 8.5.0(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.4)
     optionalDependencies:
-      prettier: 3.3.3
+      prettier: 3.4.2
     transitivePeerDependencies:
       - bufferutil
       - supports-color
@@ -21854,11 +22236,11 @@ snapshots:
       '@tokenizer/token': 0.3.0
       peek-readable: 5.3.1
 
-  stylehacks@6.1.1(postcss@8.4.49):
+  stylehacks@7.0.4(postcss@8.5.1):
     dependencies:
-      browserslist: 4.23.0
-      postcss: 8.4.49
-      postcss-selector-parser: 6.0.16
+      browserslist: 4.24.4
+      postcss: 8.5.1
+      postcss-selector-parser: 6.1.2
 
   supports-color@5.5.0:
     dependencies:
@@ -21881,7 +22263,7 @@ snapshots:
 
   supports-preserve-symlinks-flag@1.0.0: {}
 
-  svgo@3.2.0:
+  svgo@3.3.2:
     dependencies:
       '@trysound/sax': 0.2.0
       commander: 7.2.0
@@ -21889,11 +22271,11 @@ snapshots:
       css-tree: 2.3.1
       css-what: 6.1.0
       csso: 5.0.5
-      picocolors: 1.0.1
+      picocolors: 1.1.1
 
   symbol-tree@3.2.4: {}
 
-  systeminformation@5.23.5: {}
+  systeminformation@5.25.11: {}
 
   tar-stream@3.1.6:
     dependencies:
@@ -21925,7 +22307,7 @@ snapshots:
     dependencies:
       execa: 6.1.0
 
-  terser@5.36.0:
+  terser@5.37.0:
     dependencies:
       '@jridgewell/source-map': 0.3.6
       acorn: 8.14.0
@@ -21938,8 +22320,6 @@ snapshots:
       glob: 7.2.3
       minimatch: 3.1.2
 
-  text-table@0.2.0: {}
-
   textarea-caret@3.1.0: {}
 
   thenify-all@1.6.0:
@@ -21954,7 +22334,7 @@ snapshots:
     dependencies:
       real-require: 0.2.0
 
-  three@0.169.0: {}
+  three@0.172.0: {}
 
   throttle-debounce@5.0.2: {}
 
@@ -22031,8 +22411,6 @@ snapshots:
     dependencies:
       punycode: 2.3.1
 
-  trace-redirect@1.0.6: {}
-
   tree-kill@1.2.2: {}
 
   trim-lines@3.0.1: {}
@@ -22045,13 +22423,13 @@ snapshots:
 
   trough@2.2.0: {}
 
-  ts-api-utils@1.0.1(typescript@5.6.3):
+  ts-api-utils@1.3.0(typescript@5.6.3):
     dependencies:
       typescript: 5.6.3
 
-  ts-api-utils@1.3.0(typescript@5.6.3):
+  ts-api-utils@2.0.0(typescript@5.7.3):
     dependencies:
-      typescript: 5.6.3
+      typescript: 5.7.3
 
   ts-case-convert@2.1.0: {}
 
@@ -22097,10 +22475,12 @@ snapshots:
 
   tslib@2.7.0: {}
 
-  tsx@4.4.0:
+  tslib@2.8.1: {}
+
+  tsx@4.19.2:
     dependencies:
-      esbuild: 0.18.20
-      get-tsconfig: 4.7.2
+      esbuild: 0.23.1
+      get-tsconfig: 4.9.0
     optionalDependencies:
       fsevents: 2.3.3
 
@@ -22196,7 +22576,7 @@ snapshots:
 
   typedarray@0.0.6: {}
 
-  typeorm@0.3.20(ioredis@5.4.1)(pg@8.13.1):
+  typeorm@0.3.20(ioredis@5.4.2)(pg@8.13.1):
     dependencies:
       '@sqltools/formatter': 1.2.5
       app-root-path: 3.1.0
@@ -22214,15 +22594,17 @@ snapshots:
       uuid: 9.0.1
       yargs: 17.7.2
     optionalDependencies:
-      ioredis: 5.4.1
+      ioredis: 5.4.2
       pg: 8.13.1
     transitivePeerDependencies:
       - supports-color
 
-  typescript@5.4.2: {}
-
   typescript@5.6.3: {}
 
+  typescript@5.7.2: {}
+
+  typescript@5.7.3: {}
+
   ufo@1.3.2: {}
 
   uid2@0.0.4: {}
@@ -22246,7 +22628,9 @@ snapshots:
 
   undici-types@6.19.8: {}
 
-  undici@5.28.2:
+  undici-types@6.20.0: {}
+
+  undici@5.28.5:
     dependencies:
       '@fastify/busboy': 2.1.0
 
@@ -22312,17 +22696,17 @@ snapshots:
 
   untildify@4.0.0: {}
 
-  update-browserslist-db@1.0.13(browserslist@4.22.2):
-    dependencies:
-      browserslist: 4.22.2
-      escalade: 3.1.1
-      picocolors: 1.0.1
-
   update-browserslist-db@1.0.13(browserslist@4.23.0):
     dependencies:
       browserslist: 4.23.0
       escalade: 3.1.1
-      picocolors: 1.0.1
+      picocolors: 1.1.1
+
+  update-browserslist-db@1.1.2(browserslist@4.24.4):
+    dependencies:
+      browserslist: 4.24.4
+      escalade: 3.2.0
+      picocolors: 1.1.1
 
   uri-js@4.4.1:
     dependencies:
@@ -22357,17 +22741,19 @@ snapshots:
 
   uuid@10.0.0: {}
 
+  uuid@11.0.5: {}
+
   uuid@8.3.2: {}
 
   uuid@9.0.1: {}
 
-  v-code-diff@1.13.1(vue@3.5.12(typescript@5.6.3)):
+  v-code-diff@1.13.1(vue@3.5.13(typescript@5.7.3)):
     dependencies:
       diff: 5.2.0
       diff-match-patch: 1.0.5
       highlight.js: 11.10.0
-      vue: 3.5.12(typescript@5.6.3)
-      vue-demi: 0.14.7(vue@3.5.12(typescript@5.6.3))
+      vue: 3.5.13(typescript@5.7.3)
+      vue-demi: 0.14.7(vue@3.5.13(typescript@5.7.3))
 
   v8-to-istanbul@9.2.0:
     dependencies:
@@ -22401,31 +22787,13 @@ snapshots:
       unist-util-stringify-position: 4.0.0
       vfile-message: 4.0.2
 
-  vite-node@1.6.0(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0):
-    dependencies:
-      cac: 6.7.14
-      debug: 4.3.7(supports-color@8.1.1)
-      pathe: 1.1.2
-      picocolors: 1.0.1
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
-    transitivePeerDependencies:
-      - '@types/node'
-      - less
-      - lightningcss
-      - sass
-      - sass-embedded
-      - stylus
-      - sugarss
-      - supports-color
-      - terser
-
-  vite-node@1.6.0(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0):
+  vite-node@1.6.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0):
     dependencies:
       cac: 6.7.14
       debug: 4.3.7(supports-color@8.1.1)
       pathe: 1.1.2
       picocolors: 1.0.1
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0)
+      vite: 5.4.11(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -22439,72 +22807,37 @@ snapshots:
 
   vite-plugin-turbosnap@1.0.3: {}
 
-  vite@5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0):
+  vite@5.4.11(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0):
     dependencies:
       esbuild: 0.21.5
-      postcss: 8.4.49
-      rollup: 4.26.0
+      postcss: 8.5.1
+      rollup: 4.31.0
     optionalDependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       fsevents: 2.3.3
-      sass: 1.79.3
-      terser: 5.36.0
+      sass: 1.83.4
+      terser: 5.37.0
 
-  vite@5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0):
+  vite@6.0.7(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)(tsx@4.19.2):
     dependencies:
-      esbuild: 0.21.5
-      postcss: 8.4.49
-      rollup: 4.26.0
+      esbuild: 0.24.2
+      postcss: 8.5.1
+      rollup: 4.31.0
     optionalDependencies:
-      '@types/node': 22.9.0
+      '@types/node': 22.10.7
       fsevents: 2.3.3
-      sass: 1.79.4
-      terser: 5.36.0
+      sass: 1.83.4
+      terser: 5.37.0
+      tsx: 4.19.2
 
-  vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.36.0)):
+  vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0)):
     dependencies:
       cross-fetch: 3.1.6(encoding@0.1.13)
-      vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.36.0)
+      vitest: 1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0)
     transitivePeerDependencies:
       - encoding
 
-  vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.36.0):
-    dependencies:
-      '@vitest/expect': 1.6.0
-      '@vitest/runner': 1.6.0
-      '@vitest/snapshot': 1.6.0
-      '@vitest/spy': 1.6.0
-      '@vitest/utils': 1.6.0
-      acorn-walk: 8.3.2
-      chai: 4.3.10
-      debug: 4.3.4
-      execa: 8.0.1
-      local-pkg: 0.5.0
-      magic-string: 0.30.10
-      pathe: 1.1.2
-      picocolors: 1.0.0
-      std-env: 3.7.0
-      strip-literal: 2.1.0
-      tinybench: 2.6.0
-      tinypool: 0.8.4
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
-      vite-node: 1.6.0(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0)
-      why-is-node-running: 2.2.2
-    optionalDependencies:
-      '@types/node': 22.9.0
-      happy-dom: 10.0.3
-      jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
-    transitivePeerDependencies:
-      - less
-      - lightningcss
-      - sass
-      - sass-embedded
-      - stylus
-      - sugarss
-      - supports-color
-      - terser
-
-  vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0):
+  vitest@1.6.0(@types/node@22.10.7)(happy-dom@16.6.0)(jsdom@26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.83.4)(terser@5.37.0):
     dependencies:
       '@vitest/expect': 1.6.0
       '@vitest/runner': 1.6.0
@@ -22523,13 +22856,13 @@ snapshots:
       strip-literal: 2.1.0
       tinybench: 2.6.0
       tinypool: 0.8.4
-      vite: 5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0)
-      vite-node: 1.6.0(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0)
+      vite: 5.4.11(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)
+      vite-node: 1.6.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.37.0)
       why-is-node-running: 2.2.2
     optionalDependencies:
-      '@types/node': 22.9.0
-      happy-dom: 10.0.3
-      jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@types/node': 22.10.7
+      happy-dom: 16.6.0
+      jsdom: 26.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
       - less
       - lightningcss
@@ -22565,46 +22898,44 @@ snapshots:
 
   vscode-uri@3.0.8: {}
 
-  vue-component-meta@2.0.16(typescript@5.6.3):
+  vue-component-meta@2.0.16(typescript@5.7.3):
     dependencies:
       '@volar/typescript': 2.2.0
-      '@vue/language-core': 2.0.16(typescript@5.6.3)
+      '@vue/language-core': 2.0.16(typescript@5.7.3)
       path-browserify: 1.0.1
       vue-component-type-helpers: 2.0.16
     optionalDependencies:
-      typescript: 5.6.3
+      typescript: 5.7.3
 
   vue-component-type-helpers@1.8.4: {}
 
   vue-component-type-helpers@2.0.16: {}
 
-  vue-component-type-helpers@2.1.10: {}
-
   vue-component-type-helpers@2.2.0: {}
 
-  vue-demi@0.14.7(vue@3.5.12(typescript@5.6.3)):
+  vue-demi@0.14.7(vue@3.5.13(typescript@5.7.3)):
     dependencies:
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.13(typescript@5.7.3)
 
-  vue-docgen-api@4.75.1(vue@3.5.12(typescript@5.6.3)):
+  vue-docgen-api@4.75.1(vue@3.5.13(typescript@5.7.3)):
     dependencies:
       '@babel/parser': 7.25.6
       '@babel/types': 7.25.6
       '@vue/compiler-dom': 3.5.12
-      '@vue/compiler-sfc': 3.5.12
+      '@vue/compiler-sfc': 3.5.13
       ast-types: 0.16.1
       hash-sum: 2.0.0
       lru-cache: 8.0.4
       pug: 3.0.3
       recast: 0.23.6
       ts-map: 1.0.3
-      vue: 3.5.12(typescript@5.6.3)
-      vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.12(typescript@5.6.3))
+      vue: 3.5.13(typescript@5.7.3)
+      vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.13(typescript@5.7.3))
 
-  vue-eslint-parser@9.4.3(eslint@9.14.0):
+  vue-eslint-parser@9.4.3(eslint@9.18.0):
     dependencies:
       debug: 4.3.5
-      eslint: 9.14.0
+      eslint: 9.18.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -22614,21 +22945,20 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.12(typescript@5.6.3)):
+  vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.13(typescript@5.7.3)):
     dependencies:
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.13(typescript@5.7.3)
 
   vue-template-compiler@2.7.14:
     dependencies:
       de-indent: 1.0.2
       he: 1.2.0
 
-  vue-tsc@2.1.10(typescript@5.6.3):
+  vue-tsc@2.2.0(typescript@5.7.3):
     dependencies:
-      '@volar/typescript': 2.4.10
-      '@vue/language-core': 2.1.10(typescript@5.6.3)
-      semver: 7.6.3
-      typescript: 5.6.3
+      '@volar/typescript': 2.4.11
+      '@vue/language-core': 2.2.0(typescript@5.7.3)
+      typescript: 5.7.3
 
   vue@3.5.12(typescript@5.6.3):
     dependencies:
@@ -22640,18 +22970,28 @@ snapshots:
     optionalDependencies:
       typescript: 5.6.3
 
-  vuedraggable@4.1.0(vue@3.5.12(typescript@5.6.3)):
+  vue@3.5.13(typescript@5.7.3):
+    dependencies:
+      '@vue/compiler-dom': 3.5.13
+      '@vue/compiler-sfc': 3.5.13
+      '@vue/runtime-dom': 3.5.13
+      '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.7.3))
+      '@vue/shared': 3.5.13
+    optionalDependencies:
+      typescript: 5.7.3
+
+  vuedraggable@4.1.0(vue@3.5.13(typescript@5.7.3)):
     dependencies:
       sortablejs: 1.14.0
-      vue: 3.5.12(typescript@5.6.3)
+      vue: 3.5.13(typescript@5.7.3)
 
   w3c-xmlserializer@5.0.0:
     dependencies:
       xml-name-validator: 5.0.0
 
-  wait-on@8.0.1(debug@4.3.7):
+  wait-on@8.0.2(debug@4.4.0):
     dependencies:
-      axios: 1.7.7(debug@4.3.7)
+      axios: 1.7.9(debug@4.4.0)
       joi: 17.13.3
       lodash: 4.17.21
       minimist: 1.2.8
@@ -22694,10 +23034,6 @@ snapshots:
 
   webpack-virtual-modules@0.5.0: {}
 
-  whatwg-encoding@2.0.0:
-    dependencies:
-      iconv-lite: 0.6.3
-
   whatwg-encoding@3.1.1:
     dependencies:
       iconv-lite: 0.6.3
@@ -22706,7 +23042,7 @@ snapshots:
 
   whatwg-mimetype@4.0.0: {}
 
-  whatwg-url@14.0.0:
+  whatwg-url@14.1.0:
     dependencies:
       tr46: 5.0.0
       webidl-conversions: 7.0.0
-- 
cgit v1.2.3-freya


From be7e3b9a0cb81b78a744993fef2fa2fd2833fa9c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 9 Mar 2025 14:28:01 +0900
Subject: refactor(frontend): scripts -> utility

---
 packages/frontend/.storybook/preview.ts            |   4 +-
 packages/frontend/@types/theme.d.ts                |   2 +-
 packages/frontend/src/account.ts                   |   8 +-
 packages/frontend/src/aiscript/api.ts              | 125 ++++
 packages/frontend/src/aiscript/common.ts           |  16 +
 packages/frontend/src/aiscript/ui.ts               | 655 ++++++++++++++++
 packages/frontend/src/boot/common.ts               |  12 +-
 packages/frontend/src/boot/main-boot.ts            |  22 +-
 packages/frontend/src/boot/sub-boot.ts             |   2 +-
 packages/frontend/src/cache.ts                     |   4 +-
 packages/frontend/src/components/MkAbuseReport.vue |   2 +-
 .../frontend/src/components/MkAccountMoved.vue     |   2 +-
 .../src/components/MkAchievements.stories.impl.ts  |   2 +-
 .../frontend/src/components/MkAchievements.vue     |   4 +-
 packages/frontend/src/components/MkAnalogClock.vue |   2 +-
 .../src/components/MkAnnouncementDialog.vue        |   2 +-
 .../frontend/src/components/MkAntennaEditor.vue    |   6 +-
 packages/frontend/src/components/MkAsUi.vue        |   2 +-
 packages/frontend/src/components/MkAuthConfirm.vue |   4 +-
 .../src/components/MkAutocomplete.stories.impl.ts  |   2 +-
 .../frontend/src/components/MkAutocomplete.vue     |   8 +-
 packages/frontend/src/components/MkAvatars.vue     |   2 +-
 .../src/components/MkChannelFollowButton.vue       |   2 +-
 packages/frontend/src/components/MkChart.vue       |  12 +-
 packages/frontend/src/components/MkClickerGame.vue |   4 +-
 packages/frontend/src/components/MkCode.core.vue   |   2 +-
 packages/frontend/src/components/MkCode.vue        |   2 +-
 packages/frontend/src/components/MkContextMenu.vue |   2 +-
 .../frontend/src/components/MkCropperDialog.vue    |   2 +-
 packages/frontend/src/components/MkCwButton.vue    |   2 +-
 .../frontend/src/components/MkDigitalClock.vue     |   2 +-
 packages/frontend/src/components/MkDrive.file.vue  |   4 +-
 .../frontend/src/components/MkDrive.folder.vue     |   6 +-
 .../frontend/src/components/MkDrive.navFolder.vue  |   2 +-
 packages/frontend/src/components/MkDrive.vue       |   6 +-
 .../src/components/MkEmbedCodeGenDialog.vue        |   4 +-
 packages/frontend/src/components/MkEmojiPicker.vue |   6 +-
 .../frontend/src/components/MkFoldableSection.vue  |   2 +-
 packages/frontend/src/components/MkFolder.vue      |   2 +-
 .../frontend/src/components/MkFollowButton.vue     |   6 +-
 .../frontend/src/components/MkFormDialog.file.vue  |   4 +-
 packages/frontend/src/components/MkFormDialog.vue  |   2 +-
 packages/frontend/src/components/MkHeatmap.vue     |   8 +-
 .../frontend/src/components/MkHorizontalSwipe.vue  |   2 +-
 packages/frontend/src/components/MkInput.vue       |   4 +-
 .../frontend/src/components/MkInstanceCardMini.vue |   4 +-
 .../frontend/src/components/MkInstanceStats.vue    |   6 +-
 .../frontend/src/components/MkInstanceTicker.vue   |   2 +-
 packages/frontend/src/components/MkInviteCode.vue  |   2 +-
 packages/frontend/src/components/MkKeyValue.vue    |   2 +-
 packages/frontend/src/components/MkLaunchPad.vue   |   2 +-
 packages/frontend/src/components/MkLink.vue        |   2 +-
 packages/frontend/src/components/MkMediaAudio.vue  |   4 +-
 packages/frontend/src/components/MkMediaImage.vue  |   4 +-
 packages/frontend/src/components/MkMediaList.vue   |   2 +-
 packages/frontend/src/components/MkMediaVideo.vue  |   8 +-
 packages/frontend/src/components/MkMention.vue     |   2 +-
 packages/frontend/src/components/MkMenu.vue        |   8 +-
 packages/frontend/src/components/MkModal.vue       |  10 +-
 packages/frontend/src/components/MkNote.vue        |  34 +-
 .../frontend/src/components/MkNoteDetailed.vue     |  30 +-
 packages/frontend/src/components/MkNoteSub.vue     |   4 +-
 .../frontend/src/components/MkNotification.vue     |   4 +-
 packages/frontend/src/components/MkPageWindow.vue  |  10 +-
 packages/frontend/src/components/MkPagination.vue  |   2 +-
 packages/frontend/src/components/MkPoll.vue        |   8 +-
 packages/frontend/src/components/MkPollEditor.vue  |   4 +-
 packages/frontend/src/components/MkPostForm.vue    |  22 +-
 .../frontend/src/components/MkPostFormAttaches.vue |   4 +-
 .../frontend/src/components/MkPullToRefresh.vue    |   2 +-
 .../components/MkPushNotificationAllowButton.vue   |   2 +-
 packages/frontend/src/components/MkRange.vue       |   2 +-
 .../frontend/src/components/MkReactionIcon.vue     |   2 +-
 .../src/components/MkReactionsViewer.reaction.vue  |  10 +-
 .../frontend/src/components/MkRetentionHeatmap.vue |   8 +-
 .../src/components/MkRetentionLineChart.vue        |  10 +-
 .../frontend/src/components/MkRoleSelectDialog.vue |   2 +-
 .../frontend/src/components/MkSignin.input.vue     |   2 +-
 packages/frontend/src/components/MkSignin.vue      |   6 +-
 .../frontend/src/components/MkSigninDialog.vue     |   2 +-
 .../src/components/MkSignupDialog.form.vue         |   2 +-
 packages/frontend/src/components/MkSuperMenu.vue   |   4 +-
 .../src/components/MkSystemWebhookEditor.vue       |   2 +-
 packages/frontend/src/components/MkTextarea.vue    |   4 +-
 packages/frontend/src/components/MkTimeline.vue    |   2 +-
 packages/frontend/src/components/MkTooltip.vue     |   2 +-
 .../frontend/src/components/MkTutorialDialog.vue   |   2 +-
 packages/frontend/src/components/MkUpdated.vue     |   2 +-
 packages/frontend/src/components/MkUrlPreview.vue  |   4 +-
 .../components/MkUserAnnouncementEditDialog.vue    |   2 +-
 .../frontend/src/components/MkUserCardMini.vue     |   2 +-
 packages/frontend/src/components/MkUserInfo.vue    |   4 +-
 packages/frontend/src/components/MkUserPopup.vue   |   8 +-
 .../frontend/src/components/MkUserSelectDialog.vue |   2 +-
 .../src/components/MkUserSetupDialog.Privacy.vue   |   2 +-
 .../src/components/MkUserSetupDialog.Profile.vue   |   2 +-
 .../src/components/MkUserSetupDialog.User.vue      |   2 +-
 .../MkVisitorDashboard.ActiveUsersChart.vue        |   8 +-
 .../frontend/src/components/MkVisitorDashboard.vue |   2 +-
 packages/frontend/src/components/MkWindow.vue      |   2 +-
 .../frontend/src/components/MkYouTubePlayer.vue    |   2 +-
 .../src/components/global/MkA.stories.impl.ts      |   2 +-
 packages/frontend/src/components/global/MkA.vue    |   2 +-
 .../frontend/src/components/global/MkAvatar.vue    |   2 +-
 .../src/components/global/MkCustomEmoji.vue        |   8 +-
 .../frontend/src/components/global/MkEmoji.vue     |   4 +-
 .../src/components/global/MkPageHeader.vue         |   4 +-
 .../frontend/src/components/global/MkSpacer.vue    |   2 +-
 packages/frontend/src/components/global/MkUrl.vue  |   2 +-
 .../frontend/src/components/grid/MkDataCell.vue    |   2 +-
 packages/frontend/src/components/grid/MkGrid.vue   |   2 +-
 .../frontend/src/components/grid/grid-utils.ts     |   2 +-
 .../frontend/src/components/page/page.note.vue     |   2 +-
 .../frontend/src/components/page/page.text.vue     |   2 +-
 packages/frontend/src/custom-emojis.ts             |   4 +-
 packages/frontend/src/deck.ts                      |   4 +-
 packages/frontend/src/directives/adaptive-bg.ts    |   2 +-
 .../frontend/src/directives/adaptive-border.ts     |   2 +-
 packages/frontend/src/directives/hotkey.ts         |   2 +-
 packages/frontend/src/directives/panel.ts          |   2 +-
 packages/frontend/src/directives/tooltip.ts        |   2 +-
 packages/frontend/src/instance.ts                  |   2 +-
 packages/frontend/src/navbar.ts                    |   6 +-
 packages/frontend/src/os.ts                        |  14 +-
 packages/frontend/src/pages/_error_.vue            |   6 +-
 packages/frontend/src/pages/about-misskey.vue      |   6 +-
 packages/frontend/src/pages/about.overview.vue     |   2 +-
 packages/frontend/src/pages/about.vue              |   4 +-
 packages/frontend/src/pages/achievements.vue       |   4 +-
 packages/frontend/src/pages/admin-file.vue         |   4 +-
 packages/frontend/src/pages/admin-user.vue         |   4 +-
 .../src/pages/admin/RolesEditorFormula.vue         |   2 +-
 packages/frontend/src/pages/admin/_header_.vue     |   2 +-
 .../abuse-report/notification-recipient.editor.vue |   2 +-
 .../admin/abuse-report/notification-recipient.vue  |   2 +-
 packages/frontend/src/pages/admin/abuses.vue       |   2 +-
 packages/frontend/src/pages/admin/ads.vue          |   4 +-
 .../frontend/src/pages/admin/announcements.vue     |   4 +-
 .../frontend/src/pages/admin/bot-protection.vue    |   4 +-
 packages/frontend/src/pages/admin/branding.vue     |   4 +-
 .../admin/custom-emojis-manager.local.list.vue     |   4 +-
 .../admin/custom-emojis-manager.local.register.vue |  10 +-
 .../pages/admin/custom-emojis-manager.remote.vue   |   4 +-
 .../src/pages/admin/custom-emojis-manager2.vue     |   2 +-
 packages/frontend/src/pages/admin/database.vue     |   4 +-
 .../frontend/src/pages/admin/email-settings.vue    |   4 +-
 .../frontend/src/pages/admin/external-services.vue |   4 +-
 packages/frontend/src/pages/admin/federation.vue   |   2 +-
 packages/frontend/src/pages/admin/files.vue        |   4 +-
 packages/frontend/src/pages/admin/index.vue        |  10 +-
 packages/frontend/src/pages/admin/invites.vue      |   4 +-
 packages/frontend/src/pages/admin/moderation.vue   |   4 +-
 packages/frontend/src/pages/admin/modlog.vue       |   2 +-
 .../frontend/src/pages/admin/object-storage.vue    |   4 +-
 .../src/pages/admin/overview.active-users.vue      |   8 +-
 .../src/pages/admin/overview.ap-requests.vue       |  10 +-
 .../src/pages/admin/overview.federation.vue        |   4 +-
 .../src/pages/admin/overview.instances.vue         |   2 +-
 .../src/pages/admin/overview.moderators.vue        |   2 +-
 packages/frontend/src/pages/admin/overview.pie.vue |   4 +-
 .../src/pages/admin/overview.queue.chart.vue       |   8 +-
 .../frontend/src/pages/admin/overview.stats.vue    |   2 +-
 .../frontend/src/pages/admin/overview.users.vue    |   2 +-
 packages/frontend/src/pages/admin/overview.vue     |   4 +-
 packages/frontend/src/pages/admin/performance.vue  |   6 +-
 .../frontend/src/pages/admin/queue.chart.chart.vue |   8 +-
 packages/frontend/src/pages/admin/queue.chart.vue  |   2 +-
 packages/frontend/src/pages/admin/queue.vue        |   2 +-
 packages/frontend/src/pages/admin/relays.vue       |   4 +-
 packages/frontend/src/pages/admin/roles.edit.vue   |   4 +-
 packages/frontend/src/pages/admin/roles.editor.vue |   2 +-
 packages/frontend/src/pages/admin/roles.role.vue   |   4 +-
 packages/frontend/src/pages/admin/roles.vue        |   4 +-
 packages/frontend/src/pages/admin/security.vue     |   6 +-
 packages/frontend/src/pages/admin/server-rules.vue |   2 +-
 packages/frontend/src/pages/admin/settings.vue     |   6 +-
 .../frontend/src/pages/admin/system-webhook.vue    |   4 +-
 packages/frontend/src/pages/admin/users.vue        |   4 +-
 packages/frontend/src/pages/ads.vue                |   2 +-
 packages/frontend/src/pages/announcement.vue       |   4 +-
 packages/frontend/src/pages/announcements.vue      |   4 +-
 packages/frontend/src/pages/antenna-timeline.vue   |   4 +-
 packages/frontend/src/pages/api-console.vue        |   4 +-
 packages/frontend/src/pages/auth.form.vue          |   2 +-
 packages/frontend/src/pages/auth.vue               |   4 +-
 .../src/pages/avatar-decoration-edit-dialog.vue    |   2 +-
 packages/frontend/src/pages/avatar-decorations.vue |   4 +-
 packages/frontend/src/pages/channel-editor.vue     |   6 +-
 packages/frontend/src/pages/channel.vue            |  12 +-
 packages/frontend/src/pages/channels.vue           |   2 +-
 packages/frontend/src/pages/clicker.vue            |   2 +-
 packages/frontend/src/pages/clip.vue               |  10 +-
 packages/frontend/src/pages/contact.vue            |   2 +-
 .../frontend/src/pages/custom-emojis-manager.vue   |   8 +-
 packages/frontend/src/pages/drive.file.info.vue    |   2 +-
 packages/frontend/src/pages/drive.file.vue         |   2 +-
 packages/frontend/src/pages/drive.vue              |   2 +-
 .../frontend/src/pages/drop-and-fusion.game.vue    |  10 +-
 packages/frontend/src/pages/drop-and-fusion.vue    |   4 +-
 packages/frontend/src/pages/emoji-edit-dialog.vue  |   4 +-
 packages/frontend/src/pages/emojis.emoji.vue       |   4 +-
 packages/frontend/src/pages/explore.roles.vue      |   2 +-
 packages/frontend/src/pages/explore.users.vue      |   2 +-
 packages/frontend/src/pages/explore.vue            |   2 +-
 packages/frontend/src/pages/favorites.vue          |   2 +-
 packages/frontend/src/pages/flash/flash-edit.vue   |   4 +-
 packages/frontend/src/pages/flash/flash-index.vue  |   2 +-
 packages/frontend/src/pages/flash/flash.vue        |  16 +-
 packages/frontend/src/pages/follow-requests.vue    |   2 +-
 packages/frontend/src/pages/gallery/edit.vue       |   6 +-
 packages/frontend/src/pages/gallery/index.vue      |   2 +-
 packages/frontend/src/pages/gallery/post.vue       |   8 +-
 packages/frontend/src/pages/games.vue              |   2 +-
 packages/frontend/src/pages/install-extensions.vue |   8 +-
 packages/frontend/src/pages/instance-info.vue      |   6 +-
 packages/frontend/src/pages/invite.vue             |   4 +-
 packages/frontend/src/pages/list.vue               |   4 +-
 packages/frontend/src/pages/lookup.vue             |   4 +-
 packages/frontend/src/pages/miauth.vue             |   4 +-
 packages/frontend/src/pages/my-antennas/create.vue |   2 +-
 packages/frontend/src/pages/my-antennas/edit.vue   |   4 +-
 packages/frontend/src/pages/my-antennas/index.vue  |   2 +-
 packages/frontend/src/pages/my-clips/index.vue     |   4 +-
 packages/frontend/src/pages/my-lists/index.vue     |   2 +-
 packages/frontend/src/pages/my-lists/list.vue      |   4 +-
 packages/frontend/src/pages/not-found.vue          |   4 +-
 packages/frontend/src/pages/note.vue               |   8 +-
 packages/frontend/src/pages/notifications.vue      |   2 +-
 packages/frontend/src/pages/oauth.vue              |   2 +-
 .../pages/page-editor/els/page-editor.el.image.vue |   2 +-
 .../pages/page-editor/els/page-editor.el.note.vue  |   2 +-
 .../page-editor/els/page-editor.el.section.vue     |   4 +-
 .../pages/page-editor/els/page-editor.el.text.vue  |   4 +-
 .../frontend/src/pages/page-editor/page-editor.vue |   6 +-
 packages/frontend/src/pages/page.vue               |  12 +-
 packages/frontend/src/pages/pages.vue              |   2 +-
 packages/frontend/src/pages/preview.vue            |   2 +-
 packages/frontend/src/pages/registry.keys.vue      |   4 +-
 packages/frontend/src/pages/registry.value.vue     |   4 +-
 packages/frontend/src/pages/registry.vue           |   4 +-
 packages/frontend/src/pages/reset-password.vue     |   2 +-
 packages/frontend/src/pages/reversi/game.board.vue |   8 +-
 .../frontend/src/pages/reversi/game.setting.vue    |   2 +-
 packages/frontend/src/pages/reversi/game.vue       |   4 +-
 packages/frontend/src/pages/reversi/index.vue      |   8 +-
 packages/frontend/src/pages/role.vue               |   4 +-
 packages/frontend/src/pages/scratchpad.vue         |  12 +-
 packages/frontend/src/pages/search.note.vue        |   4 +-
 packages/frontend/src/pages/search.user.vue        |   2 +-
 packages/frontend/src/pages/search.vue             |   4 +-
 .../frontend/src/pages/settings/2fa.qrdialog.vue   |   2 +-
 .../frontend/src/pages/settings/accessibility.vue  |   4 +-
 packages/frontend/src/pages/settings/accounts.vue  |   4 +-
 packages/frontend/src/pages/settings/api.vue       |   4 +-
 .../frontend/src/pages/settings/appearance.vue     |   6 +-
 packages/frontend/src/pages/settings/apps.vue      |   4 +-
 .../src/pages/settings/avatar-decoration.vue       |   4 +-
 .../frontend/src/pages/settings/custom-css.vue     |   4 +-
 packages/frontend/src/pages/settings/deck.vue      |   2 +-
 .../frontend/src/pages/settings/drive-cleaner.vue  |   6 +-
 packages/frontend/src/pages/settings/drive.vue     |   4 +-
 packages/frontend/src/pages/settings/email.vue     |   4 +-
 .../frontend/src/pages/settings/emoji-picker.vue   |   8 +-
 .../frontend/src/pages/settings/import-export.vue  |   6 +-
 packages/frontend/src/pages/settings/index.vue     |   8 +-
 packages/frontend/src/pages/settings/migration.vue |   4 +-
 .../pages/settings/mute-block.instance-mute.vue    |   2 +-
 .../frontend/src/pages/settings/mute-block.vue     |   4 +-
 packages/frontend/src/pages/settings/navbar.vue    |   4 +-
 .../frontend/src/pages/settings/notifications.vue  |   4 +-
 packages/frontend/src/pages/settings/other.vue     |   6 +-
 .../frontend/src/pages/settings/plugin.install.vue |   4 +-
 packages/frontend/src/pages/settings/plugin.vue    |   6 +-
 .../frontend/src/pages/settings/preferences.vue    |   6 +-
 packages/frontend/src/pages/settings/privacy.vue   |   6 +-
 packages/frontend/src/pages/settings/profile.vue   |   8 +-
 packages/frontend/src/pages/settings/roles.vue     |   2 +-
 packages/frontend/src/pages/settings/security.vue  |   4 +-
 .../frontend/src/pages/settings/sounds.sound.vue   |   8 +-
 packages/frontend/src/pages/settings/sounds.vue    |   6 +-
 .../src/pages/settings/statusbar.statusbar.vue     |   2 +-
 packages/frontend/src/pages/settings/statusbar.vue |   4 +-
 .../frontend/src/pages/settings/theme.install.vue  |   4 +-
 .../frontend/src/pages/settings/theme.manage.vue   |   8 +-
 packages/frontend/src/pages/settings/theme.vue     |  12 +-
 .../frontend/src/pages/settings/webhook.edit.vue   |   4 +-
 .../frontend/src/pages/settings/webhook.new.vue    |   2 +-
 packages/frontend/src/pages/settings/webhook.vue   |   2 +-
 packages/frontend/src/pages/share.vue              |   6 +-
 packages/frontend/src/pages/signup-complete.vue    |   2 +-
 packages/frontend/src/pages/tag.vue                |   4 +-
 packages/frontend/src/pages/theme-editor.vue       |   8 +-
 packages/frontend/src/pages/timeline.vue           |   8 +-
 packages/frontend/src/pages/user-list-timeline.vue |   4 +-
 packages/frontend/src/pages/user-tag.vue           |   2 +-
 packages/frontend/src/pages/user/achievements.vue  |   2 +-
 .../frontend/src/pages/user/activity.following.vue |  10 +-
 .../frontend/src/pages/user/activity.notes.vue     |  10 +-
 packages/frontend/src/pages/user/activity.pv.vue   |  10 +-
 packages/frontend/src/pages/user/followers.vue     |   4 +-
 packages/frontend/src/pages/user/following.vue     |   4 +-
 packages/frontend/src/pages/user/home.vue          |  10 +-
 packages/frontend/src/pages/user/index.files.vue   |   2 +-
 packages/frontend/src/pages/user/index.vue         |   4 +-
 packages/frontend/src/pages/welcome.entrance.a.vue |   4 +-
 packages/frontend/src/pages/welcome.setup.vue      |   2 +-
 packages/frontend/src/pages/welcome.timeline.vue   |   2 +-
 packages/frontend/src/pages/welcome.vue            |   2 +-
 packages/frontend/src/pizzax.ts                    |   8 +-
 packages/frontend/src/plugin.ts                    |   4 +-
 packages/frontend/src/preferences/def.ts           |   6 +-
 packages/frontend/src/preferences/profile.ts       |   2 +-
 packages/frontend/src/preferences/utility.ts       |   6 +-
 packages/frontend/src/scripts/achievements.ts      | 513 -------------
 packages/frontend/src/scripts/admin-lookup.ts      |  88 ---
 packages/frontend/src/scripts/aiscript/api.ts      | 125 ----
 packages/frontend/src/scripts/aiscript/common.ts   |  16 -
 packages/frontend/src/scripts/aiscript/ui.ts       | 655 ----------------
 packages/frontend/src/scripts/array.ts             | 118 ---
 packages/frontend/src/scripts/autocomplete.ts      | 323 --------
 .../src/scripts/autogen/settings-search-index.ts   | 822 ---------------------
 packages/frontend/src/scripts/cache.ts             |  53 --
 packages/frontend/src/scripts/chart-legend.ts      |  17 -
 packages/frontend/src/scripts/chart-vline.ts       |  28 -
 packages/frontend/src/scripts/check-permissions.ts |  19 -
 .../src/scripts/check-reaction-permissions.ts      |  17 -
 packages/frontend/src/scripts/check-word-mute.ts   |  43 --
 packages/frontend/src/scripts/clear-cache.ts       |  24 -
 packages/frontend/src/scripts/clicker-game.ts      |  73 --
 packages/frontend/src/scripts/clone.ts             |  25 -
 packages/frontend/src/scripts/code-highlighter.ts  |  99 ---
 packages/frontend/src/scripts/collect-page-vars.ts |  73 --
 packages/frontend/src/scripts/color.ts             |  12 -
 packages/frontend/src/scripts/confetti.ts          |  30 -
 packages/frontend/src/scripts/contains.ts          |  14 -
 packages/frontend/src/scripts/copy-to-clipboard.ts |  11 -
 packages/frontend/src/scripts/device-kind.ts       |  24 -
 packages/frontend/src/scripts/emoji-picker.ts      |  61 --
 packages/frontend/src/scripts/extract-mentions.ts  |  16 -
 .../frontend/src/scripts/extract-url-from-mfm.ts   |  24 -
 packages/frontend/src/scripts/file-drop.ts         | 121 ---
 packages/frontend/src/scripts/focus-trap.ts        | 134 ----
 packages/frontend/src/scripts/focus.ts             |  80 --
 packages/frontend/src/scripts/form.ts              | 104 ---
 .../frontend/src/scripts/format-time-string.ts     |  55 --
 packages/frontend/src/scripts/fullscreen.ts        |  46 --
 .../frontend/src/scripts/get-account-from-id.ts    |  12 -
 packages/frontend/src/scripts/get-appear-note.ts   |  10 -
 packages/frontend/src/scripts/get-bg-color.ts      |  18 -
 .../frontend/src/scripts/get-dom-node-or-null.ts   |  19 -
 .../frontend/src/scripts/get-drive-file-menu.ts    | 162 ----
 packages/frontend/src/scripts/get-embed-code.ts    |  87 ---
 packages/frontend/src/scripts/get-note-menu.ts     | 685 -----------------
 packages/frontend/src/scripts/get-note-summary.ts  |  64 --
 packages/frontend/src/scripts/get-user-menu.ts     | 441 -----------
 packages/frontend/src/scripts/get-user-name.ts     |   8 -
 packages/frontend/src/scripts/hotkey.ts            | 172 -----
 packages/frontend/src/scripts/idb-proxy.ts         |  53 --
 packages/frontend/src/scripts/idle-render.ts       |  60 --
 packages/frontend/src/scripts/init-chart.ts        |  58 --
 packages/frontend/src/scripts/initialize-sw.ts     |  18 -
 packages/frontend/src/scripts/intl-const.ts        |  50 --
 packages/frontend/src/scripts/intl-string.ts       |  97 ---
 .../frontend/src/scripts/is-device-darkmode.ts     |   8 -
 packages/frontend/src/scripts/isFfVisibleForMe.ts  |  24 -
 packages/frontend/src/scripts/key-event.ts         | 153 ----
 packages/frontend/src/scripts/langmap.ts           | 671 -----------------
 packages/frontend/src/scripts/login-id.ts          |  16 -
 packages/frontend/src/scripts/lookup.ts            |  84 ---
 packages/frontend/src/scripts/media-has-audio.ts   |  14 -
 packages/frontend/src/scripts/media-proxy.ts       |  34 -
 packages/frontend/src/scripts/merge.ts             |  35 -
 .../frontend/src/scripts/mfm-function-picker.ts    |  57 --
 packages/frontend/src/scripts/misskey-api.ts       | 116 ---
 packages/frontend/src/scripts/navigator.ts         |   8 -
 packages/frontend/src/scripts/page-metadata.ts     |  71 --
 packages/frontend/src/scripts/physics.ts           | 157 ----
 .../frontend/src/scripts/player-url-transform.ts   |  26 -
 packages/frontend/src/scripts/please-login.ts      |  76 --
 packages/frontend/src/scripts/popout.ts            |  28 -
 packages/frontend/src/scripts/popup-position.ts    | 161 ----
 packages/frontend/src/scripts/post-message.ts      |  25 -
 packages/frontend/src/scripts/reaction-picker.ts   |  54 --
 packages/frontend/src/scripts/reload-ask.ts        |  40 -
 packages/frontend/src/scripts/search-emoji.ts      | 106 ---
 packages/frontend/src/scripts/select-file.ts       | 130 ----
 packages/frontend/src/scripts/show-moved-dialog.ts |  21 -
 .../frontend/src/scripts/show-suspended-dialog.ts  |  15 -
 packages/frontend/src/scripts/shuffle.ts           |  25 -
 packages/frontend/src/scripts/snowfall-effect.ts   | 490 ------------
 packages/frontend/src/scripts/sound.ts             | 258 -------
 packages/frontend/src/scripts/sticky-sidebar.ts    |  55 --
 packages/frontend/src/scripts/stream-mock.ts       |  81 --
 packages/frontend/src/scripts/test-utils.ts        |   9 -
 packages/frontend/src/scripts/theme-editor.ts      |  87 ---
 packages/frontend/src/scripts/theme.ts             | 189 -----
 packages/frontend/src/scripts/time.ts              |  45 --
 packages/frontend/src/scripts/timezones.ts         |  54 --
 packages/frontend/src/scripts/touch.ts             |  22 -
 packages/frontend/src/scripts/unison-reload.ts     |  20 -
 packages/frontend/src/scripts/upload.ts            | 162 ----
 .../frontend/src/scripts/upload/compress-config.ts |  36 -
 .../frontend/src/scripts/upload/isWebpSupported.ts |  15 -
 packages/frontend/src/scripts/use-chart-tooltip.ts |  63 --
 packages/frontend/src/scripts/use-form.ts          |  56 --
 packages/frontend/src/scripts/use-leave-guard.ts   |  50 --
 packages/frontend/src/scripts/use-note-capture.ts  | 124 ----
 packages/frontend/src/scripts/use-tooltip.ts       | 106 ---
 packages/frontend/src/store.ts                     |   4 +-
 packages/frontend/src/stream.ts                    |   2 +-
 packages/frontend/src/theme-store.ts               |   4 +-
 packages/frontend/src/ui/_common_/common.vue       |   6 +-
 packages/frontend/src/ui/_common_/navbar.vue       |   2 +-
 .../src/ui/_common_/statusbar-federation.vue       |   4 +-
 .../frontend/src/ui/_common_/statusbar-rss.vue     |   2 +-
 .../src/ui/_common_/statusbar-user-list.vue        |   4 +-
 packages/frontend/src/ui/_common_/sw-inject.ts     |   6 +-
 packages/frontend/src/ui/_common_/upload.vue       |   2 +-
 packages/frontend/src/ui/classic.sidebar.vue       |   2 +-
 packages/frontend/src/ui/classic.vue               |   6 +-
 packages/frontend/src/ui/deck.vue                  |   4 +-
 packages/frontend/src/ui/deck/antenna-column.vue   |   4 +-
 packages/frontend/src/ui/deck/channel-column.vue   |   4 +-
 packages/frontend/src/ui/deck/list-column.vue      |   4 +-
 packages/frontend/src/ui/deck/main-column.vue      |   4 +-
 .../frontend/src/ui/deck/role-timeline-column.vue  |   4 +-
 packages/frontend/src/ui/deck/tl-column.vue        |   2 +-
 .../frontend/src/ui/deck/tl-note-notification.ts   |   4 +-
 packages/frontend/src/ui/minimum.vue               |   4 +-
 packages/frontend/src/ui/universal.vue             |   6 +-
 packages/frontend/src/ui/visitor.vue               |   4 +-
 packages/frontend/src/ui/zen.vue                   |   4 +-
 packages/frontend/src/utility/achievements.ts      | 513 +++++++++++++
 packages/frontend/src/utility/admin-lookup.ts      |  88 +++
 packages/frontend/src/utility/array.ts             | 118 +++
 packages/frontend/src/utility/autocomplete.ts      | 323 ++++++++
 .../src/utility/autogen/settings-search-index.ts   | 822 +++++++++++++++++++++
 packages/frontend/src/utility/cache.ts             |  53 ++
 packages/frontend/src/utility/chart-legend.ts      |  17 +
 packages/frontend/src/utility/chart-vline.ts       |  28 +
 packages/frontend/src/utility/check-permissions.ts |  19 +
 .../src/utility/check-reaction-permissions.ts      |  17 +
 packages/frontend/src/utility/check-word-mute.ts   |  43 ++
 packages/frontend/src/utility/clear-cache.ts       |  24 +
 packages/frontend/src/utility/clicker-game.ts      |  73 ++
 packages/frontend/src/utility/clone.ts             |  25 +
 packages/frontend/src/utility/code-highlighter.ts  |  99 +++
 packages/frontend/src/utility/collect-page-vars.ts |  73 ++
 packages/frontend/src/utility/color.ts             |  12 +
 packages/frontend/src/utility/confetti.ts          |  30 +
 packages/frontend/src/utility/contains.ts          |  14 +
 packages/frontend/src/utility/copy-to-clipboard.ts |  11 +
 packages/frontend/src/utility/device-kind.ts       |  24 +
 packages/frontend/src/utility/emoji-picker.ts      |  61 ++
 packages/frontend/src/utility/extract-mentions.ts  |  16 +
 .../frontend/src/utility/extract-url-from-mfm.ts   |  24 +
 packages/frontend/src/utility/file-drop.ts         | 121 +++
 packages/frontend/src/utility/focus-trap.ts        | 134 ++++
 packages/frontend/src/utility/focus.ts             |  80 ++
 packages/frontend/src/utility/form.ts              | 104 +++
 .../frontend/src/utility/format-time-string.ts     |  55 ++
 packages/frontend/src/utility/fullscreen.ts        |  46 ++
 .../frontend/src/utility/get-account-from-id.ts    |  12 +
 packages/frontend/src/utility/get-appear-note.ts   |  10 +
 packages/frontend/src/utility/get-bg-color.ts      |  18 +
 .../frontend/src/utility/get-dom-node-or-null.ts   |  19 +
 .../frontend/src/utility/get-drive-file-menu.ts    | 162 ++++
 packages/frontend/src/utility/get-embed-code.ts    |  87 +++
 packages/frontend/src/utility/get-note-menu.ts     | 685 +++++++++++++++++
 packages/frontend/src/utility/get-note-summary.ts  |  64 ++
 packages/frontend/src/utility/get-user-menu.ts     | 441 +++++++++++
 packages/frontend/src/utility/get-user-name.ts     |   8 +
 packages/frontend/src/utility/hotkey.ts            | 172 +++++
 packages/frontend/src/utility/idb-proxy.ts         |  53 ++
 packages/frontend/src/utility/idle-render.ts       |  60 ++
 packages/frontend/src/utility/init-chart.ts        |  58 ++
 packages/frontend/src/utility/initialize-sw.ts     |  18 +
 packages/frontend/src/utility/intl-const.ts        |  50 ++
 packages/frontend/src/utility/intl-string.ts       |  97 +++
 .../frontend/src/utility/is-device-darkmode.ts     |   8 +
 packages/frontend/src/utility/isFfVisibleForMe.ts  |  24 +
 packages/frontend/src/utility/key-event.ts         | 153 ++++
 packages/frontend/src/utility/langmap.ts           | 671 +++++++++++++++++
 packages/frontend/src/utility/login-id.ts          |  16 +
 packages/frontend/src/utility/lookup.ts            |  84 +++
 packages/frontend/src/utility/media-has-audio.ts   |  14 +
 packages/frontend/src/utility/media-proxy.ts       |  34 +
 packages/frontend/src/utility/merge.ts             |  35 +
 .../frontend/src/utility/mfm-function-picker.ts    |  57 ++
 packages/frontend/src/utility/misskey-api.ts       | 116 +++
 packages/frontend/src/utility/navigator.ts         |   8 +
 packages/frontend/src/utility/page-metadata.ts     |  71 ++
 packages/frontend/src/utility/physics.ts           | 157 ++++
 .../frontend/src/utility/player-url-transform.ts   |  26 +
 packages/frontend/src/utility/please-login.ts      |  76 ++
 packages/frontend/src/utility/popout.ts            |  28 +
 packages/frontend/src/utility/popup-position.ts    | 161 ++++
 packages/frontend/src/utility/post-message.ts      |  25 +
 packages/frontend/src/utility/reaction-picker.ts   |  54 ++
 packages/frontend/src/utility/reload-ask.ts        |  40 +
 packages/frontend/src/utility/search-emoji.ts      | 106 +++
 packages/frontend/src/utility/select-file.ts       | 130 ++++
 packages/frontend/src/utility/show-moved-dialog.ts |  21 +
 .../frontend/src/utility/show-suspended-dialog.ts  |  15 +
 packages/frontend/src/utility/shuffle.ts           |  25 +
 packages/frontend/src/utility/snowfall-effect.ts   | 490 ++++++++++++
 packages/frontend/src/utility/sound.ts             | 258 +++++++
 packages/frontend/src/utility/sticky-sidebar.ts    |  55 ++
 packages/frontend/src/utility/stream-mock.ts       |  81 ++
 packages/frontend/src/utility/test-utils.ts        |   9 +
 packages/frontend/src/utility/theme-editor.ts      |  87 +++
 packages/frontend/src/utility/theme.ts             | 189 +++++
 packages/frontend/src/utility/time.ts              |  45 ++
 packages/frontend/src/utility/timezones.ts         |  54 ++
 packages/frontend/src/utility/touch.ts             |  22 +
 packages/frontend/src/utility/unison-reload.ts     |  20 +
 packages/frontend/src/utility/upload.ts            | 162 ++++
 .../frontend/src/utility/upload/compress-config.ts |  36 +
 .../frontend/src/utility/upload/isWebpSupported.ts |  15 +
 packages/frontend/src/utility/use-chart-tooltip.ts |  63 ++
 packages/frontend/src/utility/use-form.ts          |  56 ++
 packages/frontend/src/utility/use-leave-guard.ts   |  50 ++
 packages/frontend/src/utility/use-note-capture.ts  | 124 ++++
 packages/frontend/src/utility/use-tooltip.ts       | 106 +++
 packages/frontend/src/widgets/WidgetActivity.vue   |   4 +-
 packages/frontend/src/widgets/WidgetAichan.vue     |   2 +-
 packages/frontend/src/widgets/WidgetAiscript.vue   |   4 +-
 .../frontend/src/widgets/WidgetAiscriptApp.vue     |   8 +-
 .../src/widgets/WidgetBirthdayFollowings.vue       |   4 +-
 packages/frontend/src/widgets/WidgetButton.vue     |   4 +-
 packages/frontend/src/widgets/WidgetCalendar.vue   |   2 +-
 packages/frontend/src/widgets/WidgetClicker.vue    |   2 +-
 packages/frontend/src/widgets/WidgetClock.vue      |   4 +-
 .../frontend/src/widgets/WidgetDigitalClock.vue    |   4 +-
 packages/frontend/src/widgets/WidgetFederation.vue |   6 +-
 .../frontend/src/widgets/WidgetInstanceCloud.vue   |   6 +-
 .../frontend/src/widgets/WidgetInstanceInfo.vue    |   2 +-
 packages/frontend/src/widgets/WidgetJobQueue.vue   |   6 +-
 packages/frontend/src/widgets/WidgetMemo.vue       |   2 +-
 .../frontend/src/widgets/WidgetNotifications.vue   |   2 +-
 .../frontend/src/widgets/WidgetOnlineUsers.vue     |   4 +-
 packages/frontend/src/widgets/WidgetPhotos.vue     |   6 +-
 packages/frontend/src/widgets/WidgetPostForm.vue   |   2 +-
 packages/frontend/src/widgets/WidgetProfile.vue    |   2 +-
 packages/frontend/src/widgets/WidgetRss.vue        |   2 +-
 packages/frontend/src/widgets/WidgetRssTicker.vue  |   4 +-
 packages/frontend/src/widgets/WidgetSlideshow.vue  |   4 +-
 packages/frontend/src/widgets/WidgetTimeline.vue   |   4 +-
 packages/frontend/src/widgets/WidgetTrends.vue     |   4 +-
 packages/frontend/src/widgets/WidgetUnixClock.vue  |   2 +-
 packages/frontend/src/widgets/WidgetUserList.vue   |   4 +-
 .../frontend/src/widgets/server-metric/index.vue   |   4 +-
 packages/frontend/src/widgets/widget.ts            |   4 +-
 packages/frontend/test/aiscript/api.test.ts        |   4 +-
 packages/frontend/test/aiscript/common.test.ts     |   2 +-
 packages/frontend/test/aiscript/ui.test.ts         |   4 +-
 packages/frontend/test/autocomplete.test.ts        |   2 +-
 packages/frontend/test/intl-string.test.ts         |   2 +-
 packages/frontend/vite.config.ts                   |   2 +-
 559 files changed, 10755 insertions(+), 10755 deletions(-)
 create mode 100644 packages/frontend/src/aiscript/api.ts
 create mode 100644 packages/frontend/src/aiscript/common.ts
 create mode 100644 packages/frontend/src/aiscript/ui.ts
 delete mode 100644 packages/frontend/src/scripts/achievements.ts
 delete mode 100644 packages/frontend/src/scripts/admin-lookup.ts
 delete mode 100644 packages/frontend/src/scripts/aiscript/api.ts
 delete mode 100644 packages/frontend/src/scripts/aiscript/common.ts
 delete mode 100644 packages/frontend/src/scripts/aiscript/ui.ts
 delete mode 100644 packages/frontend/src/scripts/array.ts
 delete mode 100644 packages/frontend/src/scripts/autocomplete.ts
 delete mode 100644 packages/frontend/src/scripts/autogen/settings-search-index.ts
 delete mode 100644 packages/frontend/src/scripts/cache.ts
 delete mode 100644 packages/frontend/src/scripts/chart-legend.ts
 delete mode 100644 packages/frontend/src/scripts/chart-vline.ts
 delete mode 100644 packages/frontend/src/scripts/check-permissions.ts
 delete mode 100644 packages/frontend/src/scripts/check-reaction-permissions.ts
 delete mode 100644 packages/frontend/src/scripts/check-word-mute.ts
 delete mode 100644 packages/frontend/src/scripts/clear-cache.ts
 delete mode 100644 packages/frontend/src/scripts/clicker-game.ts
 delete mode 100644 packages/frontend/src/scripts/clone.ts
 delete mode 100644 packages/frontend/src/scripts/code-highlighter.ts
 delete mode 100644 packages/frontend/src/scripts/collect-page-vars.ts
 delete mode 100644 packages/frontend/src/scripts/color.ts
 delete mode 100644 packages/frontend/src/scripts/confetti.ts
 delete mode 100644 packages/frontend/src/scripts/contains.ts
 delete mode 100644 packages/frontend/src/scripts/copy-to-clipboard.ts
 delete mode 100644 packages/frontend/src/scripts/device-kind.ts
 delete mode 100644 packages/frontend/src/scripts/emoji-picker.ts
 delete mode 100644 packages/frontend/src/scripts/extract-mentions.ts
 delete mode 100644 packages/frontend/src/scripts/extract-url-from-mfm.ts
 delete mode 100644 packages/frontend/src/scripts/file-drop.ts
 delete mode 100644 packages/frontend/src/scripts/focus-trap.ts
 delete mode 100644 packages/frontend/src/scripts/focus.ts
 delete mode 100644 packages/frontend/src/scripts/form.ts
 delete mode 100644 packages/frontend/src/scripts/format-time-string.ts
 delete mode 100644 packages/frontend/src/scripts/fullscreen.ts
 delete mode 100644 packages/frontend/src/scripts/get-account-from-id.ts
 delete mode 100644 packages/frontend/src/scripts/get-appear-note.ts
 delete mode 100644 packages/frontend/src/scripts/get-bg-color.ts
 delete mode 100644 packages/frontend/src/scripts/get-dom-node-or-null.ts
 delete mode 100644 packages/frontend/src/scripts/get-drive-file-menu.ts
 delete mode 100644 packages/frontend/src/scripts/get-embed-code.ts
 delete mode 100644 packages/frontend/src/scripts/get-note-menu.ts
 delete mode 100644 packages/frontend/src/scripts/get-note-summary.ts
 delete mode 100644 packages/frontend/src/scripts/get-user-menu.ts
 delete mode 100644 packages/frontend/src/scripts/get-user-name.ts
 delete mode 100644 packages/frontend/src/scripts/hotkey.ts
 delete mode 100644 packages/frontend/src/scripts/idb-proxy.ts
 delete mode 100644 packages/frontend/src/scripts/idle-render.ts
 delete mode 100644 packages/frontend/src/scripts/init-chart.ts
 delete mode 100644 packages/frontend/src/scripts/initialize-sw.ts
 delete mode 100644 packages/frontend/src/scripts/intl-const.ts
 delete mode 100644 packages/frontend/src/scripts/intl-string.ts
 delete mode 100644 packages/frontend/src/scripts/is-device-darkmode.ts
 delete mode 100644 packages/frontend/src/scripts/isFfVisibleForMe.ts
 delete mode 100644 packages/frontend/src/scripts/key-event.ts
 delete mode 100644 packages/frontend/src/scripts/langmap.ts
 delete mode 100644 packages/frontend/src/scripts/login-id.ts
 delete mode 100644 packages/frontend/src/scripts/lookup.ts
 delete mode 100644 packages/frontend/src/scripts/media-has-audio.ts
 delete mode 100644 packages/frontend/src/scripts/media-proxy.ts
 delete mode 100644 packages/frontend/src/scripts/merge.ts
 delete mode 100644 packages/frontend/src/scripts/mfm-function-picker.ts
 delete mode 100644 packages/frontend/src/scripts/misskey-api.ts
 delete mode 100644 packages/frontend/src/scripts/navigator.ts
 delete mode 100644 packages/frontend/src/scripts/page-metadata.ts
 delete mode 100644 packages/frontend/src/scripts/physics.ts
 delete mode 100644 packages/frontend/src/scripts/player-url-transform.ts
 delete mode 100644 packages/frontend/src/scripts/please-login.ts
 delete mode 100644 packages/frontend/src/scripts/popout.ts
 delete mode 100644 packages/frontend/src/scripts/popup-position.ts
 delete mode 100644 packages/frontend/src/scripts/post-message.ts
 delete mode 100644 packages/frontend/src/scripts/reaction-picker.ts
 delete mode 100644 packages/frontend/src/scripts/reload-ask.ts
 delete mode 100644 packages/frontend/src/scripts/search-emoji.ts
 delete mode 100644 packages/frontend/src/scripts/select-file.ts
 delete mode 100644 packages/frontend/src/scripts/show-moved-dialog.ts
 delete mode 100644 packages/frontend/src/scripts/show-suspended-dialog.ts
 delete mode 100644 packages/frontend/src/scripts/shuffle.ts
 delete mode 100644 packages/frontend/src/scripts/snowfall-effect.ts
 delete mode 100644 packages/frontend/src/scripts/sound.ts
 delete mode 100644 packages/frontend/src/scripts/sticky-sidebar.ts
 delete mode 100644 packages/frontend/src/scripts/stream-mock.ts
 delete mode 100644 packages/frontend/src/scripts/test-utils.ts
 delete mode 100644 packages/frontend/src/scripts/theme-editor.ts
 delete mode 100644 packages/frontend/src/scripts/theme.ts
 delete mode 100644 packages/frontend/src/scripts/time.ts
 delete mode 100644 packages/frontend/src/scripts/timezones.ts
 delete mode 100644 packages/frontend/src/scripts/touch.ts
 delete mode 100644 packages/frontend/src/scripts/unison-reload.ts
 delete mode 100644 packages/frontend/src/scripts/upload.ts
 delete mode 100644 packages/frontend/src/scripts/upload/compress-config.ts
 delete mode 100644 packages/frontend/src/scripts/upload/isWebpSupported.ts
 delete mode 100644 packages/frontend/src/scripts/use-chart-tooltip.ts
 delete mode 100644 packages/frontend/src/scripts/use-form.ts
 delete mode 100644 packages/frontend/src/scripts/use-leave-guard.ts
 delete mode 100644 packages/frontend/src/scripts/use-note-capture.ts
 delete mode 100644 packages/frontend/src/scripts/use-tooltip.ts
 create mode 100644 packages/frontend/src/utility/achievements.ts
 create mode 100644 packages/frontend/src/utility/admin-lookup.ts
 create mode 100644 packages/frontend/src/utility/array.ts
 create mode 100644 packages/frontend/src/utility/autocomplete.ts
 create mode 100644 packages/frontend/src/utility/autogen/settings-search-index.ts
 create mode 100644 packages/frontend/src/utility/cache.ts
 create mode 100644 packages/frontend/src/utility/chart-legend.ts
 create mode 100644 packages/frontend/src/utility/chart-vline.ts
 create mode 100644 packages/frontend/src/utility/check-permissions.ts
 create mode 100644 packages/frontend/src/utility/check-reaction-permissions.ts
 create mode 100644 packages/frontend/src/utility/check-word-mute.ts
 create mode 100644 packages/frontend/src/utility/clear-cache.ts
 create mode 100644 packages/frontend/src/utility/clicker-game.ts
 create mode 100644 packages/frontend/src/utility/clone.ts
 create mode 100644 packages/frontend/src/utility/code-highlighter.ts
 create mode 100644 packages/frontend/src/utility/collect-page-vars.ts
 create mode 100644 packages/frontend/src/utility/color.ts
 create mode 100644 packages/frontend/src/utility/confetti.ts
 create mode 100644 packages/frontend/src/utility/contains.ts
 create mode 100644 packages/frontend/src/utility/copy-to-clipboard.ts
 create mode 100644 packages/frontend/src/utility/device-kind.ts
 create mode 100644 packages/frontend/src/utility/emoji-picker.ts
 create mode 100644 packages/frontend/src/utility/extract-mentions.ts
 create mode 100644 packages/frontend/src/utility/extract-url-from-mfm.ts
 create mode 100644 packages/frontend/src/utility/file-drop.ts
 create mode 100644 packages/frontend/src/utility/focus-trap.ts
 create mode 100644 packages/frontend/src/utility/focus.ts
 create mode 100644 packages/frontend/src/utility/form.ts
 create mode 100644 packages/frontend/src/utility/format-time-string.ts
 create mode 100644 packages/frontend/src/utility/fullscreen.ts
 create mode 100644 packages/frontend/src/utility/get-account-from-id.ts
 create mode 100644 packages/frontend/src/utility/get-appear-note.ts
 create mode 100644 packages/frontend/src/utility/get-bg-color.ts
 create mode 100644 packages/frontend/src/utility/get-dom-node-or-null.ts
 create mode 100644 packages/frontend/src/utility/get-drive-file-menu.ts
 create mode 100644 packages/frontend/src/utility/get-embed-code.ts
 create mode 100644 packages/frontend/src/utility/get-note-menu.ts
 create mode 100644 packages/frontend/src/utility/get-note-summary.ts
 create mode 100644 packages/frontend/src/utility/get-user-menu.ts
 create mode 100644 packages/frontend/src/utility/get-user-name.ts
 create mode 100644 packages/frontend/src/utility/hotkey.ts
 create mode 100644 packages/frontend/src/utility/idb-proxy.ts
 create mode 100644 packages/frontend/src/utility/idle-render.ts
 create mode 100644 packages/frontend/src/utility/init-chart.ts
 create mode 100644 packages/frontend/src/utility/initialize-sw.ts
 create mode 100644 packages/frontend/src/utility/intl-const.ts
 create mode 100644 packages/frontend/src/utility/intl-string.ts
 create mode 100644 packages/frontend/src/utility/is-device-darkmode.ts
 create mode 100644 packages/frontend/src/utility/isFfVisibleForMe.ts
 create mode 100644 packages/frontend/src/utility/key-event.ts
 create mode 100644 packages/frontend/src/utility/langmap.ts
 create mode 100644 packages/frontend/src/utility/login-id.ts
 create mode 100644 packages/frontend/src/utility/lookup.ts
 create mode 100644 packages/frontend/src/utility/media-has-audio.ts
 create mode 100644 packages/frontend/src/utility/media-proxy.ts
 create mode 100644 packages/frontend/src/utility/merge.ts
 create mode 100644 packages/frontend/src/utility/mfm-function-picker.ts
 create mode 100644 packages/frontend/src/utility/misskey-api.ts
 create mode 100644 packages/frontend/src/utility/navigator.ts
 create mode 100644 packages/frontend/src/utility/page-metadata.ts
 create mode 100644 packages/frontend/src/utility/physics.ts
 create mode 100644 packages/frontend/src/utility/player-url-transform.ts
 create mode 100644 packages/frontend/src/utility/please-login.ts
 create mode 100644 packages/frontend/src/utility/popout.ts
 create mode 100644 packages/frontend/src/utility/popup-position.ts
 create mode 100644 packages/frontend/src/utility/post-message.ts
 create mode 100644 packages/frontend/src/utility/reaction-picker.ts
 create mode 100644 packages/frontend/src/utility/reload-ask.ts
 create mode 100644 packages/frontend/src/utility/search-emoji.ts
 create mode 100644 packages/frontend/src/utility/select-file.ts
 create mode 100644 packages/frontend/src/utility/show-moved-dialog.ts
 create mode 100644 packages/frontend/src/utility/show-suspended-dialog.ts
 create mode 100644 packages/frontend/src/utility/shuffle.ts
 create mode 100644 packages/frontend/src/utility/snowfall-effect.ts
 create mode 100644 packages/frontend/src/utility/sound.ts
 create mode 100644 packages/frontend/src/utility/sticky-sidebar.ts
 create mode 100644 packages/frontend/src/utility/stream-mock.ts
 create mode 100644 packages/frontend/src/utility/test-utils.ts
 create mode 100644 packages/frontend/src/utility/theme-editor.ts
 create mode 100644 packages/frontend/src/utility/theme.ts
 create mode 100644 packages/frontend/src/utility/time.ts
 create mode 100644 packages/frontend/src/utility/timezones.ts
 create mode 100644 packages/frontend/src/utility/touch.ts
 create mode 100644 packages/frontend/src/utility/unison-reload.ts
 create mode 100644 packages/frontend/src/utility/upload.ts
 create mode 100644 packages/frontend/src/utility/upload/compress-config.ts
 create mode 100644 packages/frontend/src/utility/upload/isWebpSupported.ts
 create mode 100644 packages/frontend/src/utility/use-chart-tooltip.ts
 create mode 100644 packages/frontend/src/utility/use-form.ts
 create mode 100644 packages/frontend/src/utility/use-leave-guard.ts
 create mode 100644 packages/frontend/src/utility/use-note-capture.ts
 create mode 100644 packages/frontend/src/utility/use-tooltip.ts

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index 00639be642..938e4a623f 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -21,7 +21,7 @@ let moduleInitialized = false;
 let unobserve = () => {};
 let misskeyOS = null;
 
-function loadTheme(applyTheme: typeof import('../src/scripts/theme')['applyTheme']) {
+function loadTheme(applyTheme: typeof import('../src/utility/theme')['applyTheme']) {
 	unobserve();
 	const theme = themes[document.documentElement.dataset.misskeyTheme];
 	if (theme) {
@@ -67,7 +67,7 @@ queueMicrotask(() => {
 		import('../src/components'),
 		import('../src/directives'),
 		import('../src/widgets'),
-		import('../src/scripts/theme'),
+		import('../src/utility/theme'),
 		import('../src/preferences'),
 		import('../src/os'),
 	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { prefer }, os]) => {
diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts
index 70afc356c1..473e386be9 100644
--- a/packages/frontend/@types/theme.d.ts
+++ b/packages/frontend/@types/theme.d.ts
@@ -4,7 +4,7 @@
  */
 
 declare module '@@/themes/*.json5' {
-	import { Theme } from '@/scripts/theme.js';
+	import { Theme } from '@/utility/theme.js';
 
 	const theme: Theme;
 
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index 17d690cd3a..5e2a1f45ac 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -8,13 +8,13 @@ import * as Misskey from 'misskey-js';
 import { apiUrl } from '@@/js/config.js';
 import type { MenuItem, MenuButton } from '@/types/menu.js';
 import { defaultMemoryStorage } from '@/memory-storage';
-import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
+import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { del, get, set } from '@/scripts/idb-proxy.js';
+import { del, get, set } from '@/utility/idb-proxy.js';
 import { waiting, popup, popupMenu, success, alert } from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
 
 // TODO: 他のタブと永続化されたstateを同期
 
diff --git a/packages/frontend/src/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts
new file mode 100644
index 0000000000..3acc1127c9
--- /dev/null
+++ b/packages/frontend/src/aiscript/api.ts
@@ -0,0 +1,125 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { errors, utils, values } from '@syuilo/aiscript';
+import * as Misskey from 'misskey-js';
+import { url, lang } from '@@/js/config.js';
+import { assertStringAndIsIn } from './common.js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { $i } from '@/account.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { customEmojis } from '@/custom-emojis.js';
+
+const DIALOG_TYPES = [
+	'error',
+	'info',
+	'success',
+	'warning',
+	'waiting',
+	'question',
+] as const;
+
+export function aiScriptReadline(q: string): Promise<string> {
+	return new Promise(ok => {
+		os.inputText({
+			title: q,
+		}).then(({ result: a }) => {
+			ok(a ?? '');
+		});
+	});
+}
+
+export function createAiScriptEnv(opts: { storageKey: string, token?: string }) {
+	return {
+		USER_ID: $i ? values.STR($i.id) : values.NULL,
+		USER_NAME: $i?.name ? values.STR($i.name) : values.NULL,
+		USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
+		CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
+		LOCALE: values.STR(lang),
+		SERVER_URL: values.STR(url),
+		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
+			utils.assertString(title);
+			utils.assertString(text);
+			if (type != null) {
+				assertStringAndIsIn(type, DIALOG_TYPES);
+			}
+			await os.alert({
+				type: type ? type.value : 'info',
+				title: title.value,
+				text: text.value,
+			});
+			return values.NULL;
+		}),
+		'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
+			utils.assertString(title);
+			utils.assertString(text);
+			if (type != null) {
+				assertStringAndIsIn(type, DIALOG_TYPES);
+			}
+			const confirm = await os.confirm({
+				type: type ? type.value : 'question',
+				title: title.value,
+				text: text.value,
+			});
+			return confirm.canceled ? values.FALSE : values.TRUE;
+		}),
+		'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
+			utils.assertString(ep);
+			if (ep.value.includes('://')) {
+				throw new errors.AiScriptRuntimeError('invalid endpoint');
+			}
+			if (token) {
+				utils.assertString(token);
+				// バグがあればundefinedもあり得るため念のため
+				if (typeof token.value !== 'string') throw new Error('invalid token');
+			}
+			const actualToken: string | null = token?.value ?? opts.token ?? null;
+			if (param == null) {
+				throw new errors.AiScriptRuntimeError('expected param');
+			}
+			utils.assertObject(param);
+			return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => {
+				return utils.jsToVal(res);
+			}, err => {
+				return values.ERROR('request_failed', utils.jsToVal(err));
+			});
+		}),
+		/* セキュリティ上の問題があるため無効化
+		'Mk:apiExternal': values.FN_NATIVE(async ([host, ep, param, token]) => {
+			utils.assertString(host);
+			utils.assertString(ep);
+			if (token) utils.assertString(token);
+			return os.apiExternal(host.value, ep.value, utils.valToJs(param), token?.value).then(res => {
+				return utils.jsToVal(res);
+			}, err => {
+				return values.ERROR('request_failed', utils.jsToVal(err));
+			});
+		}),
+		*/
+		'Mk:save': values.FN_NATIVE(([key, value]) => {
+			utils.assertString(key);
+			utils.expectAny(value);
+			miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));
+			return values.NULL;
+		}),
+		'Mk:load': values.FN_NATIVE(([key]) => {
+			utils.assertString(key);
+			return utils.jsToVal(miLocalStorage.getItemAsJson(`aiscript:${opts.storageKey}:${key.value}`) ?? null);
+		}),
+		'Mk:remove': values.FN_NATIVE(([key]) => {
+			utils.assertString(key);
+			miLocalStorage.removeItem(`aiscript:${opts.storageKey}:${key.value}`);
+			return values.NULL;
+		}),
+		'Mk:url': values.FN_NATIVE(() => {
+			return values.STR(window.location.href);
+		}),
+		'Mk:nyaize': values.FN_NATIVE(([text]) => {
+			utils.assertString(text);
+			return values.STR(Misskey.nyaize(text.value));
+		}),
+	};
+}
diff --git a/packages/frontend/src/aiscript/common.ts b/packages/frontend/src/aiscript/common.ts
new file mode 100644
index 0000000000..ba5dfb8368
--- /dev/null
+++ b/packages/frontend/src/aiscript/common.ts
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { errors, utils } from '@syuilo/aiscript';
+import type { values } from '@syuilo/aiscript';
+
+export function assertStringAndIsIn<A extends readonly string[]>(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } {
+	utils.assertString(value);
+	const str = value.value;
+	if (!expects.includes(str)) {
+		const expected = expects.map((expect) => `"${expect}"`).join(', ');
+		throw new errors.AiScriptRuntimeError(`"${value.value}" is not in ${expected}`);
+	}
+}
diff --git a/packages/frontend/src/aiscript/ui.ts b/packages/frontend/src/aiscript/ui.ts
new file mode 100644
index 0000000000..46e193f7c1
--- /dev/null
+++ b/packages/frontend/src/aiscript/ui.ts
@@ -0,0 +1,655 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { utils, values } from '@syuilo/aiscript';
+import { v4 as uuid } from 'uuid';
+import { ref } from 'vue';
+import type { Ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { assertStringAndIsIn } from './common.js';
+
+const ALIGNS = ['left', 'center', 'right'] as const;
+const FONTS = ['serif', 'sans-serif', 'monospace'] as const;
+const BORDER_STYLES = ['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] as const;
+
+type Align = (typeof ALIGNS)[number];
+type Font = (typeof FONTS)[number];
+type BorderStyle = (typeof BORDER_STYLES)[number];
+
+export type AsUiComponentBase = {
+	id: string;
+	hidden?: boolean;
+};
+
+export type AsUiRoot = AsUiComponentBase & {
+	type: 'root';
+	children: AsUiComponent['id'][];
+};
+
+export type AsUiContainer = AsUiComponentBase & {
+	type: 'container';
+	children?: AsUiComponent['id'][];
+	align?: Align;
+	bgColor?: string;
+	fgColor?: string;
+	font?: Font;
+	borderWidth?: number;
+	borderColor?: string;
+	borderStyle?: BorderStyle;
+	borderRadius?: number;
+	padding?: number;
+	rounded?: boolean;
+	hidden?: boolean;
+};
+
+export type AsUiText = AsUiComponentBase & {
+	type: 'text';
+	text?: string;
+	size?: number;
+	bold?: boolean;
+	color?: string;
+	font?: Font;
+};
+
+export type AsUiMfm = AsUiComponentBase & {
+	type: 'mfm';
+	text?: string;
+	size?: number;
+	bold?: boolean;
+	color?: string;
+	font?: Font;
+	onClickEv?: (evId: string) => Promise<void>;
+};
+
+export type AsUiButton = AsUiComponentBase & {
+	type: 'button';
+	text?: string;
+	onClick?: () => Promise<void>;
+	primary?: boolean;
+	rounded?: boolean;
+	disabled?: boolean;
+};
+
+export type AsUiButtons = AsUiComponentBase & {
+	type: 'buttons';
+	buttons?: AsUiButton[];
+};
+
+export type AsUiSwitch = AsUiComponentBase & {
+	type: 'switch';
+	onChange?: (v: boolean) => Promise<void>;
+	default?: boolean;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiTextarea = AsUiComponentBase & {
+	type: 'textarea';
+	onInput?: (v: string) => Promise<void>;
+	default?: string;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiTextInput = AsUiComponentBase & {
+	type: 'textInput';
+	onInput?: (v: string) => Promise<void>;
+	default?: string;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiNumberInput = AsUiComponentBase & {
+	type: 'numberInput';
+	onInput?: (v: number) => Promise<void>;
+	default?: number;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiSelect = AsUiComponentBase & {
+	type: 'select';
+	items?: {
+		text: string;
+		value: string;
+	}[];
+	onChange?: (v: string) => Promise<void>;
+	default?: string;
+	label?: string;
+	caption?: string;
+};
+
+export type AsUiFolder = AsUiComponentBase & {
+	type: 'folder';
+	children?: AsUiComponent['id'][];
+	title?: string;
+	opened?: boolean;
+};
+
+type PostFormPropsForAsUi = {
+	text: string;
+	cw?: string;
+	visibility?: (typeof Misskey.noteVisibilities)[number];
+	localOnly?: boolean;
+};
+
+export type AsUiPostFormButton = AsUiComponentBase & {
+	type: 'postFormButton';
+	text?: string;
+	primary?: boolean;
+	rounded?: boolean;
+	form?: PostFormPropsForAsUi;
+};
+
+export type AsUiPostForm = AsUiComponentBase & {
+	type: 'postForm';
+	form?: PostFormPropsForAsUi;
+};
+
+export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm;
+
+type Options<T extends AsUiComponent> = T extends AsUiButtons
+	? Omit<T, 'id' | 'type' | 'buttons'> & { 'buttons'?: Options<AsUiButton>[] }
+	: Omit<T, 'id' | 'type'>;
+
+export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
+	// TODO
+}
+
+function getRootOptions(def: values.Value | undefined): Options<AsUiRoot> {
+	utils.assertObject(def);
+
+	const children = def.value.get('children');
+	utils.assertArray(children);
+
+	return {
+		children: children.value.map(v => {
+			utils.assertObject(v);
+			const id = v.value.get('id');
+			utils.assertString(id);
+			return id.value;
+		}),
+	};
+}
+
+function getContainerOptions(def: values.Value | undefined): Options<AsUiContainer> {
+	utils.assertObject(def);
+
+	const children = def.value.get('children');
+	if (children) utils.assertArray(children);
+	const align = def.value.get('align');
+	if (align) assertStringAndIsIn(align, ALIGNS);
+	const bgColor = def.value.get('bgColor');
+	if (bgColor) utils.assertString(bgColor);
+	const fgColor = def.value.get('fgColor');
+	if (fgColor) utils.assertString(fgColor);
+	const font = def.value.get('font');
+	if (font) assertStringAndIsIn(font, FONTS);
+	const borderWidth = def.value.get('borderWidth');
+	if (borderWidth) utils.assertNumber(borderWidth);
+	const borderColor = def.value.get('borderColor');
+	if (borderColor) utils.assertString(borderColor);
+	const borderStyle = def.value.get('borderStyle');
+	if (borderStyle) assertStringAndIsIn(borderStyle, BORDER_STYLES);
+	const borderRadius = def.value.get('borderRadius');
+	if (borderRadius) utils.assertNumber(borderRadius);
+	const padding = def.value.get('padding');
+	if (padding) utils.assertNumber(padding);
+	const rounded = def.value.get('rounded');
+	if (rounded) utils.assertBoolean(rounded);
+	const hidden = def.value.get('hidden');
+	if (hidden) utils.assertBoolean(hidden);
+
+	return {
+		children: children ? children.value.map(v => {
+			utils.assertObject(v);
+			const id = v.value.get('id');
+			utils.assertString(id);
+			return id.value;
+		}) : [],
+		align: align?.value,
+		fgColor: fgColor?.value,
+		bgColor: bgColor?.value,
+		font: font?.value,
+		borderWidth: borderWidth?.value,
+		borderColor: borderColor?.value,
+		borderStyle: borderStyle?.value,
+		borderRadius: borderRadius?.value,
+		padding: padding?.value,
+		rounded: rounded?.value,
+		hidden: hidden?.value,
+	};
+}
+
+function getTextOptions(def: values.Value | undefined): Options<AsUiText> {
+	utils.assertObject(def);
+
+	const text = def.value.get('text');
+	if (text) utils.assertString(text);
+	const size = def.value.get('size');
+	if (size) utils.assertNumber(size);
+	const bold = def.value.get('bold');
+	if (bold) utils.assertBoolean(bold);
+	const color = def.value.get('color');
+	if (color) utils.assertString(color);
+	const font = def.value.get('font');
+	if (font) assertStringAndIsIn(font, FONTS);
+
+	return {
+		text: text?.value,
+		size: size?.value,
+		bold: bold?.value,
+		color: color?.value,
+		font: font?.value,
+	};
+}
+
+function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiMfm> {
+	utils.assertObject(def);
+
+	const text = def.value.get('text');
+	if (text) utils.assertString(text);
+	const size = def.value.get('size');
+	if (size) utils.assertNumber(size);
+	const bold = def.value.get('bold');
+	if (bold) utils.assertBoolean(bold);
+	const color = def.value.get('color');
+	if (color) utils.assertString(color);
+	const font = def.value.get('font');
+	if (font) assertStringAndIsIn(font, FONTS);
+	const onClickEv = def.value.get('onClickEv');
+	if (onClickEv) utils.assertFunction(onClickEv);
+
+	return {
+		text: text?.value,
+		size: size?.value,
+		bold: bold?.value,
+		color: color?.value,
+		font: font?.value,
+		onClickEv: async (evId: string) => {
+			if (onClickEv) await call(onClickEv, [values.STR(evId)]);
+		},
+	};
+}
+
+function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextInput> {
+	utils.assertObject(def);
+
+	const onInput = def.value.get('onInput');
+	if (onInput) utils.assertFunction(onInput);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertString(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		onInput: async (v) => {
+			if (onInput) await call(onInput, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextarea> {
+	utils.assertObject(def);
+
+	const onInput = def.value.get('onInput');
+	if (onInput) utils.assertFunction(onInput);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertString(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		onInput: async (v) => {
+			if (onInput) await call(onInput, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiNumberInput> {
+	utils.assertObject(def);
+
+	const onInput = def.value.get('onInput');
+	if (onInput) utils.assertFunction(onInput);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertNumber(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		onInput: async (v) => {
+			if (onInput) await call(onInput, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButton> {
+	utils.assertObject(def);
+
+	const text = def.value.get('text');
+	if (text) utils.assertString(text);
+	const onClick = def.value.get('onClick');
+	if (onClick) utils.assertFunction(onClick);
+	const primary = def.value.get('primary');
+	if (primary) utils.assertBoolean(primary);
+	const rounded = def.value.get('rounded');
+	if (rounded) utils.assertBoolean(rounded);
+	const disabled = def.value.get('disabled');
+	if (disabled) utils.assertBoolean(disabled);
+
+	return {
+		text: text?.value,
+		onClick: async () => {
+			if (onClick) await call(onClick, []);
+		},
+		primary: primary?.value,
+		rounded: rounded?.value,
+		disabled: disabled?.value,
+	};
+}
+
+function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButtons> {
+	utils.assertObject(def);
+
+	const buttons = def.value.get('buttons');
+	if (buttons) utils.assertArray(buttons);
+
+	return {
+		buttons: buttons ? buttons.value.map(button => {
+			utils.assertObject(button);
+			const text = button.value.get('text');
+			utils.assertString(text);
+			const onClick = button.value.get('onClick');
+			utils.assertFunction(onClick);
+			const primary = button.value.get('primary');
+			if (primary) utils.assertBoolean(primary);
+			const rounded = button.value.get('rounded');
+			if (rounded) utils.assertBoolean(rounded);
+			const disabled = button.value.get('disabled');
+			if (disabled) utils.assertBoolean(disabled);
+
+			return {
+				text: text.value,
+				onClick: async () => {
+					await call(onClick, []);
+				},
+				primary: primary?.value,
+				rounded: rounded?.value,
+				disabled: disabled?.value,
+			};
+		}) : [],
+	};
+}
+
+function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSwitch> {
+	utils.assertObject(def);
+
+	const onChange = def.value.get('onChange');
+	if (onChange) utils.assertFunction(onChange);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertBoolean(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		onChange: async (v) => {
+			if (onChange) await call(onChange, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSelect> {
+	utils.assertObject(def);
+
+	const items = def.value.get('items');
+	if (items) utils.assertArray(items);
+	const onChange = def.value.get('onChange');
+	if (onChange) utils.assertFunction(onChange);
+	const defaultValue = def.value.get('default');
+	if (defaultValue) utils.assertString(defaultValue);
+	const label = def.value.get('label');
+	if (label) utils.assertString(label);
+	const caption = def.value.get('caption');
+	if (caption) utils.assertString(caption);
+
+	return {
+		items: items ? items.value.map(item => {
+			utils.assertObject(item);
+			const text = item.value.get('text');
+			utils.assertString(text);
+			const value = item.value.get('value');
+			if (value) utils.assertString(value);
+			return {
+				text: text.value,
+				value: value ? value.value : text.value,
+			};
+		}) : [],
+		onChange: async (v) => {
+			if (onChange) await call(onChange, [utils.jsToVal(v)]);
+		},
+		default: defaultValue?.value,
+		label: label?.value,
+		caption: caption?.value,
+	};
+}
+
+function getFolderOptions(def: values.Value | undefined): Options<AsUiFolder> {
+	utils.assertObject(def);
+
+	const children = def.value.get('children');
+	if (children) utils.assertArray(children);
+	const title = def.value.get('title');
+	if (title) utils.assertString(title);
+	const opened = def.value.get('opened');
+	if (opened) utils.assertBoolean(opened);
+
+	return {
+		children: children ? children.value.map(v => {
+			utils.assertObject(v);
+			const id = v.value.get('id');
+			utils.assertString(id);
+			return id.value;
+		}) : [],
+		title: title?.value ?? '',
+		opened: opened?.value ?? true,
+	};
+}
+
+function getPostFormProps(form: values.VObj): PostFormPropsForAsUi {
+	const text = form.value.get('text');
+	utils.assertString(text);
+	const cw = form.value.get('cw');
+	if (cw) utils.assertString(cw);
+	const visibility = form.value.get('visibility');
+	if (visibility) utils.assertString(visibility);
+	const localOnly = form.value.get('localOnly');
+	if (localOnly) utils.assertBoolean(localOnly);
+
+	return {
+		text: text.value,
+		cw: cw?.value,
+		visibility: (visibility?.value && (Misskey.noteVisibilities as readonly string[]).includes(visibility.value)) ? visibility.value as typeof Misskey.noteVisibilities[number] : undefined,
+		localOnly: localOnly?.value,
+	};
+}
+
+function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostFormButton> {
+	utils.assertObject(def);
+
+	const text = def.value.get('text');
+	if (text) utils.assertString(text);
+	const primary = def.value.get('primary');
+	if (primary) utils.assertBoolean(primary);
+	const rounded = def.value.get('rounded');
+	if (rounded) utils.assertBoolean(rounded);
+	const form = def.value.get('form');
+	if (form) utils.assertObject(form);
+
+	return {
+		text: text?.value,
+		primary: primary?.value,
+		rounded: rounded?.value,
+		form: form ? getPostFormProps(form) : {
+			text: '',
+		},
+	};
+}
+
+function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostForm> {
+	utils.assertObject(def);
+
+	const form = def.value.get('form');
+	if (form) utils.assertObject(form);
+
+	return {
+		form: form ? getPostFormProps(form) : {
+			text: '',
+		},
+	};
+}
+
+export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
+	type OptionsConverter<T extends AsUiComponent, C> = (def: values.Value | undefined, call: C) => Options<T>;
+
+	const instances = {};
+
+	function createComponentInstance<T extends AsUiComponent, C>(
+		type: T['type'],
+		def: values.Value | undefined,
+		id: values.Value | undefined,
+		getOptions: OptionsConverter<T, C>,
+		call: C,
+	) {
+		if (id) utils.assertString(id);
+		const _id = id?.value ?? uuid();
+		const component = ref({
+			...getOptions(def, call),
+			type,
+			id: _id,
+		} as T);
+		components.push(component);
+		const instance = values.OBJ(new Map<string, values.Value>([
+			['id', values.STR(_id)],
+			['update', values.FN_NATIVE(([def], opts) => {
+				utils.assertObject(def);
+				const updates = getOptions(def, call);
+				for (const update of def.value.keys()) {
+					if (!Object.hasOwn(updates, update)) continue;
+					component.value[update] = updates[update];
+				}
+			})],
+		]));
+		instances[_id] = instance;
+		return instance;
+	}
+
+	const rootInstance = createComponentInstance('root', utils.jsToVal({ children: [] }), utils.jsToVal('___root___'), getRootOptions, () => {});
+	const rootComponent = components[0] as Ref<AsUiRoot>;
+	done(rootComponent);
+
+	return {
+		'Ui:root': rootInstance,
+
+		'Ui:patch': values.FN_NATIVE(([id, val], opts) => {
+			utils.assertString(id);
+			utils.assertArray(val);
+			// patch(id.value, val.value, opts.call); // TODO
+		}),
+
+		'Ui:get': values.FN_NATIVE(([id], opts) => {
+			utils.assertString(id);
+			const instance = instances[id.value];
+			if (instance) {
+				return instance;
+			} else {
+				return values.NULL;
+			}
+		}),
+
+		// Ui:root.update({ children: [...] }) の糖衣構文
+		'Ui:render': values.FN_NATIVE(([children], opts) => {
+			utils.assertArray(children);
+
+			rootComponent.value.children = children.value.map(v => {
+				utils.assertObject(v);
+				const id = v.value.get('id');
+				utils.assertString(id);
+				return id.value;
+			});
+		}),
+
+		'Ui:C:container': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('container', def, id, getContainerOptions, opts.topCall);
+		}),
+
+		'Ui:C:text': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('text', def, id, getTextOptions, opts.topCall);
+		}),
+
+		'Ui:C:mfm': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('mfm', def, id, getMfmOptions, opts.topCall);
+		}),
+
+		'Ui:C:textarea': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('textarea', def, id, getTextareaOptions, opts.topCall);
+		}),
+
+		'Ui:C:textInput': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('textInput', def, id, getTextInputOptions, opts.topCall);
+		}),
+
+		'Ui:C:numberInput': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.topCall);
+		}),
+
+		'Ui:C:button': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('button', def, id, getButtonOptions, opts.topCall);
+		}),
+
+		'Ui:C:buttons': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('buttons', def, id, getButtonsOptions, opts.topCall);
+		}),
+
+		'Ui:C:switch': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('switch', def, id, getSwitchOptions, opts.topCall);
+		}),
+
+		'Ui:C:select': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('select', def, id, getSelectOptions, opts.topCall);
+		}),
+
+		'Ui:C:folder': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('folder', def, id, getFolderOptions, opts.topCall);
+		}),
+
+		'Ui:C:postFormButton': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.topCall);
+		}),
+
+		'Ui:C:postForm': values.FN_NATIVE(([def, id], opts) => {
+			return createComponentInstance('postForm', def, id, getPostFormOptions, opts.topCall);
+		}),
+	};
+}
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 7b35d12a80..d66ff21519 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -12,16 +12,16 @@ import type { App } from 'vue';
 import widgets from '@/widgets/index.js';
 import directives from '@/directives/index.js';
 import components from '@/components/index.js';
-import { applyTheme } from '@/scripts/theme.js';
-import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
+import { applyTheme } from '@/utility/theme.js';
+import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
 import { updateI18n, i18n } from '@/i18n.js';
 import { $i, refreshAccount, login } from '@/account.js';
 import { store } from '@/store.js';
 import { fetchInstance, instance } from '@/instance.js';
-import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
-import { reloadChannel } from '@/scripts/unison-reload.js';
-import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
-import { getAccountFromId } from '@/scripts/get-account-from-id.js';
+import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
+import { reloadChannel } from '@/utility/unison-reload.js';
+import { getUrlWithoutLoginId } from '@/utility/login-id.js';
+import { getAccountFromId } from '@/utility/get-account-from-id.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { analytics, initAnalytics } from '@/analytics.js';
 import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 0ebe55ed1d..94a57acb96 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -8,24 +8,24 @@ import { ui } from '@@/js/config.js';
 import * as Misskey from 'misskey-js';
 import { common } from './common.js';
 import type { Component } from 'vue';
-import type { Keymap } from '@/scripts/hotkey.js';
+import type { Keymap } from '@/utility/hotkey.js';
 import { i18n } from '@/i18n.js';
 import { alert, confirm, popup, post, toast } from '@/os.js';
 import { useStream } from '@/stream.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import { $i, signout, updateAccountPartial } from '@/account.js';
 import { instance } from '@/instance.js';
 import { ColdDeviceStorage, store } from '@/store.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
+import { reactionPicker } from '@/utility/reaction-picker.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
-import { initializeSw } from '@/scripts/initialize-sw.js';
-import { emojiPicker } from '@/scripts/emoji-picker.js';
+import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
+import { initializeSw } from '@/utility/initialize-sw.js';
+import { emojiPicker } from '@/utility/emoji-picker.js';
 import { mainRouter } from '@/router/main.js';
-import { makeHotkey } from '@/scripts/hotkey.js';
+import { makeHotkey } from '@/utility/hotkey.js';
 import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
 import { prefer } from '@/preferences.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { launchPlugin } from '@/plugin.js';
 
@@ -115,16 +115,16 @@ export async function mainBoot() {
 			if (prefer.s.hemisphere === 'S') {
 				// ▼南半球
 				if (month === 7 || month === 8) {
-					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 					new SnowfallEffect({}).render();
 				}
 			} else {
 				// ▼北半球
 				if (month === 12 || month === 1) {
-					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 					new SnowfallEffect({}).render();
 				} else if (month === 3 || month === 4) {
-					const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+					const SakuraEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 					new SakuraEffect({
 						sakura: true,
 					}).render();
diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts
index 35c84d5568..e24c324dfb 100644
--- a/packages/frontend/src/boot/sub-boot.ts
+++ b/packages/frontend/src/boot/sub-boot.ts
@@ -5,7 +5,7 @@
 
 import { createApp, defineAsyncComponent } from 'vue';
 import { common } from './common.js';
-import { emojiPicker } from '@/scripts/emoji-picker.js';
+import { emojiPicker } from '@/utility/emoji-picker.js';
 
 export async function subBoot() {
 	const { isClientUpdated } = await common(() => createApp(
diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts
index bfe8fbe0e4..70078b410d 100644
--- a/packages/frontend/src/cache.ts
+++ b/packages/frontend/src/cache.ts
@@ -4,8 +4,8 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { Cache } from '@/scripts/cache.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { Cache } from '@/utility/cache.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
 export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index e48b6ef781..8e37317779 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -90,7 +90,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import RouterView from '@/components/global/RouterView.vue';
 import { useRouterFactory } from '@/router/supplier';
 import MkTextarea from '@/components/MkTextarea.vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 
 const props = defineProps<{
 	report: Misskey.entities.AdminAbuseUserReportsResponse[number];
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index 0839955d9d..cb8032c019 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js';
 import MkMention from './MkMention.vue';
 import { i18n } from '@/i18n.js';
 import { host as localHost } from '@@/js/config.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const user = ref<Misskey.entities.UserLite>();
 
diff --git a/packages/frontend/src/components/MkAchievements.stories.impl.ts b/packages/frontend/src/components/MkAchievements.stories.impl.ts
index bbd3f69d7c..d838997616 100644
--- a/packages/frontend/src/components/MkAchievements.stories.impl.ts
+++ b/packages/frontend/src/components/MkAchievements.stories.impl.ts
@@ -9,7 +9,7 @@ import { HttpResponse, http } from 'msw';
 import { userDetailed } from '../../.storybook/fakes.js';
 import { commonHandlers } from '../../.storybook/mocks.js';
 import MkAchievements from './MkAchievements.vue';
-import { ACHIEVEMENT_TYPES } from '@/scripts/achievements.js';
+import { ACHIEVEMENT_TYPES } from '@/utility/achievements.js';
 export const Empty = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index c8134416b5..70766634ce 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -55,9 +55,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { onMounted, ref, computed } from 'vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
+import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utility/achievements.js';
 
 const props = withDefaults(defineProps<{
 	user: Misskey.entities.User;
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index c8fa6246e0..b39bca5b27 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
 import tinycolor from 'tinycolor2';
 import { globalEvents } from '@/events.js';
-import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
+import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
 
 // https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
 const angleDiff = (a: number, b: number) => {
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 3045a47585..41fd2564d8 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
index e622d57f1e..ac71618ee2 100644
--- a/packages/frontend/src/components/MkAntennaEditor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -59,10 +59,10 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { deepMerge } from '@/scripts/merge.js';
-import type { DeepPartial } from '@/scripts/merge.js';
+import { deepMerge } from '@/utility/merge.js';
+import type { DeepPartial } from '@/utility/merge.js';
 
 type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
 	id?: string;
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 5c4d887e0c..20a953c72c 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -71,7 +71,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSelect from '@/components/MkSelect.vue';
-import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js';
+import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 
diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue
index f78d2d38f0..090c31044e 100644
--- a/packages/frontend/src/components/MkAuthConfirm.vue
+++ b/packages/frontend/src/components/MkAuthConfirm.vue
@@ -123,8 +123,8 @@ import MkButton from '@/components/MkButton.vue';
 import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = defineProps<{
 	name?: string;
diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
index af5dd4784d..64ccb708aa 100644
--- a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
+++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
@@ -12,7 +12,7 @@ import { userDetailed } from '../../.storybook/fakes.js';
 import { commonHandlers } from '../../.storybook/mocks.js';
 import MkAutocomplete from './MkAutocomplete.vue';
 import MkInput from './MkInput.vue';
-import { tick } from '@/scripts/test-utils.js';
+import { tick } from '@/utility/test-utils.js';
 const common = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 1a68353331..912e2d65a5 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -49,16 +49,16 @@ import sanitizeHtml from 'sanitize-html';
 import { emojilist, getEmojiName } from '@@/js/emojilist.js';
 import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
 import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
-import type { EmojiDef } from '@/scripts/search-emoji.js';
-import contains from '@/scripts/contains.js';
+import type { EmojiDef } from '@/utility/search-emoji.js';
+import contains from '@/utility/contains.js';
 import { acct } from '@/filters/user.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { customEmojis } from '@/custom-emojis.js';
-import { searchEmoji } from '@/scripts/search-emoji.js';
+import { searchEmoji } from '@/utility/search-emoji.js';
 import { prefer } from '@/preferences.js';
 
 const lib = emojilist.filter(x => x.category !== 'flags');
diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue
index 8236d0ddb9..1c44ed60d8 100644
--- a/packages/frontend/src/components/MkAvatars.vue
+++ b/packages/frontend/src/components/MkAvatars.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = withDefaults(defineProps<{
 	userIds: string[];
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index d4e4f6179a..2ef2d84198 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index c8685f8618..4707d02ce9 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -53,15 +53,15 @@ export type ChartSrc =
 import { onMounted, ref, shallowRef, watch } from 'vue';
 import { Chart } from 'chart.js';
 import * as Misskey from 'misskey-js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { alpha } from '@/utility/color.js';
 import date from '@/filters/date.js';
 import bytes from '@/filters/bytes.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
+import { initChart } from '@/utility/init-chart.js';
+import { chartLegend } from '@/utility/chart-legend.js';
 import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index 9a0a9fba05..775964af50 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -23,9 +23,9 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
 import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
 import * as os from '@/os.js';
 import { useInterval } from '@@/js/use-interval.js';
-import * as game from '@/scripts/clicker-game.js';
+import * as game from '@/utility/clicker-game.js';
 import number from '@/filters/number.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 
 const saveData = game.saveData;
 const cookies = computed(() => saveData.value?.cookies);
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index a9001b376a..2fc8c6591e 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref, watch } from 'vue';
 import { bundledLanguagesInfo } from 'shiki/langs';
 import type { BundledLanguage } from 'shiki/langs';
-import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
+import { getHighlighter, getTheme } from '@/utility/code-highlighter.js';
 import { store } from '@/store.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index 06e4d32878..9708b78a30 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -29,7 +29,7 @@ import { defineAsyncComponent, ref } from 'vue';
 import * as os from '@/os.js';
 import MkLoading from '@/components/global/MkLoading.vue';
 import { i18n } from '@/i18n.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index ed9e5127b0..46da0840fa 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
 import MkMenu from './MkMenu.vue';
 import type { MenuItem } from '@/types/menu.js';
-import contains from '@/scripts/contains.js';
+import contains from '@/utility/contains.js';
 import { prefer } from '@/preferences.js';
 import * as os from '@/os.js';
 
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 9f9c5b32e2..3c41d597de 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -40,7 +40,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import * as os from '@/os.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
 
 const emit = defineEmits<{
diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue
index b5f6e78b6c..cc8bbf1104 100644
--- a/packages/frontend/src/components/MkCwButton.vue
+++ b/packages/frontend/src/components/MkCwButton.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
-import { concat } from '@/scripts/array.js';
+import { concat } from '@/utility/array.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 
diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue
index 2e2321e6ac..8198356a76 100644
--- a/packages/frontend/src/components/MkDigitalClock.vue
+++ b/packages/frontend/src/components/MkDigitalClock.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref, watch } from 'vue';
-import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
+import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
 
 const props = withDefaults(defineProps<{
 	showS?: boolean;
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index e45c3bd9ce..733d50728e 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -45,8 +45,8 @@ import bytes from '@/filters/bytes.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index f91b248d0c..7b8d81f7f0 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -38,10 +38,10 @@ import { computed, defineAsyncComponent, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue
index 8df3c86ebf..7433aea061 100644
--- a/packages/frontend/src/components/MkDrive.navFolder.vue
+++ b/packages/frontend/src/components/MkDrive.navFolder.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 6863e65b05..270ef51916 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -104,11 +104,11 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue';
 import XFolder from '@/components/MkDrive.folder.vue';
 import XFile from '@/components/MkDrive.file.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { uploadFile, uploads } from '@/scripts/upload.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { uploadFile, uploads } from '@/utility/upload.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index 6e9eb75920..a53c9c7904 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -105,8 +105,8 @@ import MkInfo from '@/components/MkInfo.vue';
 
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
 
 const emit = defineEmits<{
 	(ev: 'ok'): void;
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 3c5b3133da..214b244802 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -131,13 +131,13 @@ import type {
 import XSection from '@/components/MkEmojiPicker.section.vue';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import * as os from '@/os.js';
-import { isTouchUsing } from '@/scripts/touch.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { isTouchUsing } from '@/utility/touch.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import { i18n } from '@/i18n.js';
 import { store } from '@/store.js';
 import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
 import { $i } from '@/account.js';
-import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
+import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index 08b8d60d09..554586b2f3 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, ref, shallowRef, watch } from 'vue';
 import { miLocalStorage } from '@/local-storage.js';
 import { prefer } from '@/preferences.js';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 const miLocalStoragePrefix = 'ui:folder:' as const;
 
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 1f27e9e221..1e2057d28e 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { nextTick, onMounted, ref, shallowRef } from 'vue';
 import { prefer } from '@/preferences.js';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 const props = withDefaults(defineProps<{
 	defaultOpen?: boolean;
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index bd45df843a..64828699d6 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -39,11 +39,11 @@ import { onBeforeUnmount, onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { host } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { pleaseLogin } from '@/utility/please-login.js';
 import { $i } from '@/account.js';
 import { store } from '@/store.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue
index ecb6cf882b..0a902f3400 100644
--- a/packages/frontend/src/components/MkFormDialog.file.vue
+++ b/packages/frontend/src/components/MkFormDialog.file.vue
@@ -15,8 +15,8 @@ import * as Misskey from 'misskey-js';
 import { computed, ref } from 'vue';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
-import { selectFile } from '@/scripts/select-file.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { selectFile } from '@/utility/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = defineProps<{
 	fileId?: string | null;
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index a639eae208..8fd16e13d4 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -80,7 +80,7 @@ import MkRange from './MkRange.vue';
 import MkButton from './MkButton.vue';
 import MkRadios from './MkRadios.vue';
 import XFile from './MkFormDialog.file.vue';
-import type { Form } from '@/scripts/form.js';
+import type { Form } from '@/utility/form.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index 058a2d7c78..4c4ae86db3 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue
index ca597b43cb..ad85660fc4 100644
--- a/packages/frontend/src/components/MkHorizontalSwipe.vue
+++ b/packages/frontend/src/components/MkHorizontalSwipe.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, shallowRef, computed, nextTick, watch } from 'vue';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
-import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
+import { isHorizontalSwipeSwiping as isSwiping } from '@/utility/touch.js';
 import { prefer } from '@/preferences.js';
 
 const rootEl = shallowRef<HTMLDivElement>();
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 739061bce1..aff9a67020 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -50,8 +50,8 @@ import { debounce } from 'throttle-debounce';
 import MkButton from '@/components/MkButton.vue';
 import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
-import { Autocomplete } from '@/scripts/autocomplete.js';
-import type { SuggestionType } from '@/scripts/autocomplete.js';
+import { Autocomplete } from '@/utility/autocomplete.js';
+import type { SuggestionType } from '@/utility/autocomplete.js';
 
 const props = defineProps<{
 	modelValue: string | number | null;
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index b0601cf7f9..7902151921 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkMiniChart from '@/components/MkMiniChart.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
 	instance: Misskey.entities.FederationInstance;
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index c2860ed89b..9d475bc8aa 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -88,10 +88,10 @@ import { onMounted, ref, computed, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkChart from '@/components/MkChart.vue';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
 import { $i } from '@/account.js';
 import * as os from '@/os.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import MkHeatmap from '@/components/MkHeatmap.vue';
@@ -99,7 +99,7 @@ import type { HeatmapSource } from '@/components/MkHeatmap.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
 import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
-import { initChart } from '@/scripts/init-chart.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index 1da3f14ad4..eba8a73aec 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -15,7 +15,7 @@ import { computed } from 'vue';
 import type { CSSProperties } from 'vue';
 import { instanceName as localInstanceName } from '@@/js/config.js';
 import { instance as localInstance } from '@/instance.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
 	host: string | null;
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index 1a71f6574f..08eddfc03c 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -64,7 +64,7 @@ import { computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkButton from '@/components/MkButton.vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 
diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue
index 50c9e16e5e..6a4b298523 100644
--- a/packages/frontend/src/components/MkKeyValue.vue
+++ b/packages/frontend/src/components/MkKeyValue.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { } from 'vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 9538979dd1..9a78eb67f2 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { shallowRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import { navbarItemDef } from '@/navbar.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index a276bf68b4..140fc9816c 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from 'vue';
 import { url as local } from '@@/js/config.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
 import * as os from '@/os.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 import type { MkABehavior } from '@/components/global/MkA.vue';
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 83ce212efd..33d4d269e7 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -91,8 +91,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
-import type { Keymap } from '@/scripts/hotkey.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard';
+import type { Keymap } from '@/utility/hotkey.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import bytes from '@/filters/bytes.js';
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 8206e3d8f6..6029b1e0b6 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -54,8 +54,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import bytes from '@/filters/bytes.js';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 1c9494f8d6..8a7a423869 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -38,7 +38,7 @@ import XBanner from '@/components/MkMediaBanner.vue';
 import XImage from '@/components/MkMediaImage.vue';
 import XVideo from '@/components/MkMediaVideo.vue';
 import * as os from '@/os.js';
-import { focusParent } from '@/scripts/focus.js';
+import { focusParent } from '@/utility/focus.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 1433b17dc0..74c1aefc3a 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -112,14 +112,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
-import type { Keymap } from '@/scripts/hotkey.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard';
+import type { Keymap } from '@/utility/hotkey.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard';
 import bytes from '@/filters/bytes.js';
 import { hms } from '@/filters/hms.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
-import hasAudio from '@/scripts/media-has-audio.js';
+import { exitFullscreen, requestFullscreen } from '@/utility/fullscreen.js';
+import hasAudio from '@/utility/media-has-audio.js';
 import MkMediaRange from '@/components/MkMediaRange.vue';
 import { $i, iAmModerator } from '@/account.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index ff3442abe0..419407955f 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -19,7 +19,7 @@ import { computed } from 'vue';
 import { host as localHost } from '@@/js/config.js';
 import type { MkABehavior } from '@/components/global/MkA.vue';
 import { $i } from '@/account.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index d484c1b338..61b3fa2fee 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -181,10 +181,10 @@ import MkSwitchButton from '@/components/MkSwitch.button.vue';
 import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { isTouchUsing } from '@/scripts/touch.js';
-import type { Keymap } from '@/scripts/hotkey.js';
-import { isFocusable } from '@/scripts/focus.js';
-import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { isTouchUsing } from '@/utility/touch.js';
+import type { Keymap } from '@/utility/hotkey.js';
+import { isFocusable } from '@/utility/focus.js';
+import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js';
 
 const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
 </script>
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 521c851d8b..8b3086d55e 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -43,12 +43,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue';
-import type { Keymap } from '@/scripts/hotkey.js';
+import type { Keymap } from '@/utility/hotkey.js';
 import * as os from '@/os.js';
-import { isTouchUsing } from '@/scripts/touch.js';
-import { deviceKind } from '@/scripts/device-kind.js';
-import { focusTrap } from '@/scripts/focus-trap.js';
-import { focusParent } from '@/scripts/focus.js';
+import { isTouchUsing } from '@/utility/touch.js';
+import { deviceKind } from '@/utility/device-kind.js';
+import { focusTrap } from '@/utility/focus-trap.js';
+import { focusParent } from '@/utility/focus.js';
 import { prefer } from '@/preferences.js';
 
 function getFixedContainer(el: Element | null): Element | null {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index c88b5c16ee..0daa917500 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -185,8 +185,8 @@ import { shouldCollapsed } from '@@/js/collapsed.js';
 import { host } from '@@/js/config.js';
 import type { Ref } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-import type { Keymap } from '@/scripts/hotkey.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
+import type { Keymap } from '@/utility/hotkey.js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -198,30 +198,30 @@ import MkPoll from '@/components/MkPoll.vue';
 import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import { checkWordMute } from '@/utility/check-word-mute.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
 import number from '@/filters/number.js';
 import * as os from '@/os.js';
-import * as sound from '@/scripts/sound.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import * as sound from '@/utility/sound.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { noteViewInterruptors } from '@/store.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
+import { reactionPicker } from '@/utility/reaction-picker.js';
+import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
-import { useNoteCapture } from '@/scripts/use-note-capture.js';
-import { deepClone } from '@/scripts/clone.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { getNoteSummary } from '@/scripts/get-note-summary.js';
+import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
+import { useNoteCapture } from '@/utility/use-note-capture.js';
+import { deepClone } from '@/utility/clone.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { getNoteSummary } from '@/utility/get-note-summary.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
+import { showMovedDialog } from '@/utility/show-moved-dialog.js';
 import { isEnabledUrlPreview } from '@/instance.js';
-import { focusPrev, focusNext } from '@/scripts/focus.js';
-import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { focusPrev, focusNext } from '@/utility/focus.js';
+import { getAppearNote } from '@/utility/get-appear-note.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 75c08087f3..10f375fcae 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -215,9 +215,9 @@ import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { isLink } from '@@/js/is-link.js';
 import { host } from '@@/js/config.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
 import type { Paging } from '@/components/MkPagination.vue';
-import type { Keymap } from '@/scripts/hotkey.js';
+import type { Keymap } from '@/utility/hotkey.js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -228,32 +228,32 @@ import MkPoll from '@/components/MkPoll.vue';
 import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import { checkWordMute } from '@/utility/check-word-mute.js';
 import { userPage } from '@/filters/user.js';
 import { notePage } from '@/filters/note.js';
 import number from '@/filters/number.js';
 import * as os from '@/os.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import * as sound from '@/scripts/sound.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
+import * as sound from '@/utility/sound.js';
 import { noteViewInterruptors } from '@/store.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
+import { reactionPicker } from '@/utility/reaction-picker.js';
+import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
-import { useNoteCapture } from '@/scripts/use-note-capture.js';
-import { deepClone } from '@/scripts/clone.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
+import { useNoteCapture } from '@/utility/use-note-capture.js';
+import { deepClone } from '@/utility/clone.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
+import { showMovedDialog } from '@/utility/show-moved-dialog.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
 import { isEnabledUrlPreview } from '@/instance.js';
-import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { getAppearNote } from '@/utility/get-appear-note.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index e4bade309b..bb7347cd26 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -46,11 +46,11 @@ import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
 import MkCwButton from '@/components/MkCwButton.vue';
 import { notePage } from '@/filters/note.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { userPage } from '@/filters/user.js';
-import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { checkWordMute } from '@/utility/check-word-mute.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 80cb9a45bb..5d096cf92d 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -164,11 +164,11 @@ import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getNoteSummary } from '@/scripts/get-note-summary.js';
+import { getNoteSummary } from '@/utility/get-note-summary.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { signinRequired } from '@/account.js';
 import { infoImageUrl } from '@/instance.js';
 
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index c3fc1961eb..780b434456 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -32,16 +32,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
 import { url } from '@@/js/config.js';
 import { getScrollContainer } from '@@/js/scroll.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 import RouterView from '@/components/global/RouterView.vue';
 import MkWindow from '@/components/MkWindow.vue';
-import { popout as _popout } from '@/scripts/popout.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { popout as _popout } from '@/utility/popout.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { i18n } from '@/i18n.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
 import { openingWindowsCount } from '@/os.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { useRouterFactory } from '@/router/supplier.js';
 import { mainRouter } from '@/router/main.js';
 import { analytics } from '@/analytics.js';
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index d4b0180ef9..1253d7883f 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -49,7 +49,7 @@ import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
 import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
 import type { ComputedRef } from 'vue';
 import type { MisskeyEntity } from '@/types/date-separated-list.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 1b2b3e48ba..2d3ec45bca 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -31,11 +31,11 @@ import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { host } from '@@/js/config.js';
 import { useInterval } from '@@/js/use-interval.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-import { sum } from '@/scripts/array.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
+import { sum } from '@/utility/array.js';
+import { pleaseLogin } from '@/utility/please-login.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue
index 3726ddf822..22fe189a63 100644
--- a/packages/frontend/src/components/MkPollEditor.vue
+++ b/packages/frontend/src/components/MkPollEditor.vue
@@ -58,8 +58,8 @@ import MkInput from './MkInput.vue';
 import MkSelect from './MkSelect.vue';
 import MkSwitch from './MkSwitch.vue';
 import MkButton from './MkButton.vue';
-import { formatDateTimeString } from '@/scripts/format-time-string.js';
-import { addTime } from '@/scripts/time.js';
+import { formatDateTimeString } from '@/utility/format-time-string.js';
+import { addTime } from '@/utility/time.js';
 import { i18n } from '@/i18n.js';
 
 export type PollEditorModelValue = {
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 6aa460b506..306c4be61b 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -116,25 +116,25 @@ import MkNotePreview from '@/components/MkNotePreview.vue';
 import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
 import MkPollEditor from '@/components/MkPollEditor.vue';
 import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
-import { erase, unique } from '@/scripts/array.js';
-import { extractMentions } from '@/scripts/extract-mentions.js';
-import { formatTimeString } from '@/scripts/format-time-string.js';
-import { Autocomplete } from '@/scripts/autocomplete.js';
+import { erase, unique } from '@/utility/array.js';
+import { extractMentions } from '@/utility/extract-mentions.js';
+import { formatTimeString } from '@/utility/format-time-string.js';
+import { Autocomplete } from '@/utility/autocomplete.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { selectFiles } from '@/scripts/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { selectFiles } from '@/utility/select-file.js';
 import { store, notePostInterruptors, postFormActions } from '@/store.js';
 import MkInfo from '@/components/MkInfo.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js';
-import { uploadFile } from '@/scripts/upload.js';
-import { deepClone } from '@/scripts/clone.js';
+import { uploadFile } from '@/utility/upload.js';
+import { deepClone } from '@/utility/clone.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { miLocalStorage } from '@/local-storage.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { emojiPicker } from '@/scripts/emoji-picker.js';
-import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { emojiPicker } from '@/utility/emoji-picker.js';
+import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js';
 import { prefer } from '@/preferences.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 83d0d66cc3..3ce0c4957b 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -36,10 +36,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard';
+import { copyToClipboard } from '@/utility/copy-to-clipboard';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 4fb4c6fe56..af4e2a4fe0 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
 import { i18n } from '@/i18n.js';
 import { getScrollContainer } from '@@/js/scroll.js';
-import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
+import { isHorizontalSwipeSwiping } from '@/utility/touch.js';
 
 const SCROLL_STOP = 10;
 const MAX_PULL_DISTANCE = Infinity;
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index 5e42df4795..780f8bc6d0 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -46,7 +46,7 @@ import { $i, getAccounts } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
 import { instance } from '@/instance.js';
 import { apiWithDialog, promiseDialog } from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 defineProps<{
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 264b559222..8e5be38e03 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
-import { isTouchUsing } from '@/scripts/touch.js';
+import { isTouchUsing } from '@/utility/touch.js';
 import * as os from '@/os.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue
index c0cbd8a65d..cb25580bc2 100644
--- a/packages/frontend/src/components/MkReactionIcon.vue
+++ b/packages/frontend/src/components/MkReactionIcon.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, shallowRef } from 'vue';
-import { useTooltip } from '@/scripts/use-tooltip.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
 import * as os from '@/os.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 2e453aeb8f..f35c938d02 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -25,14 +25,14 @@ import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
 import XDetails from '@/components/MkReactionsViewer.details.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import * as os from '@/os.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
 import { $i } from '@/account.js';
 import MkReactionEffect from '@/components/MkReactionEffect.vue';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { i18n } from '@/i18n.js';
-import * as sound from '@/scripts/sound.js';
-import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
+import * as sound from '@/utility/sound.js';
+import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index 3ca680d8f2..1dc1da0bc3 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, nextTick, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index 13cee1b1cd..dd4a8a1a6d 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -12,11 +12,11 @@ import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import tinycolor from 'tinycolor2';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 initChart();
 
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue
index 32f35ed5ad..229fd9d1ba 100644
--- a/packages/frontend/src/components/MkRoleSelectDialog.vue
+++ b/packages/frontend/src/components/MkRoleSelectDialog.vue
@@ -49,7 +49,7 @@ import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkRolePreview from '@/components/MkRolePreview.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import * as os from '@/os.js';
 import MkSpacer from '@/components/global/MkSpacer.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue
index e98ac9cfd2..aacd1eae2a 100644
--- a/packages/frontend/src/components/MkSignin.input.vue
+++ b/packages/frontend/src/components/MkSignin.input.vue
@@ -58,7 +58,7 @@ import { toUnicode } from 'punycode.js';
 
 import { query, extractDomain } from '@@/js/url.js';
 import { host as configHost } from '@@/js/config.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 2fb97e8e46..2dcc87f425 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -69,9 +69,9 @@ import * as Misskey from 'misskey-js';
 import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
 
 import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
 import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 676a336ec7..a970d1ce00 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
 import { shallowRef } from 'vue';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
 import MkSignin from '@/components/MkSignin.vue';
 import MkModal from '@/components/MkModal.vue';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 4227e1317a..151cd60fb2 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -88,7 +88,7 @@ import MkInput from './MkInput.vue';
 import MkCaptcha from '@/components/MkCaptcha.vue';
 import type { Captcha } from '@/components/MkCaptcha.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { login } from '@/account.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index ce0fa86219..fddf3934bb 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -94,12 +94,12 @@ export type SuperMenuDef = {
 
 <script lang="ts" setup>
 import { useTemplateRef, ref, watch, nextTick } from 'vue';
-import type { SearchIndexItem } from '@/scripts/autogen/settings-search-index.js';
+import type { SearchIndexItem } from '@/utility/autogen/settings-search-index.js';
 import MkInput from '@/components/MkInput.vue';
 import { i18n } from '@/i18n.js';
 import { getScrollContainer } from '@@/js/scroll.js';
 import { useRouter } from '@/router/supplier.js';
-import { initIntlString, compareStringIncludes } from '@/scripts/intl-string.js';
+import { initIntlString, compareStringIncludes } from '@/utility/intl-string.js';
 
 const props = defineProps<{
 	def: SuperMenuDef[];
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 7e92726dcb..2e1a96c326 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -103,7 +103,7 @@ import type {
 } from '@/components/MkSystemWebhookEditor.impl.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 3e8588018c..eb47a8b858 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -40,8 +40,8 @@ import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallow
 import { debounce } from 'throttle-debounce';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { Autocomplete } from '@/scripts/autocomplete.js';
-import type { SuggestionType } from '@/scripts/autocomplete.js';
+import { Autocomplete } from '@/utility/autocomplete.js';
+import type { SuggestionType } from '@/utility/autocomplete.js';
 
 const props = defineProps<{
 	modelValue: string | null;
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 3cc9b69341..62ae9a048a 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -24,7 +24,7 @@ import type { Paging } from '@/components/MkPagination.vue';
 import MkNotes from '@/components/MkNotes.vue';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import { useStream } from '@/stream.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import { $i } from '@/account.js';
 import { instance } from '@/instance.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index c30ca1ffe6..8031abd563 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue';
 import * as os from '@/os.js';
-import { calcPopupPosition } from '@/scripts/popup-position.js';
+import { calcPopupPosition } from '@/utility/popup-position.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 11d7c8dc4d..5908db33c4 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -159,7 +159,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { host } from '@@/js/config.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import * as os from '@/os.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index c937b4ce59..95732d5796 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -21,7 +21,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
 import { version } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { confetti } from '@/scripts/confetti.js';
+import { confetti } from '@/utility/confetti.js';
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 850dd2d257..f8ac638e40 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -89,9 +89,9 @@ import { versatileLang } from '@@/js/intl-const.js';
 import type { summaly } from '@misskey-dev/summaly';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import MkButton from '@/components/MkButton.vue';
-import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
+import { transformPlayerUrl } from '@/utility/player-url-transform.js';
 import { store } from '@/store.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 34991fa0dd..34e86444ad 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -56,7 +56,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index 7a2e878931..dde2efd8ee 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { onMounted, ref } from 'vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { acct } from '@/filters/user.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index 9abf61e893..eb189b446b 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -40,8 +40,8 @@ import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
 
 defineProps<{
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 4027893a8e..8a89b253e3 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -60,14 +60,14 @@ import * as Misskey from 'misskey-js';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import { userPage } from '@/filters/user.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { getUserMenu } from '@/scripts/get-user-menu.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { getUserMenu } from '@/utility/get-user-menu.js';
 import number from '@/filters/number.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
 import { $i } from '@/account.js';
-import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
 	showing: boolean;
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 2c5757e24e..1dbc143f8f 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -67,7 +67,7 @@ import { host as currentHost, hostname } from '@@/js/config.js';
 import MkInput from '@/components/MkInput.vue';
 import FormSplit from '@/components/form/split.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
index 62e5d1da8a..629bc30667 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -49,7 +49,7 @@ import { i18n } from '@/i18n.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const isLocked = ref(false);
 const hideOnlineStatus = ref(false);
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 7cb48f6afb..14acfd3f89 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -37,7 +37,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import FormSlot from '@/components/form/slot.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { chooseFileFromPc } from '@/scripts/select-file.js';
+import { chooseFileFromPc } from '@/utility/select-file.js';
 import * as os from '@/os.js';
 import { signinRequired } from '@/account.js';
 
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index 004edab630..4accc6183b 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js';
 import { ref } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = defineProps<{
 	user: Misskey.entities.UserDetailed;
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index ac48b11c3f..3aa76be0bc 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -17,11 +17,11 @@ import { onMounted, shallowRef, ref, nextTick } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import tinycolor from 'tinycolor2';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 898ab75fc0..bad0375136 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -62,7 +62,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { instanceName } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import MkNumber from '@/components/MkNumber.vue';
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 4759d217e8..4b3c728ee4 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import contains from '@/scripts/contains.js';
+import contains from '@/utility/contains.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 1c069d65fc..ab62a5113d 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { versatileLang } from '@@/js/intl-const.js';
 import MkWindow from '@/components/MkWindow.vue';
-import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
+import { transformPlayerUrl } from '@/utility/player-url-transform.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts
index 1ccf105dbb..deb2b8a52b 100644
--- a/packages/frontend/src/components/global/MkA.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkA.stories.impl.ts
@@ -7,7 +7,7 @@
 import { expect, userEvent, within } from '@storybook/test';
 import type { StoryObj } from '@storybook/vue3';
 import MkA from './MkA.vue';
-import { tick } from '@/scripts/test-utils.js';
+import { tick } from '@/utility/test-utils.js';
 export const Default = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 8eacf16d6d..173f6a849f 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -17,7 +17,7 @@ export type MkABehavior = 'window' | 'browser' | null;
 import { computed, inject, shallowRef } from 'vue';
 import { url } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 71c309fd89..dc129b4c30 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -45,7 +45,7 @@ import * as Misskey from 'misskey-js';
 import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
 import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
 import MkA from './MkA.vue';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import { acct, userPage } from '@/filters/user.js';
 import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 82c5e2a1dc..9981092dff 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -27,12 +27,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, inject, ref } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrl, getStaticImageUrl } from '@/utility/media-proxy.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 import * as os from '@/os.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import * as sound from '@/scripts/sound.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import * as sound from '@/utility/sound.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
 import { $i } from '@/account.js';
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index a00004fdb8..37df945233 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -14,8 +14,8 @@ import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js';
 import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js';
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import * as sound from '@/scripts/sound.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import * as sound from '@/utility/sound.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 1070c0c83b..a7cf70745e 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -47,10 +47,10 @@ import XTabs from './MkPageHeader.tabs.vue';
 import type { Tab } from './MkPageHeader.tabs.vue';
 import { scrollToTop } from '@@/js/scroll.js';
 import { globalEvents } from '@/events.js';
-import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
+import { injectReactiveMetadata } from '@/utility/page-metadata.js';
 import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import type { PageHeaderItem } from '@/types/page-header.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 
 const props = withDefaults(defineProps<{
 	overridePageMetadata?: PageMetadata;
diff --git a/packages/frontend/src/components/global/MkSpacer.vue b/packages/frontend/src/components/global/MkSpacer.vue
index db01c10eb0..6080bad9cd 100644
--- a/packages/frontend/src/components/global/MkSpacer.vue
+++ b/packages/frontend/src/components/global/MkSpacer.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { inject } from 'vue';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
 
 const props = withDefaults(defineProps<{
 	contentMax?: number | null;
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 3d2036e376..d28db0d0ca 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -29,7 +29,7 @@ import { defineAsyncComponent, ref } from 'vue';
 import { toUnicode as decodePunycode } from 'punycode.js';
 import { url as local } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 import type { MkABehavior } from '@/components/global/MkA.vue';
 
diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue
index c2dc05efe6..f813bcb73f 100644
--- a/packages/frontend/src/components/grid/MkDataCell.vue
+++ b/packages/frontend/src/components/grid/MkDataCell.vue
@@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script setup lang="ts">
 import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue';
 import { GridEventEmitter } from '@/components/grid/grid.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
 import * as os from '@/os.js';
 import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
 import type { Size } from '@/components/grid/grid.js';
diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue
index c89e23c135..94f4f3dab1 100644
--- a/packages/frontend/src/components/grid/MkGrid.vue
+++ b/packages/frontend/src/components/grid/MkGrid.vue
@@ -66,7 +66,7 @@ import {
 import * as os from '@/os.js';
 import { createColumn } from '@/components/grid/column.js';
 import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js';
-import { handleKeyEvent } from '@/scripts/key-event.js';
+import { handleKeyEvent } from '@/utility/key-event.js';
 
 import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
 import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
diff --git a/packages/frontend/src/components/grid/grid-utils.ts b/packages/frontend/src/components/grid/grid-utils.ts
index 4f48af194c..9e5402354e 100644
--- a/packages/frontend/src/components/grid/grid-utils.ts
+++ b/packages/frontend/src/components/grid/grid-utils.ts
@@ -10,7 +10,7 @@ import { CELL_ADDRESS_NONE } from '@/components/grid/cell.js';
 import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
 import type { GridRow } from '@/components/grid/row.js';
 import type { GridContext } from '@/components/grid/grid-event.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import type { GridColumn, GridColumnSetting } from '@/components/grid/column.js';
 
 export function isCellElement(elem: HTMLElement): boolean {
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index 84436e7adb..df26874c17 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -15,7 +15,7 @@ import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkNote from '@/components/MkNote.vue';
 import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = defineProps<{
 	block: Misskey.entities.PageBlock,
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index e0c7956f6e..7702e250e4 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
+import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 
 const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 0d03282cee..45d4b40fd7 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -5,8 +5,8 @@
 
 import { shallowRef, computed, markRaw, watch } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { get, set } from '@/scripts/idb-proxy.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
+import { get, set } from '@/utility/idb-proxy.js';
 
 const storageCache = await get('emojis');
 export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(Array.isArray(storageCache) ? storageCache : []);
diff --git a/packages/frontend/src/deck.ts b/packages/frontend/src/deck.ts
index dee9a3db78..5607b54515 100644
--- a/packages/frontend/src/deck.ts
+++ b/packages/frontend/src/deck.ts
@@ -7,8 +7,8 @@ import { throttle } from 'throttle-debounce';
 import { notificationTypes } from 'misskey-js';
 import type { BasicTimelineType } from '@/timelines.js';
 import type { SoundStore } from '@/preferences/def.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { deepClone } from '@/scripts/clone.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { deepClone } from '@/utility/clone.js';
 import { store } from '@/store.js';
 
 type ColumnWidget = {
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts
index 98e3d91b29..a68cd1b18b 100644
--- a/packages/frontend/src/directives/adaptive-bg.ts
+++ b/packages/frontend/src/directives/adaptive-bg.ts
@@ -4,7 +4,7 @@
  */
 
 import type { Directive } from 'vue';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 export default {
 	mounted(src, binding, vn) {
diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts
index 4037076cae..af7deb5960 100644
--- a/packages/frontend/src/directives/adaptive-border.ts
+++ b/packages/frontend/src/directives/adaptive-border.ts
@@ -4,7 +4,7 @@
  */
 
 import type { Directive } from 'vue';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 export default {
 	mounted(src, binding, vn) {
diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts
index ec00652381..75e022e98f 100644
--- a/packages/frontend/src/directives/hotkey.ts
+++ b/packages/frontend/src/directives/hotkey.ts
@@ -4,7 +4,7 @@
  */
 
 import type { Directive } from 'vue';
-import { makeHotkey } from '@/scripts/hotkey.js';
+import { makeHotkey } from '@/utility/hotkey.js';
 
 export default {
 	mounted(el, binding) {
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
index 19fd374861..17916fb6d3 100644
--- a/packages/frontend/src/directives/panel.ts
+++ b/packages/frontend/src/directives/panel.ts
@@ -4,7 +4,7 @@
  */
 
 import type { Directive } from 'vue';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 export default {
 	mounted(src, binding, vn) {
diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts
index 6bfe6ac31d..068186dfa0 100644
--- a/packages/frontend/src/directives/tooltip.ts
+++ b/packages/frontend/src/directives/tooltip.ts
@@ -8,7 +8,7 @@
 
 import { defineAsyncComponent, ref } from 'vue';
 import type { Directive } from 'vue';
-import { isTouchUsing } from '@/scripts/touch.js';
+import { isTouchUsing } from '@/utility/touch.js';
 import { popup, alert } from '@/os.js';
 
 const start = isTouchUsing ? 'touchstart' : 'mouseenter';
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index 71cb42b30c..c694d49c8a 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -5,7 +5,7 @@
 
 import { computed, reactive } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js';
 
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 096d404a57..837f333c9a 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -4,15 +4,15 @@
  */
 
 import { computed, reactive } from 'vue';
-import { clearCache } from './scripts/clear-cache.js';
+import { clearCache } from './utility/clear-cache.js';
 import { $i } from '@/account.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
-import { lookup } from '@/scripts/lookup.js';
+import { lookup } from '@/utility/lookup.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { ui } from '@@/js/config.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 
 export const navbarItemDef = reactive({
 	notifications: {
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 2142432953..a009a3f3d1 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -10,10 +10,10 @@ import { EventEmitter } from 'eventemitter3';
 import * as Misskey from 'misskey-js';
 import type { Component, Ref } from 'vue';
 import type { ComponentProps as CP } from 'vue-component-type-helpers';
-import type { Form, GetFormResultType } from '@/scripts/form.js';
+import type { Form, GetFormResultType } from '@/utility/form.js';
 import type { MenuItem } from '@/types/menu.js';
 import type { PostFormProps } from '@/types/post-form.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
@@ -25,11 +25,11 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
 import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
 import MkPopupMenu from '@/components/MkPopupMenu.vue';
 import MkContextMenu from '@/components/MkContextMenu.vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
-import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
-import { focusParent } from '@/scripts/focus.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import { showMovedDialog } from '@/utility/show-moved-dialog.js';
+import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
+import { focusParent } from '@/utility/focus.js';
 
 export const openingWindowsCount = ref(0);
 
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index 9e3a6777f1..05fa8a8a6b 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -30,10 +30,10 @@ import * as Misskey from 'misskey-js';
 import { version } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import MkLink from '@/components/MkLink.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { prefer } from '@/preferences.js';
 import { serverErrorImageUrl } from '@/instance.js';
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 6e5b52ea8a..d734ea64b1 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -140,13 +140,13 @@ import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { physics } from '@/scripts/physics.js';
+import { physics } from '@/utility/physics.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
 import { $i } from '@/account.js';
 
 const patronsWithIcon = [{
diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue
index e5e57c05c4..0b9eee7d49 100644
--- a/packages/frontend/src/pages/about.overview.vue
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -130,7 +130,7 @@ import { host, version } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import number from '@/filters/number.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import FormSplit from '@/components/form/split.vue';
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index ef0fd39ffe..11a2ed4062 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -27,8 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, defineAsyncComponent, ref, watch } from 'vue';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
 const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue
index 77ab473ea2..f23046b476 100644
--- a/packages/frontend/src/pages/achievements.vue
+++ b/packages/frontend/src/pages/achievements.vue
@@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 import MkAchievements from '@/components/MkAchievements.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { $i } from '@/account.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 
 let timer: number | null;
 
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue
index 7ccb996fff..46a5388e7e 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -83,9 +83,9 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import bytes from '@/filters/bytes.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { iAmAdmin, iAmModerator } from '@/account.js';
 
 const tab = ref('overview');
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 578945bf07..e1da305b84 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -229,9 +229,9 @@ import FormSuspense from '@/components/form/suspense.vue';
 import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { acct } from '@/filters/user.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { iAmAdmin, $i, iAmModerator } from '@/account.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index 4762ef3f97..6c47e6397f 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -71,7 +71,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import { rolesCache } from '@/cache.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index 9b1bf51f58..8ba35d65d5 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -39,7 +39,7 @@ import { popupMenu } from '@/os.js';
 import { scrollToTop } from '@@/js/scroll.js';
 import MkButton from '@/components/MkButton.vue';
 import { globalEvents } from '@/events.js';
-import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
+import { injectReactiveMetadata } from '@/utility/page-metadata.js';
 
 type Tab = {
 	key?: string | null;
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index 5f683c7a1d..5b049e5bad 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -77,7 +77,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
 import MkInput from '@/components/MkInput.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkSelect from '@/components/MkSelect.vue';
 import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
 import type { MkSystemWebhookResult } from '@/components/MkSystemWebhookEditor.impl.js';
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
index f5249261be..ee87fae606 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
@@ -49,7 +49,7 @@ import { entities } from 'misskey-js';
 import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
 import XRecipient from './notification-recipient.item.vue';
 import XHeader from '@/pages/admin/_header_.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index c7a45a502e..7df25f48c1 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -65,7 +65,7 @@ import MkSelect from '@/components/MkSelect.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import XAbuseReport from '@/components/MkAbuseReport.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { store } from '@/store.js';
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index 0d67359e47..d707e15c61 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -96,9 +96,9 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const ads = ref<Misskey.entities.Ad[]>([]);
 
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index e420586017..48a072bc8a 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -94,9 +94,9 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 2d314c822d..59b8435595 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -160,10 +160,10 @@ import MkRadios from '@/components/MkRadios.vue';
 import MkInput from '@/components/MkInput.vue';
 import FormSlot from '@/components/form/slot.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { useForm } from '@/scripts/use-form.js';
+import { useForm } from '@/utility/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInfo from '@/components/MkInfo.vue';
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index 95f82c1f24..02f08ecc05 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -111,10 +111,10 @@ import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { instance, fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkColorInput from '@/components/MkColorInput.vue';
 import { host } from '@@/js/config.js';
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
index 06d13cda75..260177c894 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
@@ -85,9 +85,9 @@ import MkGrid from '@/components/grid/MkGrid.vue';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import { validators } from '@/components/grid/cell-validators.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkPagingButtons from '@/components/MkPagingButtons.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
 import { useLoading } from '@/components/hook/useLoading.js';
 
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
index 6537b844c3..666e3c95ac 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
@@ -80,10 +80,10 @@ import * as Misskey from 'misskey-js';
 import { onMounted, ref, useCssModule } from 'vue';
 import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
 import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
-import type { DroppedFile } from '@/scripts/file-drop.js';
+import type { DroppedFile } from '@/utility/file-drop.js';
 import type { GridSetting } from '@/components/grid/grid.js';
 import type { GridRow } from '@/components/grid/row.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import {
 	emptyStrToEmptyArray,
 	emptyStrToNull,
@@ -97,9 +97,9 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { validators } from '@/components/grid/cell-validators.js';
-import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
-import { uploadFile } from '@/scripts/upload.js';
-import { extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
+import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js';
+import { uploadFile } from '@/utility/upload.js';
+import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
 import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
 import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
 
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
index 609d445d79..c868a700f1 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
@@ -143,7 +143,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onMounted, ref, useCssModule } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -152,7 +152,7 @@ import { emptyStrToUndefined, gridSortOrderKeys } from '@/pages/admin/custom-emo
 import MkFolder from '@/components/MkFolder.vue';
 import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
 import * as os from '@/os.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import MkPagingButtons from '@/components/MkPagingButtons.vue';
 import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
 import { useLoading } from '@/components/hook/useLoading.js';
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
index fb930064ff..f75393baac 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script setup lang="ts">
 import { computed, ref } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import XGridLocalComponent from '@/pages/admin/custom-emojis-manager.local.vue';
 import XGridRemoteComponent from '@/pages/admin/custom-emojis-manager.remote.vue';
 import MkPageHeader from '@/components/global/MkPageHeader.vue';
diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue
index e092efd92c..807a4cc401 100644
--- a/packages/frontend/src/pages/admin/database.vue
+++ b/packages/frontend/src/pages/admin/database.vue
@@ -21,11 +21,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import bytes from '@/filters/bytes.js';
 import number from '@/filters/number.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const databasePromiseFactory = () => misskeyApi('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
 
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 5b60e67dac..930c174121 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -73,10 +73,10 @@ import FormSuspense from '@/components/form/suspense.vue';
 import FormSplit from '@/components/form/split.vue';
 import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance, instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 
 const enableEmail = ref<boolean>(false);
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index a312ecce12..559b73db16 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -49,10 +49,10 @@ import MkButton from '@/components/MkButton.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkFolder from '@/components/MkFolder.vue';
 
 const deeplAuthKey = ref<string>('');
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index e7b9fd8621..7fef797c62 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -67,7 +67,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
 import FormSplit from '@/components/form/split.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const host = ref('');
 const state = ref('federating');
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index 4cc859227f..f9955e8d13 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -42,9 +42,9 @@ import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
 import * as os from '@/os.js';
-import { lookupFile } from '@/scripts/admin-lookup.js';
+import { lookupFile } from '@/utility/admin-lookup.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const origin = ref('local');
 const type = ref<string | null>(null);
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 672ee8da18..26159a97d7 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -37,12 +37,12 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue';
 import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { instance } from '@/instance.js';
-import { lookup } from '@/scripts/lookup.js';
+import { lookup } from '@/utility/lookup.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js';
-import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
+import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 import { useRouter } from '@/router/supplier.js';
 
 const isEmpty = (x: string | null) => x == null || x === '';
diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue
index 5189e12899..80bb101e67 100644
--- a/packages/frontend/src/pages/admin/invites.vue
+++ b/packages/frontend/src/pages/admin/invites.vue
@@ -59,7 +59,7 @@ import { computed, ref, shallowRef } from 'vue';
 import XHeader from './_header_.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSelect from '@/components/MkSelect.vue';
@@ -68,7 +68,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkInviteCode from '@/components/MkInviteCode.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index ac1fe7783c..001af0e059 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -134,10 +134,10 @@ import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import FormLink from '@/components/form/link.vue';
 import MkFolder from '@/components/MkFolder.vue';
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index c9eaf07531..9bde2b5978 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -38,7 +38,7 @@ import MkSelect from '@/components/MkSelect.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 
 const logs = shallowRef<InstanceType<typeof MkPagination>>();
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index d5a664934c..6347673704 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -90,10 +90,10 @@ import MkInput from '@/components/MkInput.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 
 const useObjectStorage = ref<boolean>(false);
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 1a72f9050c..bb7eef36ad 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index 197f792755..dd5d4b0e72 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -24,12 +24,12 @@ import { onMounted, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import isChromatic from 'chromatic';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
 import { store } from '@/store.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index 7d80f8c2e3..362f0974e0 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -50,11 +50,11 @@ import { onMounted, ref } from 'vue';
 import XPie from './overview.pie.vue';
 import type { InstanceForPie } from './overview.pie.vue';
 import * as os from '@/os.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import number from '@/filters/number.js';
 import MkNumberDiff from '@/components/MkNumberDiff.vue';
 import { i18n } from '@/i18n.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
 
 const topSubInstancesForPie = ref<InstanceForPie[] | null>(null);
 const topPubInstancesForPie = ref<InstanceForPie[] | null>(null);
diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue
index 67c0297dec..c8291459fe 100644
--- a/packages/frontend/src/pages/admin/overview.instances.vue
+++ b/packages/frontend/src/pages/admin/overview.instances.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue
index 4d503c0035..fb2c5ea13c 100644
--- a/packages/frontend/src/pages/admin/overview.moderators.vue
+++ b/packages/frontend/src/pages/admin/overview.moderators.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { prefer } from '@/preferences.js';
 
 const moderators = ref<Misskey.entities.UserDetailed[] | null>(null);
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index a21ec6c464..792565dd96 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { initChart } from '@/utility/init-chart.js';
 
 export type InstanceForPie = {
 	name: string,
diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue
index 7b83641fbd..6f461b838d 100644
--- a/packages/frontend/src/pages/admin/overview.queue.chart.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue
@@ -11,10 +11,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 72b93ee97c..fd8145b308 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import MkNumberDiff from '@/components/MkNumberDiff.vue';
 import MkNumber from '@/components/MkNumber.vue';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue
index 8de4f298ce..6a39f4561f 100644
--- a/packages/frontend/src/pages/admin/overview.users.vue
+++ b/packages/frontend/src/pages/admin/overview.users.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index 1de4dc0dc8..3c0f3d0769 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -79,10 +79,10 @@ import XModerators from './overview.moderators.vue';
 import XHeatmap from './overview.heatmap.vue';
 import type { InstanceForPie } from './overview.pie.vue';
 import * as os from '@/os.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 
 const rootEl = shallowRef<HTMLElement>();
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index 12338f0bf9..cf9448a813 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -111,15 +111,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import XHeader from './_header_.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkLink from '@/components/MkLink.vue';
-import { useForm } from '@/scripts/use-form.js';
+import { useForm } from '@/utility/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 
 const meta = await misskeyApi('admin/meta');
diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue
index debb48a15a..77d3332523 100644
--- a/packages/frontend/src/pages/admin/queue.chart.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue
@@ -11,10 +11,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 7c171ba0e1..607a974d20 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -53,7 +53,7 @@ import * as Misskey from 'misskey-js';
 import XChart from './queue.chart.chart.vue';
 import type { ApQueueDomain } from '@/pages/admin/queue.vue';
 import number from '@/filters/number.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import MkFolder from '@/components/MkFolder.vue';
diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue
index 1a26c00ddb..e5b62e566b 100644
--- a/packages/frontend/src/pages/admin/queue.vue
+++ b/packages/frontend/src/pages/admin/queue.vue
@@ -23,7 +23,7 @@ import XHeader from './_header_.vue';
 import * as os from '@/os.js';
 import * as config from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 
 export type ApQueueDomain = 'deliver' | 'inbox';
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 17e99e6593..8a6a46934d 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -29,9 +29,9 @@ import * as Misskey from 'misskey-js';
 import XHeader from './_header_.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const relays = ref<Misskey.entities.AdminRelaysListResponse>([]);
 
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index 2b4006c3f7..87aff1e1ef 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -28,9 +28,9 @@ import { v4 as uuid } from 'uuid';
 import XHeader from './_header_.vue';
 import XEditor from './roles.editor.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import { rolesCache } from '@/cache.js';
 import { useRouter } from '@/router/supplier.js';
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index d05f52334e..4e9f4edb70 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -710,7 +710,7 @@ import MkRange from '@/components/MkRange.vue';
 import FormSlot from '@/components/form/slot.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 
 const emit = defineEmits<{
 	(ev: 'update:modelValue', v: any): void;
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 1c237a69b4..4efcab37cb 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -67,9 +67,9 @@ import XHeader from './_header_.vue';
 import XEditor from './roles.editor.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkInfo from '@/components/MkInfo.vue';
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 782c1256de..1b183d67c8 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -290,9 +290,9 @@ import MkButton from '@/components/MkButton.vue';
 import MkRange from '@/components/MkRange.vue';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { instance, fetchInstance } from '@/instance.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { useRouter } from '@/router/supplier.js';
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 975a4a1265..3eaf0fb358 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -131,11 +131,11 @@ import MkRange from '@/components/MkRange.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useForm } from '@/scripts/use-form.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { useForm } from '@/utility/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 
 const meta = await misskeyApi('admin/meta');
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index cd50f92143..314a2be517 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -46,7 +46,7 @@ import XHeader from './_header_.vue';
 import * as os from '@/os.js';
 import { fetchInstance, instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index aed593fc54..06b52f11b9 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -266,14 +266,14 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance, instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
-import { useForm } from '@/scripts/use-form.js';
+import { useForm } from '@/utility/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 import MkRadios from '@/components/MkRadios.vue';
 
diff --git a/packages/frontend/src/pages/admin/system-webhook.vue b/packages/frontend/src/pages/admin/system-webhook.vue
index c59abda24a..26db61cb0b 100644
--- a/packages/frontend/src/pages/admin/system-webhook.vue
+++ b/packages/frontend/src/pages/admin/system-webhook.vue
@@ -30,11 +30,11 @@ import { computed, onMounted, ref } from 'vue';
 import { entities } from 'misskey-js';
 import XItem from './system-webhook.item.vue';
 import FormSection from '@/components/form/section.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import XHeader from '@/pages/admin/_header_.vue';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
 import * as os from '@/os.js';
 
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index 91104b676d..3f722e2dc8 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -68,9 +68,9 @@ import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import * as os from '@/os.js';
-import { lookupUser } from '@/scripts/admin-lookup.js';
+import { lookupUser } from '@/utility/admin-lookup.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import { dateString } from '@/filters/date.js';
 
diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue
index b31807f9f5..81aee08f70 100644
--- a/packages/frontend/src/pages/ads.vue
+++ b/packages/frontend/src/pages/ads.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index b8903a8ea6..c47074165d 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -52,9 +52,9 @@ import { ref, computed, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { $i, updateAccountPartial } from '@/account.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 75c0fd98dc..28d9866a69 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -53,9 +53,9 @@ import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { $i, updateAccountPartial } from '@/account.js';
 
 const paginationCurrent = {
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index a01bafd996..b1fa874f65 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -29,8 +29,8 @@ import * as Misskey from 'misskey-js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { scroll } from '@@/js/scroll.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue
index 9bddb0a9d2..b6942d0b9a 100644
--- a/packages/frontend/src/pages/api-console.vue
+++ b/packages/frontend/src/pages/api-console.vue
@@ -41,8 +41,8 @@ import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const body = ref('{}');
 const endpoint = ref('');
diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue
index f4fb2ef4d5..1917293c06 100644
--- a/packages/frontend/src/pages/auth.form.vue
+++ b/packages/frontend/src/pages/auth.form.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index 4170b4f73e..ef6be9b9ee 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -46,9 +46,9 @@ import { onMounted, ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import XForm from './auth.form.vue';
 import MkSignin from '@/components/MkSignin.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { $i, login } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
index a834f1c5fd..a3c5a36614 100644
--- a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRolePreview from '@/components/MkRolePreview.vue';
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index a5cafb1678..2858be9fd7 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -30,9 +30,9 @@ import { ref, computed, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
 import { signinRequired } from '@/account.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index 6d8274a55c..b695008088 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -74,10 +74,10 @@ import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkColorInput from '@/components/MkColorInput.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index d333204ff8..4a841329e0 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -81,11 +81,11 @@ import MkPostForm from '@/components/MkPostForm.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { $i, iAmModerator } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import MkNotes from '@/components/MkNotes.vue';
 import { favoritedChannelsCache } from '@/cache.js';
 import MkButton from '@/components/MkButton.vue';
@@ -95,9 +95,9 @@ import MkNote from '@/components/MkNote.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { notesSearchAvailable } from '@/scripts/check-permissions.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { notesSearchAvailable } from '@/utility/check-permissions.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index 6830c1ace4..d21d2a9c53 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -69,7 +69,7 @@ import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/pages/clicker.vue b/packages/frontend/src/pages/clicker.vue
index 9e9b5e8688..b0adb98354 100644
--- a/packages/frontend/src/pages/clicker.vue
+++ b/packages/frontend/src/pages/clicker.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import MkClickerGame from '@/components/MkClickerGame.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 definePageMetadata(() => ({
 	title: '🍪👈',
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 224bc51599..5a89151775 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -39,13 +39,13 @@ import MkNotes from '@/components/MkNotes.vue';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import { clipsCache } from '@/cache.js';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { genEmbedCode } from '@/scripts/get-embed-code.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { genEmbedCode } from '@/utility/get-embed-code.js';
 import { assertServerContext, serverContext } from '@/server-context.js';
 
 // contextは非ログイン状態の情報しかないためログイン時は利用できない
diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue
index 1f2bee5a77..46bf551da5 100644
--- a/packages/frontend/src/pages/contact.vue
+++ b/packages/frontend/src/pages/contact.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkLink from '@/components/MkLink.vue';
 
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 82c6d8df4e..450e7ef74b 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -81,12 +81,12 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import FormSplit from '@/components/form/split.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index dfcc82c77b..3f78935b0a 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -85,7 +85,7 @@ import bytes from '@/filters/bytes.js';
 import { infoImageUrl } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
diff --git a/packages/frontend/src/pages/drive.file.vue b/packages/frontend/src/pages/drive.file.vue
index 5711ec8b3a..1a9f13daf4 100644
--- a/packages/frontend/src/pages/drive.file.vue
+++ b/packages/frontend/src/pages/drive.file.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref, defineAsyncComponent } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue
index 25e140f67f..466fedb479 100644
--- a/packages/frontend/src/pages/drive.vue
+++ b/packages/frontend/src/pages/drive.vue
@@ -14,7 +14,7 @@ import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XDrive from '@/components/MkDrive.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const folder = ref<Misskey.entities.DriveFolder | null>(null);
 
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index 55c984e1bf..3478a11ad4 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -198,20 +198,20 @@ import { DropAndFusionGame } from 'misskey-bubble-game';
 import { useInterval } from '@@/js/use-interval.js';
 import { apiUrl } from '@@/js/config.js';
 import type { Mono } from 'misskey-bubble-game';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import * as os from '@/os.js';
 import MkNumber from '@/components/MkNumber.vue';
 import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
 import MkButton from '@/components/MkButton.vue';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { store } from '@/store.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import MkRange from '@/components/MkRange.vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { prefer } from '@/preferences.js';
 
 type FrontendMonoDefinition = {
diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue
index 54352c9b0d..47b04229fc 100644
--- a/packages/frontend/src/pages/drop-and-fusion.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.vue
@@ -88,12 +88,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
 import XGame from './drop-and-fusion.game.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 
 const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
 const gameStarted = ref(false);
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index da3315cff5..581198c89d 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -87,11 +87,11 @@ import MkInput from '@/components/MkInput.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { customEmojiCategories } from '@/custom-emojis.js';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index 979d50966e..35a240b9ba 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -18,8 +18,8 @@ import * as Misskey from 'misskey-js';
 import { defineAsyncComponent } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
 import { $i } from '@/account.js';
diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue
index 389cd23ad2..ffefeb9618 100644
--- a/packages/frontend/src/pages/explore.roles.vue
+++ b/packages/frontend/src/pages/explore.roles.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const roles = ref<Misskey.entities.Role[] | null>(null);
 
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index 56ae08b322..c112045a21 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -68,7 +68,7 @@ import * as Misskey from 'misskey-js';
 import MkUserList from '@/components/MkUserList.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkTab from '@/components/MkTab.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue
index b1a8183d9b..878c7be4d9 100644
--- a/packages/frontend/src/pages/explore.vue
+++ b/packages/frontend/src/pages/explore.vue
@@ -27,7 +27,7 @@ import XUsers from './explore.users.vue';
 import XRoles from './explore.roles.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index 6716566101..aec45d0045 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -30,7 +30,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkNote from '@/components/MkNote.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { infoImageUrl } from '@/instance.js';
 
 const pagination = {
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index d84ec4873b..d01acc3787 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -46,9 +46,9 @@ import * as Misskey from 'misskey-js';
 import { AISCRIPT_VERSION } from '@syuilo/aiscript';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkInput from '@/components/MkInput.vue';
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index 2b85489706..688b5c2ac6 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -46,7 +46,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 532ec87dac..e2e7cc6f74 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -67,23 +67,23 @@ import * as Misskey from 'misskey-js';
 import { Interpreter, Parser, values } from '@syuilo/aiscript';
 import { url } from '@@/js/config.js';
 import type { Ref } from 'vue';
-import type { AsUiComponent, AsUiRoot } from '@/scripts/aiscript/ui.js';
+import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
 import type { MenuItem } from '@/types/menu.js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkAsUi from '@/components/MkAsUi.vue';
-import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { registerAsUiLib } from '@/aiscript/ui.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkCode from '@/components/MkCode.vue';
 import { prefer } from '@/preferences.js';
 import { $i } from '@/account.js';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { pleaseLogin } from '@/utility/please-login.js';
 
 const props = defineProps<{
 	id: string;
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 863ae018ba..43532026fc 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -52,7 +52,7 @@ import MkButton from '@/components/MkButton.vue';
 import { userPage, acct } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { infoImageUrl } from '@/instance.js';
 import { $i } from '@/account.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index 70f8b2c31d..974dd18224 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -45,10 +45,10 @@ import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import FormSuspense from '@/components/form/suspense.vue';
-import { selectFiles } from '@/scripts/select-file.js';
+import { selectFiles } from '@/utility/select-file.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index f396fd2c0c..49b7e349e8 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -52,7 +52,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 7e1b7be16c..81c63b0f4e 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -69,17 +69,17 @@ import { url } from '@@/js/config.js';
 import type { MenuItem } from '@/types/menu.js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { prefer } from '@/preferences.js';
 import { $i } from '@/account.js';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue
index 998b8be0f3..f6eb8a5d7e 100644
--- a/packages/frontend/src/pages/games.vue
+++ b/packages/frontend/src/pages/games.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 definePageMetadata(() => ({
 	title: 'Misskey Games',
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 423bd611e6..055ee8a609 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -54,12 +54,12 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkUrl from '@/components/global/MkUrl.vue';
 import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { parsePluginMeta, installPlugin } from '@/plugin.js';
-import { parseThemeCode, installTheme } from '@/scripts/theme.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { parseThemeCode, installTheme } from '@/utility/theme.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const uiPhase = ref<'fetching' | 'confirm' | 'error'>('fetching');
 const errorKV = ref<{
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 35d1c7dcd1..78ecd180cc 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -146,16 +146,16 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import number from '@/filters/number.js';
 import { iAmModerator, iAmAdmin } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 import { dateString } from '@/filters/date.js';
 import MkTextarea from '@/components/MkTextarea.vue';
 
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 49c0d931c5..fbe54e9951 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -38,12 +38,12 @@ import { computed, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkButton from '@/components/MkButton.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkInviteCode from '@/components/MkInviteCode.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { serverErrorImageUrl, instance } from '@/instance.js';
 import { $i } from '@/account.js';
 
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 0ff1854154..f1b3f7a22f 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -37,12 +37,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkButton from '@/components/MkButton.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { serverErrorImageUrl } from '@/instance.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue
index 6f10c69640..ff072ba527 100644
--- a/packages/frontend/src/pages/lookup.vue
+++ b/packages/frontend/src/pages/lookup.vue
@@ -22,9 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { mainRouter } from '@/router/main.js';
 import MkButton from '@/components/MkButton.vue';
 
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index ab060587c5..3060c97a49 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -36,8 +36,8 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
 import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
 
 import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const props = defineProps<{
 	session: string;
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index 2b8518747f..2f87de76fe 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { antennasCache } from '@/cache.js';
 import { useRouter } from '@/router/supplier.js';
 import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index 9f927cd1a0..ed9afd49ae 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { antennasCache } from '@/cache.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index f387740728..0e9a709a99 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onActivated, computed } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { antennasCache } from '@/cache.js';
 import { infoImageUrl } from '@/instance.js';
 
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index acf37a9a2f..2fba8d3bc9 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -30,9 +30,9 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkClipPreview from '@/components/MkClipPreview.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { clipsCache } from '@/cache.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 6cbcca73c2..5449aded15 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -34,7 +34,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkAvatars from '@/components/MkAvatars.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { userListsCache } from '@/cache.js';
 import { infoImageUrl } from '@/instance.js';
 import { signinRequired } from '@/account.js';
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index acedeeeb7d..5160124994 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -57,8 +57,8 @@ import { computed, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { userPage } from '@/filters/user.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 6a2d01b6fa..4605d67b49 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { pleaseLogin } from '@/utility/please-login.js';
 import { notFoundImageUrl } from '@/instance.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index eaeef2e566..2292072817 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -56,14 +56,14 @@ import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
 import MkNotes from '@/components/MkNotes.vue';
 import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { dateString } from '@/filters/date.js';
 import MkClipPreview from '@/components/MkClipPreview.vue';
 import { prefer } from '@/preferences.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import { getAppearNote } from '@/utility/get-appear-note.js';
 import { serverContext, assertServerContext } from '@/server-context.js';
 import { $i } from '@/account.js';
 
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index 46ee501c76..9887c39e6b 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -29,7 +29,7 @@ import MkNotes from '@/components/MkNotes.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { notificationTypes } from '@@/js/const.js';
 
 const tab = ref('all');
diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue
index 860c884d13..1346c696b9 100644
--- a/packages/frontend/src/pages/oauth.vue
+++ b/packages/frontend/src/pages/oauth.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
 import MkAnimBg from '@/components/MkAnimBg.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
 
 const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index c3ad6657b0..1b98425719 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -26,7 +26,7 @@ import * as Misskey from 'misskey-js';
 import XContainer from '../page-editor.container.vue';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
index 36e03b4790..f275ec9517 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
@@ -30,7 +30,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkNote from '@/components/MkNote.vue';
 import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
index 3fed07f7e8..4d1a3716e7 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
@@ -21,14 +21,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
- 
+
 import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { v4 as uuid } from 'uuid';
 import XContainer from '../page-editor.container.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import MkButton from '@/components/MkButton.vue';
 import { getPageBlockList } from '@/pages/page-editor/common.js';
 
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index 5795b46c00..a8b4da2a06 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -15,12 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
- 
+
 import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import XContainer from '../page-editor.container.vue';
 import { i18n } from '@/i18n.js';
-import { Autocomplete } from '@/scripts/autocomplete.js';
+import { Autocomplete } from '@/utility/autocomplete.js';
 
 const props = defineProps<{
 	modelValue: Misskey.entities.PageBlock & { type: 'text' }
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index c08cfebab3..5750b77d20 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -71,10 +71,10 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkInput from '@/components/MkInput.vue';
 import { url } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { selectFile } from '@/scripts/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { $i } from '@/account.js';
 import { mainRouter } from '@/router/main.js';
 import { getPageBlockList } from '@/pages/page-editor/common.js';
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 3444ef49aa..420b83035b 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -105,7 +105,7 @@ import type { MenuItem } from '@/types/menu.js';
 import XPage from '@/components/page/page.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkMediaImage from '@/components/MkMediaImage.vue';
 import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
@@ -113,14 +113,14 @@ import MkContainer from '@/components/MkContainer.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkPagePreview from '@/components/MkPagePreview.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { pageViewInterruptors } from '@/store.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import { $i } from '@/account.js';
-import { isSupportShare } from '@/scripts/navigator.js';
+import { isSupportShare } from '@/utility/navigator.js';
 import { instance } from '@/instance.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue
index 4ef9d3b091..83b922e5b5 100644
--- a/packages/frontend/src/pages/pages.vue
+++ b/packages/frontend/src/pages/pages.vue
@@ -44,7 +44,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
diff --git a/packages/frontend/src/pages/preview.vue b/packages/frontend/src/pages/preview.vue
index 8e07b190aa..52b41f3153 100644
--- a/packages/frontend/src/pages/preview.vue
+++ b/packages/frontend/src/pages/preview.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import MkSample from '@/components/MkPreview.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const headerActions = computed(() => []);
 
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index 4cacbd0906..3bde026763 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -36,9 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, computed, ref } from 'vue';
 import JSON5 from 'json5';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue
index c40d13f664..393e44beb3 100644
--- a/packages/frontend/src/pages/registry.value.vue
+++ b/packages/frontend/src/pages/registry.value.vue
@@ -48,9 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, computed, ref } from 'vue';
 import JSON5 from 'json5';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue
index c641874b17..b251365100 100644
--- a/packages/frontend/src/pages/registry.vue
+++ b/packages/frontend/src/pages/registry.vue
@@ -26,9 +26,9 @@ import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import JSON5 from 'json5';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index 6d24029535..12226da272 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -25,7 +25,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { mainRouter } from '@/router/main.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 429f502133..71dd220cfe 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -148,16 +148,16 @@ import * as Reversi from 'misskey-reversi';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { signinRequired } from '@/account.js';
 import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { userPage } from '@/filters/user.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import * as os from '@/os.js';
-import { confetti } from '@/scripts/confetti.js';
+import { confetti } from '@/utility/confetti.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 437a1a2294..03b75f89ae 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -115,7 +115,7 @@ import * as Misskey from 'misskey-js';
 import * as Reversi from 'misskey-reversi';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import MkButton from '@/components/MkButton.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index 10ea3717ab..934aa69c6a 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -14,8 +14,8 @@ import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import GameSetting from './game.setting.vue';
 import GameBoard from './game.board.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { useStream } from '@/stream.js';
 import { signinRequired } from '@/account.js';
 import { useRouter } from '@/router/supplier.js';
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index d608a2411c..4000ce13c7 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -107,8 +107,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onDeactivated, onMounted, onUnmounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { useStream } from '@/stream.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
@@ -118,8 +118,8 @@ import MkPagination from '@/components/MkPagination.vue';
 import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import * as sound from '@/scripts/sound.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import * as sound from '@/utility/sound.js';
 
 const myGamesPagination = {
 	endpoint: 'reversi/games' as const,
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index 46e510b49b..19d26a7316 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -38,9 +38,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkUserList from '@/components/MkUserList.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { instanceName } from '@@/js/config.js';
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 22c06bfc85..357e6621ac 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -64,18 +64,18 @@ import MkContainer from '@/components/MkContainer.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import * as os from '@/os.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
-import type { AsUiComponent } from '@/scripts/aiscript/ui.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { registerAsUiLib } from '@/aiscript/ui.js';
+import type { AsUiComponent } from '@/aiscript/ui.js';
 import MkAsUi from '@/components/MkAsUi.vue';
 import { miLocalStorage } from '@/local-storage.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 
-import type { AsUiRoot } from '@/scripts/aiscript/ui.js';
+import type { AsUiRoot } from '@/aiscript/ui.js';
 
 const parser = new Parser();
 let aiscript: Interpreter;
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 32a35f8110..bfe56e793b 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -119,8 +119,8 @@ import { host as localHost } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { apLookup } from '@/scripts/lookup.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { apLookup } from '@/utility/lookup.js';
 import { useRouter } from '@/router/supplier.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index 2b8faf5465..40019cc870 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -36,7 +36,7 @@ import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import * as os from '@/os.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useRouter } from '@/router/supplier.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index 38d7548fa8..e3d1d427a8 100644
--- a/packages/frontend/src/pages/search.vue
+++ b/packages/frontend/src/pages/search.vue
@@ -27,8 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, ref, toRef } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { notesSearchAvailable } from '@/scripts/check-permissions.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { notesSearchAvailable } from '@/utility/check-permissions.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 18c82ffdf6..9093ffd7a9 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -116,7 +116,7 @@ import * as os from '@/os.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkLink from '@/components/MkLink.vue';
-import { confetti } from '@/scripts/confetti.js';
+import { confetti } from '@/utility/confetti.js';
 import { signinRequired } from '@/account.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue
index f464f728ff..c79fb40ae1 100644
--- a/packages/frontend/src/pages/settings/accessibility.vue
+++ b/packages/frontend/src/pages/settings/accessibility.vue
@@ -75,9 +75,9 @@ import { computed, ref, watch } from 'vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import { prefer } from '@/preferences.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
 const reduceAnimation = prefer.model('animation', v => !v, v => !v);
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 4a7301f405..d4a1ffd708 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -33,10 +33,10 @@ import * as Misskey from 'misskey-js';
 import FormSuspense from '@/components/form/suspense.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import type { MenuItem } from '@/types/menu.js';
 
diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue
index b35d406a98..b8bb2bb32a 100644
--- a/packages/frontend/src/pages/settings/api.vue
+++ b/packages/frontend/src/pages/settings/api.vue
@@ -16,9 +16,9 @@ import { defineAsyncComponent, ref, computed } from 'vue';
 import FormLink from '@/components/form/link.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const isDesktop = ref(window.innerWidth >= 1100);
 
diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue
index b23f32aec4..2ecbf535ba 100644
--- a/packages/frontend/src/pages/settings/appearance.vue
+++ b/packages/frontend/src/pages/settings/appearance.vue
@@ -216,13 +216,13 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import { prefer } from '@/preferences.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
 import FormLink from '@/components/form/link.vue';
 import { globalEvents } from '@/events.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import MkButton from '@/components/MkButton.vue';
 import FormSection from '@/components/form/section.vue';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 6515503505..1962623c3e 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -57,9 +57,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import FormPagination from '@/components/MkPagination.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index 79be2b9b1e..dbc0d3697d 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -52,11 +52,11 @@ import * as Misskey from 'misskey-js';
 import XDecoration from './avatar-decoration.decoration.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
 import MkInfo from '@/components/MkInfo.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue
index cf05e75acc..4cdc0d957d 100644
--- a/packages/frontend/src/pages/settings/custom-css.vue
+++ b/packages/frontend/src/pages/settings/custom-css.vue
@@ -18,9 +18,9 @@ import { ref, watch, computed } from 'vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
 
 const localCustomCss = ref(miLocalStorage.getItem('customCss') ?? '');
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index e1965b6d36..6186c493f9 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -24,7 +24,7 @@ import { computed } from 'vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { prefer } from '@/preferences.js';
 
 const navWindow = prefer.model('deck.navWindow');
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index c5657fce68..3c50677746 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -52,14 +52,14 @@ import { computed, ref, watch } from 'vue';
 import type { StyleValue } from 'vue';
 import tinycolor from 'tinycolor2';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkPagination from '@/components/MkPagination.vue';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import { i18n } from '@/i18n.js';
 import bytes from '@/filters/bytes.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkSelect from '@/components/MkSelect.vue';
-import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
+import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
 
 const sortMode = ref('+size');
 const pagination = {
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 8e61b11dbe..aab04ebc9c 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -95,11 +95,11 @@ import FormSection from '@/components/form/section.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import bytes from '@/filters/bytes.js';
 import MkChart from '@/components/MkChart.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { signinRequired } from '@/account.js';
 import { prefer } from '@/preferences.js';
 import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index e7a8fc5634..1ca2e8f085 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -66,10 +66,10 @@ import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkDisableSection from '@/components/MkDisableSection.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { signinRequired } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { instance } from '@/instance.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index d2060a9112..c453952613 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -144,10 +144,10 @@ import MkSelect from '@/components/MkSelect.vue';
 import * as os from '@/os.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { deepClone } from '@/scripts/clone.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
-import { emojiPicker } from '@/scripts/emoji-picker.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { deepClone } from '@/utility/clone.js';
+import { reactionPicker } from '@/utility/reaction-picker.js';
+import { emojiPicker } from '@/utility/emoji-picker.js';
 import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
 import MkEmoji from '@/components/global/MkEmoji.vue';
 import MkFolder from '@/components/MkFolder.vue';
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 949f9019d9..43fb20bb3c 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -150,10 +150,10 @@ import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { selectFile } from '@/scripts/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { $i } from '@/account.js';
 import { store } from '@/store.js';
 
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 6203e7f698..9a87f404e7 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -33,18 +33,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script setup lang="ts">
 import { computed, onActivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
 import { i18n } from '@/i18n.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkSuperMenu from '@/components/MkSuperMenu.vue';
 import { signout, $i } from '@/account.js';
-import { clearCache } from '@/scripts/clear-cache.js';
+import { clearCache } from '@/utility/clear-cache.js';
 import { instance } from '@/instance.js';
-import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
 import * as os from '@/os.js';
 import { useRouter } from '@/router/supplier.js';
-import { searchIndexes } from '@/scripts/autogen/settings-search-index.js';
+import { searchIndexes } from '@/utility/autogen/settings-search-index.js';
 import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
 import { store } from '@/store.js';
 
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index 1c00d64d73..60386da545 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -66,10 +66,10 @@ import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkUserInfo from '@/components/MkUserInfo.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
index d1fde2fc1c..52e1937663 100644
--- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
@@ -20,7 +20,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
 import { signinRequired } from '@/account.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 3620c05ca8..6fa2b5e689 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -178,7 +178,7 @@ import XWordMute from './mute-block.word-mute.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import * as os from '@/os.js';
 import { instance, infoImageUrl } from '@/instance.js';
@@ -186,7 +186,7 @@ import { signinRequired } from '@/account.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { prefer } from '@/preferences.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index 5e1dedd709..bb2b201afb 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -54,9 +54,9 @@ import MkContainer from '@/components/MkContainer.vue';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { store } from '@/store.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { prefer } from '@/preferences.js';
 import { PREF_DEF } from '@/preferences/def.js';
 
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 1e7436bf9c..dfbcf91a3f 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -71,9 +71,9 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
 import { signinRequired } from '@/account.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
 import { notificationTypes } from '@@/js/const.js';
 
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 21133d72e7..f2d998579f 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -116,12 +116,12 @@ import FormInfo from '@/components/MkInfo.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { signout, signinRequired } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import FormSection from '@/components/form/section.vue';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue
index c79aec91c0..c4db223050 100644
--- a/packages/frontend/src/pages/settings/plugin.install.vue
+++ b/packages/frontend/src/pages/settings/plugin.install.vue
@@ -23,9 +23,9 @@ import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkButton from '@/components/MkButton.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { installPlugin } from '@/plugin.js';
 
 const code = ref<string | null>(null);
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index fe57812a1d..741c776bb8 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -102,10 +102,10 @@ import MkCode from '@/components/MkCode.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import * as os from '@/os.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin } from '@/plugin.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index a1aa0e77ec..7e079940a5 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -374,10 +374,10 @@ import MkLink from '@/components/MkLink.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { prefer } from '@/preferences.js';
 import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 0149241337..7660a591b4 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -178,13 +178,13 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { signinRequired } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import FormSlot from '@/components/form/slot.vue';
-import { formatDateTimeString } from '@/scripts/format-time-string.js';
+import { formatDateTimeString } from '@/utility/format-time-string.js';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
 import MkDisableSection from '@/components/MkDisableSection.vue';
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index f9ddbbc9ed..5efd986ce7 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -161,13 +161,13 @@ import MkSelect from '@/components/MkSelect.vue';
 import FormSplit from '@/components/form/split.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import FormSlot from '@/components/form/slot.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
-import { langmap } from '@/scripts/langmap.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { langmap } from '@/utility/langmap.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { store } from '@/store.js';
 import { globalEvents } from '@/events.js';
 import MkInfo from '@/components/MkInfo.vue';
diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue
index 5346a58a79..4b0daff7fa 100644
--- a/packages/frontend/src/pages/settings/roles.vue
+++ b/packages/frontend/src/pages/settings/roles.vue
@@ -28,7 +28,7 @@ import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index f365146e0a..fec760582c 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -56,9 +56,9 @@ import FormSlot from '@/components/form/slot.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const pagination = {
 	endpoint: 'i/signin-history' as const,
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 56f65e2309..1bac19fe47 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -32,15 +32,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed, watch } from 'vue';
-import type { SoundType } from '@/scripts/sound.js';
+import type { SoundType } from '@/utility/sound.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkRange from '@/components/MkRange.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
-import { selectFile } from '@/scripts/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js';
+import { selectFile } from '@/utility/select-file.js';
 
 const props = defineProps<{
 	type: SoundType;
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 808ae06f41..7d800a8a37 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref } from 'vue';
 import XSound from './sounds.sound.vue';
 import type { Ref } from 'vue';
-import type { SoundType, OperationType } from '@/scripts/sound.js';
+import type { SoundType, OperationType } from '@/utility/sound.js';
 import type { SoundStore } from '@/preferences/def.js';
 import { prefer } from '@/preferences.js';
 import MkRange from '@/components/MkRange.vue';
@@ -65,8 +65,8 @@ import MkButton from '@/components/MkButton.vue';
 import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { operationTypes } from '@/scripts/sound.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { operationTypes } from '@/utility/sound.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 import { PREF_DEF } from '@/preferences/def.js';
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index ede395e51e..55d976295a 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -96,7 +96,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkRange from '@/components/MkRange.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue
index 068d28bc4e..2737af3aa4 100644
--- a/packages/frontend/src/pages/settings/statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.vue
@@ -21,9 +21,9 @@ import { v4 as uuid } from 'uuid';
 import XStatusbar from './statusbar.statusbar.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { prefer } from '@/preferences.js';
 
 const statusbars = prefer.r.statusbars;
diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue
index b19b12aaab..536e624a2d 100644
--- a/packages/frontend/src/pages/settings/theme.install.vue
+++ b/packages/frontend/src/pages/settings/theme.install.vue
@@ -20,10 +20,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkButton from '@/components/MkButton.vue';
-import { parseThemeCode, previewTheme, installTheme } from '@/scripts/theme.js';
+import { parseThemeCode, previewTheme, installTheme } from '@/utility/theme.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const installThemeCode = ref<string | null>(null);
 
diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue
index f63f15fc13..2b952cb4e3 100644
--- a/packages/frontend/src/pages/settings/theme.manage.vue
+++ b/packages/frontend/src/pages/settings/theme.manage.vue
@@ -37,13 +37,13 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getBuiltinThemesRef } from '@/scripts/theme.js';
-import type { Theme } from '@/scripts/theme.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { getBuiltinThemesRef } from '@/utility/theme.js';
+import type { Theme } from '@/utility/theme.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import * as os from '@/os.js';
 import { getThemes, removeTheme } from '@/theme-store.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 41de2aa6a6..0254dbda5b 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -83,17 +83,17 @@ import MkSelect from '@/components/MkSelect.vue';
 import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getBuiltinThemesRef } from '@/scripts/theme.js';
-import { selectFile } from '@/scripts/select-file.js';
-import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
+import { getBuiltinThemesRef } from '@/utility/theme.js';
+import { selectFile } from '@/utility/select-file.js';
+import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { uniqueBy } from '@/scripts/array.js';
+import { uniqueBy } from '@/utility/array.js';
 import { getThemes } from '@/theme-store.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { prefer } from '@/preferences.js';
 
 const installedThemes = ref(getThemes());
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index 22b008fb61..83902fe6ba 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -76,9 +76,9 @@ import FormSection from '@/components/form/section.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index 727c4df2d6..ef5b7393bb 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -46,7 +46,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const name = ref('');
 const url = ref('');
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index af8b7ca945..7f84d87973 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -37,7 +37,7 @@ import { computed } from 'vue';
 import MkPagination from '@/components/MkPagination.vue';
 import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 
 const pagination = {
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index 37f6558d64..fc93f86a5b 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -37,9 +37,9 @@ import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { postMessageToParentWindow } from '@/scripts/post-message.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
+import { postMessageToParentWindow } from '@/utility/post-message.js';
 import { i18n } from '@/i18n.js';
 
 const urlParams = new URLSearchParams(window.location.search);
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index 14fb96d4f1..c3f1b4b18e 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -31,7 +31,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
 import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const submitting = ref(false);
 
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 5fc921c55c..f7e0302ca7 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -23,12 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref } from 'vue';
 import MkNotes from '@/components/MkNotes.vue';
 import MkButton from '@/components/MkButton.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { genEmbedCode } from '@/scripts/get-embed-code.js';
+import { genEmbedCode } from '@/utility/get-embed-code.js';
 
 const props = defineProps<{
 	tag: string;
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index c701030f9e..50445d8a34 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -81,19 +81,19 @@ import JSON5 from 'json5';
 import lightTheme from '@@/themes/_light.json5';
 import darkTheme from '@@/themes/_dark.json5';
 import { host } from '@@/js/config.js';
-import type { Theme } from '@/scripts/theme.js';
+import type { Theme } from '@/utility/theme.js';
 import MkButton from '@/components/MkButton.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import { $i } from '@/account.js';
-import { applyTheme } from '@/scripts/theme.js';
+import { applyTheme } from '@/utility/theme.js';
 import * as os from '@/os.js';
 import { store } from '@/store.js';
 import { addTheme } from '@/theme-store.js';
 import { i18n } from '@/i18n.js';
-import { useLeaveGuard } from '@/scripts/use-leave-guard.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useLeaveGuard } from '@/utility/use-leave-guard.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { prefer } from '@/preferences.js';
 
 const bgColors = [
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 4c15194672..ab7d22ec59 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -45,14 +45,14 @@ import MkInfo from '@/components/MkInfo.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
-import { deviceKind } from '@/scripts/device-kind.js';
-import { deepMerge } from '@/scripts/merge.js';
+import { deviceKind } from '@/utility/device-kind.js';
+import { deepMerge } from '@/utility/merge.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index 3efeb46c0a..7ab1c91975 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -28,8 +28,8 @@ import { computed, watch, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { scroll } from '@@/js/scroll.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue
index a77493fe47..af543ac723 100644
--- a/packages/frontend/src/pages/user-tag.vue
+++ b/packages/frontend/src/pages/user-tag.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed } from 'vue';
 import MkUserList from '@/components/MkUserList.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 
 const props = defineProps<{
 	tag: string;
diff --git a/packages/frontend/src/pages/user/achievements.vue b/packages/frontend/src/pages/user/achievements.vue
index 403e74904c..b78ac2dc17 100644
--- a/packages/frontend/src/pages/user/achievements.vue
+++ b/packages/frontend/src/pages/user/achievements.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkAchievements from '@/components/MkAchievements.vue';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { $i } from '@/account.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index 64d20f7945..b4c0fe4052 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -19,12 +19,12 @@ import { Chart } from 'chart.js';
 import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { initChart } from '@/utility/init-chart.js';
+import { chartLegend } from '@/utility/chart-legend.js';
 import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index cb171928eb..56c5204817 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -19,12 +19,12 @@ import { Chart } from 'chart.js';
 import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { initChart } from '@/utility/init-chart.js';
+import { chartLegend } from '@/utility/chart-legend.js';
 import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index 2804211f95..1b81fda95f 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -19,12 +19,12 @@ import { Chart } from 'chart.js';
 import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { initChart } from '@/utility/init-chart.js';
+import { chartLegend } from '@/utility/chart-legend.js';
 import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue
index 70883242e5..f970401646 100644
--- a/packages/frontend/src/pages/user/followers.vue
+++ b/packages/frontend/src/pages/user/followers.vue
@@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XFollowList from './follow-list.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue
index 37b25f694f..08a6630873 100644
--- a/packages/frontend/src/pages/user/following.vue
+++ b/packages/frontend/src/pages/user/following.vue
@@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XFollowList from './follow-list.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 932d944dde..6450f1e077 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -171,18 +171,18 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkOmit from '@/components/MkOmit.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getUserMenu } from '@/scripts/get-user-menu.js';
+import { getUserMenu } from '@/utility/get-user-menu.js';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { $i, iAmModerator } from '@/account.js';
 import { dateString } from '@/filters/date.js';
-import { confetti } from '@/scripts/confetti.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { confetti } from '@/utility/confetti.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
 import { useRouter } from '@/router/supplier.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import MkSparkle from '@/components/MkSparkle.vue';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index b5e5f29ade..6c3b8408fb 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { i18n } from '@/i18n.js';
 import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue';
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index c43f6c76d9..3b27f3a08c 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -35,8 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent, computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { acct as getAcct } from '@/filters/user.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 34c5c3ce6c..ebbe27e3d4 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -42,9 +42,9 @@ import XTimeline from './welcome.timeline.vue';
 import MarqueeText from '@/components/MkMarquee.vue';
 import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
 import misskeysvg from '/client-assets/misskey.svg';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 import { instance as meta } from '@/instance.js';
 
 const instances = ref<Misskey.entities.FederationInstance[]>();
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 33cc139a45..939ca934e8 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -44,7 +44,7 @@ import { host, version } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import MkAnimBg from '@/components/MkAnimBg.vue';
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 9be3a80a9e..180b2e5f77 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { onUpdated, ref, shallowRef } from 'vue';
 import XNote from '@/pages/welcome.timeline.note.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { getScrollContainer } from '@@/js/scroll.js';
 
 const notes = ref<Misskey.entities.Note[]>([]);
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 38d257506c..724356737c 100644
--- a/packages/frontend/src/pages/welcome.vue
+++ b/packages/frontend/src/pages/welcome.vue
@@ -16,7 +16,7 @@ import * as Misskey from 'misskey-js';
 import XSetup from './welcome.setup.vue';
 import XEntrance from './welcome.entrance.a.vue';
 import { instanceName } from '@@/js/config.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePageMetadata } from '@/utility/page-metadata.js';
 import { fetchInstance } from '@/instance.js';
 
 const instance = ref<Misskey.entities.MetaDetailed | null>(null);
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index 778d5c1981..f53f19f342 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -9,12 +9,12 @@ import { onUnmounted, ref, watch } from 'vue';
 import { BroadcastChannel } from 'broadcast-channel';
 import type { Ref } from 'vue';
 import { $i } from '@/account.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { get, set } from '@/scripts/idb-proxy.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { get, set } from '@/utility/idb-proxy.js';
 import { store } from '@/store.js';
 import { useStream } from '@/stream.js';
-import { deepClone } from '@/scripts/clone.js';
-import { deepMerge } from '@/scripts/merge.js';
+import { deepClone } from '@/utility/clone.js';
+import { deepMerge } from '@/utility/merge.js';
 
 type StateDef = Record<string, {
 	where: 'account' | 'device' | 'deviceAccount';
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index 609c819053..4d56be75b9 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -7,10 +7,10 @@ import { ref, defineAsyncComponent } from 'vue';
 import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
 import { compareVersions } from 'compare-versions';
 import { v4 as uuid } from 'uuid';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors, store } from '@/store.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index 19284de6a1..d17cff0f5d 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -5,10 +5,10 @@
 
 import * as Misskey from 'misskey-js';
 import { hemisphere } from '@@/js/intl-const.js';
-import type { Theme } from '@/scripts/theme.js';
-import type { SoundType } from '@/scripts/sound.js';
+import type { Theme } from '@/utility/theme.js';
+import type { SoundType } from '@/utility/sound.js';
 import type { Plugin } from '@/plugin.js';
-import { DEFAULT_DEVICE_KIND } from '@/scripts/device-kind.js';
+import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
 
 /** サウンド設定 */
 export type SoundStore = {
diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/profile.ts
index 516fa2b557..1d03639d37 100644
--- a/packages/frontend/src/preferences/profile.ts
+++ b/packages/frontend/src/preferences/profile.ts
@@ -11,7 +11,7 @@ import { PREF_DEF } from './def.js';
 import { Store } from './store.js';
 import type { MenuItem } from '@/types/menu.js';
 import { $i } from '@/account.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 
 // NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
index e9dd088462..2d1921f8a9 100644
--- a/packages/frontend/src/preferences/utility.ts
+++ b/packages/frontend/src/preferences/utility.ts
@@ -6,15 +6,15 @@
 import { ref, watch } from 'vue';
 import type { PreferencesProfile } from './profile.js';
 import type { MenuItem } from '@/types/menu.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { prefer, profileManager } from '@/preferences.js';
 import * as os from '@/os.js';
 import { store } from '@/store.js';
 import { $i } from '@/account.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 
 function canAutoBackup() {
 	return profileManager.profile.name != null && profileManager.profile.name.trim() !== '';
diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts
deleted file mode 100644
index f5d0ab559f..0000000000
--- a/packages/frontend/src/scripts/achievements.ts
+++ /dev/null
@@ -1,513 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { $i } from '@/account.js';
-
-export const ACHIEVEMENT_TYPES = [
-	'notes1',
-	'notes10',
-	'notes100',
-	'notes500',
-	'notes1000',
-	'notes5000',
-	'notes10000',
-	'notes20000',
-	'notes30000',
-	'notes40000',
-	'notes50000',
-	'notes60000',
-	'notes70000',
-	'notes80000',
-	'notes90000',
-	'notes100000',
-	'login3',
-	'login7',
-	'login15',
-	'login30',
-	'login60',
-	'login100',
-	'login200',
-	'login300',
-	'login400',
-	'login500',
-	'login600',
-	'login700',
-	'login800',
-	'login900',
-	'login1000',
-	'passedSinceAccountCreated1',
-	'passedSinceAccountCreated2',
-	'passedSinceAccountCreated3',
-	'loggedInOnBirthday',
-	'loggedInOnNewYearsDay',
-	'noteClipped1',
-	'noteFavorited1',
-	'myNoteFavorited1',
-	'profileFilled',
-	'markedAsCat',
-	'following1',
-	'following10',
-	'following50',
-	'following100',
-	'following300',
-	'followers1',
-	'followers10',
-	'followers50',
-	'followers100',
-	'followers300',
-	'followers500',
-	'followers1000',
-	'collectAchievements30',
-	'viewAchievements3min',
-	'iLoveMisskey',
-	'foundTreasure',
-	'client30min',
-	'client60min',
-	'noteDeletedWithin1min',
-	'postedAtLateNight',
-	'postedAt0min0sec',
-	'selfQuote',
-	'htl20npm',
-	'viewInstanceChart',
-	'outputHelloWorldOnScratchpad',
-	'open3windows',
-	'driveFolderCircularReference',
-	'reactWithoutRead',
-	'clickedClickHere',
-	'justPlainLucky',
-	'setNameToSyuilo',
-	'cookieClicked',
-	'brainDiver',
-	'smashTestNotificationButton',
-	'tutorialCompleted',
-	'bubbleGameExplodingHead',
-	'bubbleGameDoubleExplodingHead',
-] as const;
-
-export const ACHIEVEMENT_BADGES = {
-	'notes1': {
-		img: '/fluent-emoji/1f4dd.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'notes10': {
-		img: '/fluent-emoji/1f4d1.png',
-		bg: null,
-		frame: 'bronze',
-	},
-	'notes100': {
-		img: '/fluent-emoji/1f4d2.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'notes500': {
-		img: '/fluent-emoji/1f4da.png',
-		bg: null,
-		frame: 'bronze',
-	},
-	'notes1000': {
-		img: '/fluent-emoji/1f5c3.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'notes5000': {
-		img: '/fluent-emoji/1f304.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'notes10000': {
-		img: '/fluent-emoji/1f3d9.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'silver',
-	},
-	'notes20000': {
-		img: '/fluent-emoji/1f307.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'silver',
-	},
-	'notes30000': {
-		img: '/fluent-emoji/1f306.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'silver',
-	},
-	'notes40000': {
-		img: '/fluent-emoji/1f303.png',
-		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
-		frame: 'silver',
-	},
-	'notes50000': {
-		img: '/fluent-emoji/1fa90.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'gold',
-	},
-	'notes60000': {
-		img: '/fluent-emoji/2604.png',
-		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
-		frame: 'gold',
-	},
-	'notes70000': {
-		img: '/fluent-emoji/1f30c.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'gold',
-	},
-	'notes80000': {
-		img: '/fluent-emoji/1f30c.png',
-		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
-		frame: 'gold',
-	},
-	'notes90000': {
-		img: '/fluent-emoji/1f30c.png',
-		bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
-		frame: 'gold',
-	},
-	'notes100000': {
-		img: '/fluent-emoji/267e.png',
-		bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
-		frame: 'platinum',
-	},
-	'login3': {
-		img: '/fluent-emoji/1f331.png',
-		bg: null,
-		frame: 'bronze',
-	},
-	'login7': {
-		img: '/fluent-emoji/1f331.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'login15': {
-		img: '/fluent-emoji/1f331.png',
-		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
-		frame: 'bronze',
-	},
-	'login30': {
-		img: '/fluent-emoji/1fab4.png',
-		bg: null,
-		frame: 'bronze',
-	},
-	'login60': {
-		img: '/fluent-emoji/1fab4.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'login100': {
-		img: '/fluent-emoji/1fab4.png',
-		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
-		frame: 'silver',
-	},
-	'login200': {
-		img: '/fluent-emoji/1f333.png',
-		bg: null,
-		frame: 'silver',
-	},
-	'login300': {
-		img: '/fluent-emoji/1f333.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'silver',
-	},
-	'login400': {
-		img: '/fluent-emoji/1f333.png',
-		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
-		frame: 'silver',
-	},
-	'login500': {
-		img: '/fluent-emoji/1f304.png',
-		bg: null,
-		frame: 'silver',
-	},
-	'login600': {
-		img: '/fluent-emoji/1f304.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'gold',
-	},
-	'login700': {
-		img: '/fluent-emoji/1f304.png',
-		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
-		frame: 'gold',
-	},
-	'login800': {
-		img: '/fluent-emoji/1f307.png',
-		bg: null,
-		frame: 'gold',
-	},
-	'login900': {
-		img: '/fluent-emoji/1f307.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'gold',
-	},
-	'login1000': {
-		img: '/fluent-emoji/1f307.png',
-		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
-		frame: 'platinum',
-	},
-	'noteClipped1': {
-		img: '/fluent-emoji/1f587.png',
-		bg: null,
-		frame: 'bronze',
-	},
-	'noteFavorited1': {
-		img: '/fluent-emoji/1f31f.png',
-		bg: null,
-		frame: 'bronze',
-	},
-	'myNoteFavorited1': {
-		img: '/fluent-emoji/1f320.png',
-		bg: null,
-		frame: 'silver',
-	},
-	'profileFilled': {
-		img: '/fluent-emoji/1f44c.png',
-		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
-		frame: 'bronze',
-	},
-	'markedAsCat': {
-		img: '/fluent-emoji/1f408.png',
-		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
-		frame: 'bronze',
-	},
-	'following1': {
-		img: '/fluent-emoji/2618.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'following10': {
-		img: '/fluent-emoji/1f6b8.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'following50': {
-		img: '/fluent-emoji/1f91d.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'following100': {
-		img: '/fluent-emoji/1f4af.png',
-		bg: 'linear-gradient(0deg, rgb(255 53 184), rgb(255 206 69))',
-		frame: 'silver',
-	},
-	'following300': {
-		img: '/fluent-emoji/1f970.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'silver',
-	},
-	'followers1': {
-		img: '/fluent-emoji/2618.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'followers10': {
-		img: '/fluent-emoji/1f44b.png',
-		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
-		frame: 'bronze',
-	},
-	'followers50': {
-		img: '/fluent-emoji/1f411.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'bronze',
-	},
-	'followers100': {
-		img: '/fluent-emoji/1f60e.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'silver',
-	},
-	'followers300': {
-		img: '/fluent-emoji/1f3c6.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'silver',
-	},
-	'followers500': {
-		img: '/fluent-emoji/1f4e1.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'gold',
-	},
-	'followers1000': {
-		img: '/fluent-emoji/1f451.png',
-		bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
-		frame: 'platinum',
-	},
-	'collectAchievements30': {
-		img: '/fluent-emoji/1f3c5.png',
-		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
-		frame: 'silver',
-	},
-	'viewAchievements3min': {
-		img: '/fluent-emoji/1f3c5.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'bronze',
-	},
-	'iLoveMisskey': {
-		img: '/fluent-emoji/2764.png',
-		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
-		frame: 'silver',
-	},
-	'foundTreasure': {
-		img: '/fluent-emoji/1f3c6.png',
-		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
-		frame: 'gold',
-	},
-	'client30min': {
-		img: '/fluent-emoji/1f552.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'bronze',
-	},
-	'client60min': {
-		img: '/fluent-emoji/1f552.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'silver',
-	},
-	'noteDeletedWithin1min': {
-		img: '/fluent-emoji/1f5d1.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'bronze',
-	},
-	'postedAtLateNight': {
-		img: '/fluent-emoji/1f319.png',
-		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
-		frame: 'bronze',
-	},
-	'postedAt0min0sec': {
-		img: '/fluent-emoji/1f55b.png',
-		bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))',
-		frame: 'bronze',
-	},
-	'selfQuote': {
-		img: '/fluent-emoji/1f4dd.png',
-		bg: null,
-		frame: 'bronze',
-	},
-	'htl20npm': {
-		img: '/fluent-emoji/1f30a.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'bronze',
-	},
-	'viewInstanceChart': {
-		img: '/fluent-emoji/1f4ca.png',
-		bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))',
-		frame: 'bronze',
-	},
-	'outputHelloWorldOnScratchpad': {
-		img: '/fluent-emoji/1f530.png',
-		bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))',
-		frame: 'bronze',
-	},
-	'open3windows': {
-		img: '/fluent-emoji/1f5a5.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'bronze',
-	},
-	'driveFolderCircularReference': {
-		img: '/fluent-emoji/1f4c2.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'bronze',
-	},
-	'reactWithoutRead': {
-		img: '/fluent-emoji/2753.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'bronze',
-	},
-	'clickedClickHere': {
-		img: '/fluent-emoji/2757.png',
-		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
-		frame: 'bronze',
-	},
-	'justPlainLucky': {
-		img: '/fluent-emoji/1f340.png',
-		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
-		frame: 'silver',
-	},
-	'setNameToSyuilo': {
-		img: '/fluent-emoji/1f36e.png',
-		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
-		frame: 'bronze',
-	},
-	'passedSinceAccountCreated1': {
-		img: '/fluent-emoji/0031-20e3.png',
-		bg: null,
-		frame: 'bronze',
-	},
-	'passedSinceAccountCreated2': {
-		img: '/fluent-emoji/0032-20e3.png',
-		bg: null,
-		frame: 'silver',
-	},
-	'passedSinceAccountCreated3': {
-		img: '/fluent-emoji/0033-20e3.png',
-		bg: null,
-		frame: 'gold',
-	},
-	'loggedInOnBirthday': {
-		img: '/fluent-emoji/1f382.png',
-		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
-		frame: 'silver',
-	},
-	'loggedInOnNewYearsDay': {
-		img: '/fluent-emoji/1f38d.png',
-		bg: 'linear-gradient(0deg, rgb(255 144 144), rgb(255 232 168))',
-		frame: 'silver',
-	},
-	'cookieClicked': {
-		img: '/fluent-emoji/1f36a.png',
-		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
-		frame: 'bronze',
-	},
-	'brainDiver': {
-		img: '/fluent-emoji/1f9e0.png',
-		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
-		frame: 'bronze',
-	},
-	'smashTestNotificationButton': {
-		img: '/fluent-emoji/1f514.png',
-		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
-		frame: 'bronze',
-	},
-	'tutorialCompleted': {
-		img: '/fluent-emoji/1f393.png',
-		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
-		frame: 'bronze',
-	},
-	'bubbleGameExplodingHead': {
-		img: '/fluent-emoji/1f92f.png',
-		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
-		frame: 'bronze',
-	},
-	'bubbleGameDoubleExplodingHead': {
-		img: '/fluent-emoji/1f92f.png',
-		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
-		frame: 'silver',
-	},
-/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
-} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
-	img: string;
-	bg: string | null;
-	frame: 'bronze' | 'silver' | 'gold' | 'platinum';
-}>;
- */
-} as const;
-
-export const claimedAchievements: typeof ACHIEVEMENT_TYPES[number][] = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : [];
-
-const claimingQueue = new Set<string>();
-
-export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
-	if ($i == null) return;
-	if ($i.movedTo) return;
-	if (claimedAchievements.includes(type)) return;
-	claimingQueue.add(type);
-	claimedAchievements.push(type);
-	await new Promise(resolve => setTimeout(resolve, (claimingQueue.size - 1) * 500));
-	window.setTimeout(() => {
-		claimingQueue.delete(type);
-	}, 500);
-	misskeyApi('i/claim-achievement', { name: type });
-}
-
-if (_DEV_) {
-	(window as any).unlockAllAchievements = () => {
-		for (const t of ACHIEVEMENT_TYPES) {
-			claimAchievement(t);
-		}
-	};
-}
diff --git a/packages/frontend/src/scripts/admin-lookup.ts b/packages/frontend/src/scripts/admin-lookup.ts
deleted file mode 100644
index 1b57b853c9..0000000000
--- a/packages/frontend/src/scripts/admin-lookup.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-import { i18n } from '@/i18n.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-
-export async function lookupUser() {
-	const { canceled, result } = await os.inputText({
-		title: i18n.ts.usernameOrUserId,
-	});
-	if (canceled) return;
-
-	const show = (user) => {
-		os.pageWindow(`/admin/user/${user.id}`);
-	};
-
-	const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result));
-	const idPromise = misskeyApi('users/show', { userId: result });
-	let _notFound = false;
-	const notFound = () => {
-		if (_notFound) {
-			os.alert({
-				type: 'error',
-				text: i18n.ts.noSuchUser,
-			});
-		} else {
-			_notFound = true;
-		}
-	};
-	usernamePromise.then(show).catch(err => {
-		if (err.code === 'NO_SUCH_USER') {
-			notFound();
-		}
-	});
-	idPromise.then(show).catch(err => {
-		notFound();
-	});
-}
-
-export async function lookupUserByEmail() {
-	const { canceled, result } = await os.inputText({
-		title: i18n.ts.emailAddress,
-		type: 'email',
-	});
-	if (canceled) return;
-
-	try {
-		const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result });
-
-		os.pageWindow(`/admin/user/${user.id}`);
-	} catch (err) {
-		if (err.code === 'USER_NOT_FOUND') {
-			os.alert({
-				type: 'error',
-				text: i18n.ts.noSuchUser,
-			});
-		} else {
-			throw err;
-		}
-	}
-}
-
-export async function lookupFile() {
-	const { canceled, result: q } = await os.inputText({
-		title: i18n.ts.fileIdOrUrl,
-		minLength: 1,
-	});
-	if (canceled) return;
-
-	const show = (file) => {
-		os.pageWindow(`/admin/file/${file.id}`);
-	};
-
-	misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
-		show(file);
-	}).catch(err => {
-		if (err.code === 'NO_SUCH_FILE') {
-			os.alert({
-				type: 'error',
-				text: i18n.ts.notFound,
-			});
-		}
-	});
-}
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts
deleted file mode 100644
index 2c0c8c816e..0000000000
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { errors, utils, values } from '@syuilo/aiscript';
-import * as Misskey from 'misskey-js';
-import { url, lang } from '@@/js/config.js';
-import { assertStringAndIsIn } from './common.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { $i } from '@/account.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { customEmojis } from '@/custom-emojis.js';
-
-const DIALOG_TYPES = [
-	'error',
-	'info',
-	'success',
-	'warning',
-	'waiting',
-	'question',
-] as const;
-
-export function aiScriptReadline(q: string): Promise<string> {
-	return new Promise(ok => {
-		os.inputText({
-			title: q,
-		}).then(({ result: a }) => {
-			ok(a ?? '');
-		});
-	});
-}
-
-export function createAiScriptEnv(opts: { storageKey: string, token?: string }) {
-	return {
-		USER_ID: $i ? values.STR($i.id) : values.NULL,
-		USER_NAME: $i?.name ? values.STR($i.name) : values.NULL,
-		USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
-		CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
-		LOCALE: values.STR(lang),
-		SERVER_URL: values.STR(url),
-		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
-			utils.assertString(title);
-			utils.assertString(text);
-			if (type != null) {
-				assertStringAndIsIn(type, DIALOG_TYPES);
-			}
-			await os.alert({
-				type: type ? type.value : 'info',
-				title: title.value,
-				text: text.value,
-			});
-			return values.NULL;
-		}),
-		'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
-			utils.assertString(title);
-			utils.assertString(text);
-			if (type != null) {
-				assertStringAndIsIn(type, DIALOG_TYPES);
-			}
-			const confirm = await os.confirm({
-				type: type ? type.value : 'question',
-				title: title.value,
-				text: text.value,
-			});
-			return confirm.canceled ? values.FALSE : values.TRUE;
-		}),
-		'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
-			utils.assertString(ep);
-			if (ep.value.includes('://')) {
-				throw new errors.AiScriptRuntimeError('invalid endpoint');
-			}
-			if (token) {
-				utils.assertString(token);
-				// バグがあればundefinedもあり得るため念のため
-				if (typeof token.value !== 'string') throw new Error('invalid token');
-			}
-			const actualToken: string | null = token?.value ?? opts.token ?? null;
-			if (param == null) {
-				throw new errors.AiScriptRuntimeError('expected param');
-			}
-			utils.assertObject(param);
-			return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => {
-				return utils.jsToVal(res);
-			}, err => {
-				return values.ERROR('request_failed', utils.jsToVal(err));
-			});
-		}),
-		/* セキュリティ上の問題があるため無効化
-		'Mk:apiExternal': values.FN_NATIVE(async ([host, ep, param, token]) => {
-			utils.assertString(host);
-			utils.assertString(ep);
-			if (token) utils.assertString(token);
-			return os.apiExternal(host.value, ep.value, utils.valToJs(param), token?.value).then(res => {
-				return utils.jsToVal(res);
-			}, err => {
-				return values.ERROR('request_failed', utils.jsToVal(err));
-			});
-		}),
-		*/
-		'Mk:save': values.FN_NATIVE(([key, value]) => {
-			utils.assertString(key);
-			utils.expectAny(value);
-			miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));
-			return values.NULL;
-		}),
-		'Mk:load': values.FN_NATIVE(([key]) => {
-			utils.assertString(key);
-			return utils.jsToVal(miLocalStorage.getItemAsJson(`aiscript:${opts.storageKey}:${key.value}`) ?? null);
-		}),
-		'Mk:remove': values.FN_NATIVE(([key]) => {
-			utils.assertString(key);
-			miLocalStorage.removeItem(`aiscript:${opts.storageKey}:${key.value}`);
-			return values.NULL;
-		}),
-		'Mk:url': values.FN_NATIVE(() => {
-			return values.STR(window.location.href);
-		}),
-		'Mk:nyaize': values.FN_NATIVE(([text]) => {
-			utils.assertString(text);
-			return values.STR(Misskey.nyaize(text.value));
-		}),
-	};
-}
diff --git a/packages/frontend/src/scripts/aiscript/common.ts b/packages/frontend/src/scripts/aiscript/common.ts
deleted file mode 100644
index ba5dfb8368..0000000000
--- a/packages/frontend/src/scripts/aiscript/common.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { errors, utils } from '@syuilo/aiscript';
-import type { values } from '@syuilo/aiscript';
-
-export function assertStringAndIsIn<A extends readonly string[]>(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } {
-	utils.assertString(value);
-	const str = value.value;
-	if (!expects.includes(str)) {
-		const expected = expects.map((expect) => `"${expect}"`).join(', ');
-		throw new errors.AiScriptRuntimeError(`"${value.value}" is not in ${expected}`);
-	}
-}
diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts
deleted file mode 100644
index 46e193f7c1..0000000000
--- a/packages/frontend/src/scripts/aiscript/ui.ts
+++ /dev/null
@@ -1,655 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { utils, values } from '@syuilo/aiscript';
-import { v4 as uuid } from 'uuid';
-import { ref } from 'vue';
-import type { Ref } from 'vue';
-import * as Misskey from 'misskey-js';
-import { assertStringAndIsIn } from './common.js';
-
-const ALIGNS = ['left', 'center', 'right'] as const;
-const FONTS = ['serif', 'sans-serif', 'monospace'] as const;
-const BORDER_STYLES = ['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] as const;
-
-type Align = (typeof ALIGNS)[number];
-type Font = (typeof FONTS)[number];
-type BorderStyle = (typeof BORDER_STYLES)[number];
-
-export type AsUiComponentBase = {
-	id: string;
-	hidden?: boolean;
-};
-
-export type AsUiRoot = AsUiComponentBase & {
-	type: 'root';
-	children: AsUiComponent['id'][];
-};
-
-export type AsUiContainer = AsUiComponentBase & {
-	type: 'container';
-	children?: AsUiComponent['id'][];
-	align?: Align;
-	bgColor?: string;
-	fgColor?: string;
-	font?: Font;
-	borderWidth?: number;
-	borderColor?: string;
-	borderStyle?: BorderStyle;
-	borderRadius?: number;
-	padding?: number;
-	rounded?: boolean;
-	hidden?: boolean;
-};
-
-export type AsUiText = AsUiComponentBase & {
-	type: 'text';
-	text?: string;
-	size?: number;
-	bold?: boolean;
-	color?: string;
-	font?: Font;
-};
-
-export type AsUiMfm = AsUiComponentBase & {
-	type: 'mfm';
-	text?: string;
-	size?: number;
-	bold?: boolean;
-	color?: string;
-	font?: Font;
-	onClickEv?: (evId: string) => Promise<void>;
-};
-
-export type AsUiButton = AsUiComponentBase & {
-	type: 'button';
-	text?: string;
-	onClick?: () => Promise<void>;
-	primary?: boolean;
-	rounded?: boolean;
-	disabled?: boolean;
-};
-
-export type AsUiButtons = AsUiComponentBase & {
-	type: 'buttons';
-	buttons?: AsUiButton[];
-};
-
-export type AsUiSwitch = AsUiComponentBase & {
-	type: 'switch';
-	onChange?: (v: boolean) => Promise<void>;
-	default?: boolean;
-	label?: string;
-	caption?: string;
-};
-
-export type AsUiTextarea = AsUiComponentBase & {
-	type: 'textarea';
-	onInput?: (v: string) => Promise<void>;
-	default?: string;
-	label?: string;
-	caption?: string;
-};
-
-export type AsUiTextInput = AsUiComponentBase & {
-	type: 'textInput';
-	onInput?: (v: string) => Promise<void>;
-	default?: string;
-	label?: string;
-	caption?: string;
-};
-
-export type AsUiNumberInput = AsUiComponentBase & {
-	type: 'numberInput';
-	onInput?: (v: number) => Promise<void>;
-	default?: number;
-	label?: string;
-	caption?: string;
-};
-
-export type AsUiSelect = AsUiComponentBase & {
-	type: 'select';
-	items?: {
-		text: string;
-		value: string;
-	}[];
-	onChange?: (v: string) => Promise<void>;
-	default?: string;
-	label?: string;
-	caption?: string;
-};
-
-export type AsUiFolder = AsUiComponentBase & {
-	type: 'folder';
-	children?: AsUiComponent['id'][];
-	title?: string;
-	opened?: boolean;
-};
-
-type PostFormPropsForAsUi = {
-	text: string;
-	cw?: string;
-	visibility?: (typeof Misskey.noteVisibilities)[number];
-	localOnly?: boolean;
-};
-
-export type AsUiPostFormButton = AsUiComponentBase & {
-	type: 'postFormButton';
-	text?: string;
-	primary?: boolean;
-	rounded?: boolean;
-	form?: PostFormPropsForAsUi;
-};
-
-export type AsUiPostForm = AsUiComponentBase & {
-	type: 'postForm';
-	form?: PostFormPropsForAsUi;
-};
-
-export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm;
-
-type Options<T extends AsUiComponent> = T extends AsUiButtons
-	? Omit<T, 'id' | 'type' | 'buttons'> & { 'buttons'?: Options<AsUiButton>[] }
-	: Omit<T, 'id' | 'type'>;
-
-export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
-	// TODO
-}
-
-function getRootOptions(def: values.Value | undefined): Options<AsUiRoot> {
-	utils.assertObject(def);
-
-	const children = def.value.get('children');
-	utils.assertArray(children);
-
-	return {
-		children: children.value.map(v => {
-			utils.assertObject(v);
-			const id = v.value.get('id');
-			utils.assertString(id);
-			return id.value;
-		}),
-	};
-}
-
-function getContainerOptions(def: values.Value | undefined): Options<AsUiContainer> {
-	utils.assertObject(def);
-
-	const children = def.value.get('children');
-	if (children) utils.assertArray(children);
-	const align = def.value.get('align');
-	if (align) assertStringAndIsIn(align, ALIGNS);
-	const bgColor = def.value.get('bgColor');
-	if (bgColor) utils.assertString(bgColor);
-	const fgColor = def.value.get('fgColor');
-	if (fgColor) utils.assertString(fgColor);
-	const font = def.value.get('font');
-	if (font) assertStringAndIsIn(font, FONTS);
-	const borderWidth = def.value.get('borderWidth');
-	if (borderWidth) utils.assertNumber(borderWidth);
-	const borderColor = def.value.get('borderColor');
-	if (borderColor) utils.assertString(borderColor);
-	const borderStyle = def.value.get('borderStyle');
-	if (borderStyle) assertStringAndIsIn(borderStyle, BORDER_STYLES);
-	const borderRadius = def.value.get('borderRadius');
-	if (borderRadius) utils.assertNumber(borderRadius);
-	const padding = def.value.get('padding');
-	if (padding) utils.assertNumber(padding);
-	const rounded = def.value.get('rounded');
-	if (rounded) utils.assertBoolean(rounded);
-	const hidden = def.value.get('hidden');
-	if (hidden) utils.assertBoolean(hidden);
-
-	return {
-		children: children ? children.value.map(v => {
-			utils.assertObject(v);
-			const id = v.value.get('id');
-			utils.assertString(id);
-			return id.value;
-		}) : [],
-		align: align?.value,
-		fgColor: fgColor?.value,
-		bgColor: bgColor?.value,
-		font: font?.value,
-		borderWidth: borderWidth?.value,
-		borderColor: borderColor?.value,
-		borderStyle: borderStyle?.value,
-		borderRadius: borderRadius?.value,
-		padding: padding?.value,
-		rounded: rounded?.value,
-		hidden: hidden?.value,
-	};
-}
-
-function getTextOptions(def: values.Value | undefined): Options<AsUiText> {
-	utils.assertObject(def);
-
-	const text = def.value.get('text');
-	if (text) utils.assertString(text);
-	const size = def.value.get('size');
-	if (size) utils.assertNumber(size);
-	const bold = def.value.get('bold');
-	if (bold) utils.assertBoolean(bold);
-	const color = def.value.get('color');
-	if (color) utils.assertString(color);
-	const font = def.value.get('font');
-	if (font) assertStringAndIsIn(font, FONTS);
-
-	return {
-		text: text?.value,
-		size: size?.value,
-		bold: bold?.value,
-		color: color?.value,
-		font: font?.value,
-	};
-}
-
-function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiMfm> {
-	utils.assertObject(def);
-
-	const text = def.value.get('text');
-	if (text) utils.assertString(text);
-	const size = def.value.get('size');
-	if (size) utils.assertNumber(size);
-	const bold = def.value.get('bold');
-	if (bold) utils.assertBoolean(bold);
-	const color = def.value.get('color');
-	if (color) utils.assertString(color);
-	const font = def.value.get('font');
-	if (font) assertStringAndIsIn(font, FONTS);
-	const onClickEv = def.value.get('onClickEv');
-	if (onClickEv) utils.assertFunction(onClickEv);
-
-	return {
-		text: text?.value,
-		size: size?.value,
-		bold: bold?.value,
-		color: color?.value,
-		font: font?.value,
-		onClickEv: async (evId: string) => {
-			if (onClickEv) await call(onClickEv, [values.STR(evId)]);
-		},
-	};
-}
-
-function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextInput> {
-	utils.assertObject(def);
-
-	const onInput = def.value.get('onInput');
-	if (onInput) utils.assertFunction(onInput);
-	const defaultValue = def.value.get('default');
-	if (defaultValue) utils.assertString(defaultValue);
-	const label = def.value.get('label');
-	if (label) utils.assertString(label);
-	const caption = def.value.get('caption');
-	if (caption) utils.assertString(caption);
-
-	return {
-		onInput: async (v) => {
-			if (onInput) await call(onInput, [utils.jsToVal(v)]);
-		},
-		default: defaultValue?.value,
-		label: label?.value,
-		caption: caption?.value,
-	};
-}
-
-function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextarea> {
-	utils.assertObject(def);
-
-	const onInput = def.value.get('onInput');
-	if (onInput) utils.assertFunction(onInput);
-	const defaultValue = def.value.get('default');
-	if (defaultValue) utils.assertString(defaultValue);
-	const label = def.value.get('label');
-	if (label) utils.assertString(label);
-	const caption = def.value.get('caption');
-	if (caption) utils.assertString(caption);
-
-	return {
-		onInput: async (v) => {
-			if (onInput) await call(onInput, [utils.jsToVal(v)]);
-		},
-		default: defaultValue?.value,
-		label: label?.value,
-		caption: caption?.value,
-	};
-}
-
-function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiNumberInput> {
-	utils.assertObject(def);
-
-	const onInput = def.value.get('onInput');
-	if (onInput) utils.assertFunction(onInput);
-	const defaultValue = def.value.get('default');
-	if (defaultValue) utils.assertNumber(defaultValue);
-	const label = def.value.get('label');
-	if (label) utils.assertString(label);
-	const caption = def.value.get('caption');
-	if (caption) utils.assertString(caption);
-
-	return {
-		onInput: async (v) => {
-			if (onInput) await call(onInput, [utils.jsToVal(v)]);
-		},
-		default: defaultValue?.value,
-		label: label?.value,
-		caption: caption?.value,
-	};
-}
-
-function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButton> {
-	utils.assertObject(def);
-
-	const text = def.value.get('text');
-	if (text) utils.assertString(text);
-	const onClick = def.value.get('onClick');
-	if (onClick) utils.assertFunction(onClick);
-	const primary = def.value.get('primary');
-	if (primary) utils.assertBoolean(primary);
-	const rounded = def.value.get('rounded');
-	if (rounded) utils.assertBoolean(rounded);
-	const disabled = def.value.get('disabled');
-	if (disabled) utils.assertBoolean(disabled);
-
-	return {
-		text: text?.value,
-		onClick: async () => {
-			if (onClick) await call(onClick, []);
-		},
-		primary: primary?.value,
-		rounded: rounded?.value,
-		disabled: disabled?.value,
-	};
-}
-
-function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButtons> {
-	utils.assertObject(def);
-
-	const buttons = def.value.get('buttons');
-	if (buttons) utils.assertArray(buttons);
-
-	return {
-		buttons: buttons ? buttons.value.map(button => {
-			utils.assertObject(button);
-			const text = button.value.get('text');
-			utils.assertString(text);
-			const onClick = button.value.get('onClick');
-			utils.assertFunction(onClick);
-			const primary = button.value.get('primary');
-			if (primary) utils.assertBoolean(primary);
-			const rounded = button.value.get('rounded');
-			if (rounded) utils.assertBoolean(rounded);
-			const disabled = button.value.get('disabled');
-			if (disabled) utils.assertBoolean(disabled);
-
-			return {
-				text: text.value,
-				onClick: async () => {
-					await call(onClick, []);
-				},
-				primary: primary?.value,
-				rounded: rounded?.value,
-				disabled: disabled?.value,
-			};
-		}) : [],
-	};
-}
-
-function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSwitch> {
-	utils.assertObject(def);
-
-	const onChange = def.value.get('onChange');
-	if (onChange) utils.assertFunction(onChange);
-	const defaultValue = def.value.get('default');
-	if (defaultValue) utils.assertBoolean(defaultValue);
-	const label = def.value.get('label');
-	if (label) utils.assertString(label);
-	const caption = def.value.get('caption');
-	if (caption) utils.assertString(caption);
-
-	return {
-		onChange: async (v) => {
-			if (onChange) await call(onChange, [utils.jsToVal(v)]);
-		},
-		default: defaultValue?.value,
-		label: label?.value,
-		caption: caption?.value,
-	};
-}
-
-function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSelect> {
-	utils.assertObject(def);
-
-	const items = def.value.get('items');
-	if (items) utils.assertArray(items);
-	const onChange = def.value.get('onChange');
-	if (onChange) utils.assertFunction(onChange);
-	const defaultValue = def.value.get('default');
-	if (defaultValue) utils.assertString(defaultValue);
-	const label = def.value.get('label');
-	if (label) utils.assertString(label);
-	const caption = def.value.get('caption');
-	if (caption) utils.assertString(caption);
-
-	return {
-		items: items ? items.value.map(item => {
-			utils.assertObject(item);
-			const text = item.value.get('text');
-			utils.assertString(text);
-			const value = item.value.get('value');
-			if (value) utils.assertString(value);
-			return {
-				text: text.value,
-				value: value ? value.value : text.value,
-			};
-		}) : [],
-		onChange: async (v) => {
-			if (onChange) await call(onChange, [utils.jsToVal(v)]);
-		},
-		default: defaultValue?.value,
-		label: label?.value,
-		caption: caption?.value,
-	};
-}
-
-function getFolderOptions(def: values.Value | undefined): Options<AsUiFolder> {
-	utils.assertObject(def);
-
-	const children = def.value.get('children');
-	if (children) utils.assertArray(children);
-	const title = def.value.get('title');
-	if (title) utils.assertString(title);
-	const opened = def.value.get('opened');
-	if (opened) utils.assertBoolean(opened);
-
-	return {
-		children: children ? children.value.map(v => {
-			utils.assertObject(v);
-			const id = v.value.get('id');
-			utils.assertString(id);
-			return id.value;
-		}) : [],
-		title: title?.value ?? '',
-		opened: opened?.value ?? true,
-	};
-}
-
-function getPostFormProps(form: values.VObj): PostFormPropsForAsUi {
-	const text = form.value.get('text');
-	utils.assertString(text);
-	const cw = form.value.get('cw');
-	if (cw) utils.assertString(cw);
-	const visibility = form.value.get('visibility');
-	if (visibility) utils.assertString(visibility);
-	const localOnly = form.value.get('localOnly');
-	if (localOnly) utils.assertBoolean(localOnly);
-
-	return {
-		text: text.value,
-		cw: cw?.value,
-		visibility: (visibility?.value && (Misskey.noteVisibilities as readonly string[]).includes(visibility.value)) ? visibility.value as typeof Misskey.noteVisibilities[number] : undefined,
-		localOnly: localOnly?.value,
-	};
-}
-
-function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostFormButton> {
-	utils.assertObject(def);
-
-	const text = def.value.get('text');
-	if (text) utils.assertString(text);
-	const primary = def.value.get('primary');
-	if (primary) utils.assertBoolean(primary);
-	const rounded = def.value.get('rounded');
-	if (rounded) utils.assertBoolean(rounded);
-	const form = def.value.get('form');
-	if (form) utils.assertObject(form);
-
-	return {
-		text: text?.value,
-		primary: primary?.value,
-		rounded: rounded?.value,
-		form: form ? getPostFormProps(form) : {
-			text: '',
-		},
-	};
-}
-
-function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostForm> {
-	utils.assertObject(def);
-
-	const form = def.value.get('form');
-	if (form) utils.assertObject(form);
-
-	return {
-		form: form ? getPostFormProps(form) : {
-			text: '',
-		},
-	};
-}
-
-export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
-	type OptionsConverter<T extends AsUiComponent, C> = (def: values.Value | undefined, call: C) => Options<T>;
-
-	const instances = {};
-
-	function createComponentInstance<T extends AsUiComponent, C>(
-		type: T['type'],
-		def: values.Value | undefined,
-		id: values.Value | undefined,
-		getOptions: OptionsConverter<T, C>,
-		call: C,
-	) {
-		if (id) utils.assertString(id);
-		const _id = id?.value ?? uuid();
-		const component = ref({
-			...getOptions(def, call),
-			type,
-			id: _id,
-		} as T);
-		components.push(component);
-		const instance = values.OBJ(new Map<string, values.Value>([
-			['id', values.STR(_id)],
-			['update', values.FN_NATIVE(([def], opts) => {
-				utils.assertObject(def);
-				const updates = getOptions(def, call);
-				for (const update of def.value.keys()) {
-					if (!Object.hasOwn(updates, update)) continue;
-					component.value[update] = updates[update];
-				}
-			})],
-		]));
-		instances[_id] = instance;
-		return instance;
-	}
-
-	const rootInstance = createComponentInstance('root', utils.jsToVal({ children: [] }), utils.jsToVal('___root___'), getRootOptions, () => {});
-	const rootComponent = components[0] as Ref<AsUiRoot>;
-	done(rootComponent);
-
-	return {
-		'Ui:root': rootInstance,
-
-		'Ui:patch': values.FN_NATIVE(([id, val], opts) => {
-			utils.assertString(id);
-			utils.assertArray(val);
-			// patch(id.value, val.value, opts.call); // TODO
-		}),
-
-		'Ui:get': values.FN_NATIVE(([id], opts) => {
-			utils.assertString(id);
-			const instance = instances[id.value];
-			if (instance) {
-				return instance;
-			} else {
-				return values.NULL;
-			}
-		}),
-
-		// Ui:root.update({ children: [...] }) の糖衣構文
-		'Ui:render': values.FN_NATIVE(([children], opts) => {
-			utils.assertArray(children);
-
-			rootComponent.value.children = children.value.map(v => {
-				utils.assertObject(v);
-				const id = v.value.get('id');
-				utils.assertString(id);
-				return id.value;
-			});
-		}),
-
-		'Ui:C:container': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('container', def, id, getContainerOptions, opts.topCall);
-		}),
-
-		'Ui:C:text': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('text', def, id, getTextOptions, opts.topCall);
-		}),
-
-		'Ui:C:mfm': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('mfm', def, id, getMfmOptions, opts.topCall);
-		}),
-
-		'Ui:C:textarea': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('textarea', def, id, getTextareaOptions, opts.topCall);
-		}),
-
-		'Ui:C:textInput': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('textInput', def, id, getTextInputOptions, opts.topCall);
-		}),
-
-		'Ui:C:numberInput': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.topCall);
-		}),
-
-		'Ui:C:button': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('button', def, id, getButtonOptions, opts.topCall);
-		}),
-
-		'Ui:C:buttons': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('buttons', def, id, getButtonsOptions, opts.topCall);
-		}),
-
-		'Ui:C:switch': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('switch', def, id, getSwitchOptions, opts.topCall);
-		}),
-
-		'Ui:C:select': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('select', def, id, getSelectOptions, opts.topCall);
-		}),
-
-		'Ui:C:folder': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('folder', def, id, getFolderOptions, opts.topCall);
-		}),
-
-		'Ui:C:postFormButton': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.topCall);
-		}),
-
-		'Ui:C:postForm': values.FN_NATIVE(([def, id], opts) => {
-			return createComponentInstance('postForm', def, id, getPostFormOptions, opts.topCall);
-		}),
-	};
-}
diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts
deleted file mode 100644
index f2feb29dfc..0000000000
--- a/packages/frontend/src/scripts/array.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-type EndoRelation<T> = (a: T, b: T) => boolean;
-type Predicate<T> = (x: T) => boolean;
-
-/**
- * Count the number of elements that satisfy the predicate
- */
-
-export function countIf<T>(f: Predicate<T>, xs: T[]): number {
-	return xs.filter(f).length;
-}
-
-/**
- * Count the number of elements that is equal to the element
- */
-export function count<T>(a: T, xs: T[]): number {
-	return countIf(x => x === a, xs);
-}
-
-/**
- * Concatenate an array of arrays
- */
-export function concat<T>(xss: T[][]): T[] {
-	return ([] as T[]).concat(...xss);
-}
-
-/**
- * Intersperse the element between the elements of the array
- * @param sep The element to be interspersed
- */
-export function intersperse<T>(sep: T, xs: T[]): T[] {
-	return concat(xs.map(x => [sep, x])).slice(1);
-}
-
-/**
- * Returns the array of elements that is not equal to the element
- */
-export function erase<T>(a: T, xs: T[]): T[] {
-	return xs.filter(x => x !== a);
-}
-
-/**
- * Finds the array of all elements in the first array not contained in the second array.
- * The order of result values are determined by the first array.
- */
-export function difference<T>(xs: T[], ys: T[]): T[] {
-	return xs.filter(x => !ys.includes(x));
-}
-
-/**
- * Remove all but the first element from every group of equivalent elements
- */
-export function unique<T>(xs: T[]): T[] {
-	return [...new Set(xs)];
-}
-
-export function uniqueBy<TValue, TKey>(values: TValue[], keySelector: (value: TValue) => TKey): TValue[] {
-	const map = new Map<TKey, TValue>();
-
-	for (const value of values) {
-		const key = keySelector(value);
-		if (!map.has(key)) map.set(key, value);
-	}
-
-	return [...map.values()];
-}
-
-export function sum(xs: number[]): number {
-	return xs.reduce((a, b) => a + b, 0);
-}
-
-export function maximum(xs: number[]): number {
-	return Math.max(...xs);
-}
-
-/**
- * Compare two arrays by lexicographical order
- */
-export function lessThan(xs: number[], ys: number[]): boolean {
-	for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
-		if (xs[i] < ys[i]) return true;
-		if (xs[i] > ys[i]) return false;
-	}
-	return xs.length < ys.length;
-}
-
-/**
- * Returns the longest prefix of elements that satisfy the predicate
- */
-export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
-	const ys: T[] = [];
-	for (const x of xs) {
-		if (f(x)) {
-			ys.push(x);
-		} else {
-			break;
-		}
-	}
-	return ys;
-}
-
-export function cumulativeSum(xs: number[]): number[] {
-	const ys = Array.from(xs); // deep copy
-	for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
-	return ys;
-}
-
-export function toArray<T>(x: T | T[] | undefined): T[] {
-	return Array.isArray(x) ? x : x != null ? [x] : [];
-}
-
-export function toSingle<T>(x: T | T[] | undefined): T | undefined {
-	return Array.isArray(x) ? x[0] : x;
-}
diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts
deleted file mode 100644
index 9a603b848c..0000000000
--- a/packages/frontend/src/scripts/autocomplete.ts
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { nextTick, ref, defineAsyncComponent } from 'vue';
-import getCaretCoordinates from 'textarea-caret';
-import { toASCII } from 'punycode.js';
-import type { Ref } from 'vue';
-import { popup } from '@/os.js';
-
-export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
-
-export class Autocomplete {
-	private suggestion: {
-		x: Ref<number>;
-		y: Ref<number>;
-		q: Ref<any>;
-		close: () => void;
-	} | null;
-	private textarea: HTMLInputElement | HTMLTextAreaElement;
-	private currentType: string;
-	private textRef: Ref<string | number | null>;
-	private opening: boolean;
-	private onlyType: SuggestionType[];
-
-	private get text(): string {
-		// Use raw .value to get the latest value
-		// (Because v-model does not update while composition)
-		return this.textarea.value;
-	}
-
-	private set text(text: string) {
-		// Use ref value to notify other watchers
-		// (Because .value setter never fires input/change events)
-		this.textRef.value = text;
-	}
-
-	/**
-	 * 対象のテキストエリアを与えてインスタンスを初期化します。
-	 */
-	constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string | number | null>, onlyType?: SuggestionType[]) {
-		//#region BIND
-		this.onInput = this.onInput.bind(this);
-		this.complete = this.complete.bind(this);
-		this.close = this.close.bind(this);
-		//#endregion
-
-		this.suggestion = null;
-		this.textarea = textarea;
-		this.textRef = textRef;
-		this.opening = false;
-		this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam'];
-
-		this.attach();
-	}
-
-	/**
-	 * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
-	 */
-	public attach() {
-		this.textarea.addEventListener('input', this.onInput);
-	}
-
-	/**
-	 * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
-	 */
-	public detach() {
-		this.textarea.removeEventListener('input', this.onInput);
-		this.close();
-	}
-
-	/**
-	 * テキスト入力時
-	 */
-	private onInput() {
-		const caretPos = this.textarea.selectionStart;
-		const text = this.text.substring(0, caretPos).split('\n').pop()!;
-
-		const mentionIndex = text.lastIndexOf('@');
-		const hashtagIndex = text.lastIndexOf('#');
-		const emojiIndex = text.lastIndexOf(':');
-		const mfmTagIndex = text.lastIndexOf('$');
-		const mfmParamIndex = text.lastIndexOf('.');
-
-		const max = Math.max(
-			mentionIndex,
-			hashtagIndex,
-			emojiIndex,
-			mfmTagIndex);
-
-		if (max === -1) {
-			this.close();
-			return;
-		}
-
-		const afterLastMfmParam = text.split(/\$\[[a-zA-Z]+/).pop();
-
-		const isMention = mentionIndex !== -1;
-		const isHashtag = hashtagIndex !== -1;
-		const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam.includes(' ');
-		const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
-		const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
-
-		let opened = false;
-
-		if (isMention && this.onlyType.includes('user')) {
-			// ユーザのサジェスト中に@を入力すると、その位置から新たにユーザ名を取りなおそうとしてしまう
-			// この動きはリモートユーザのサジェストを阻害するので、@を検知したらその位置よりも前の@を探し、
-			// ホスト名を含むリモートのユーザ名を全て拾えるようにする
-			const mentionIndexAlt = text.lastIndexOf('@', mentionIndex - 1);
-			const username = mentionIndexAlt === -1
-				? text.substring(mentionIndex + 1)
-				: text.substring(mentionIndexAlt + 1);
-			if (username !== '' && username.match(/^[a-zA-Z0-9_@.]+$/)) {
-				this.open('user', username);
-				opened = true;
-			} else if (username === '') {
-				this.open('user', null);
-				opened = true;
-			}
-		}
-
-		if (isHashtag && !opened && this.onlyType.includes('hashtag')) {
-			const hashtag = text.substring(hashtagIndex + 1);
-			if (!hashtag.includes(' ')) {
-				this.open('hashtag', hashtag);
-				opened = true;
-			}
-		}
-
-		if (isEmoji && !opened && this.onlyType.includes('emoji')) {
-			const emoji = text.substring(emojiIndex + 1);
-			if (!emoji.includes(' ')) {
-				this.open('emoji', emoji);
-				opened = true;
-			}
-		}
-
-		if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) {
-			const mfmTag = text.substring(mfmTagIndex + 1);
-			if (!mfmTag.includes(' ')) {
-				this.open('mfmTag', mfmTag.replace('[', ''));
-				opened = true;
-			}
-		}
-
-		if (isMfmParam && !opened && this.onlyType.includes('mfmParam')) {
-			const mfmParam = text.substring(mfmParamIndex + 1);
-			if (!mfmParam.includes(' ')) {
-				this.open('mfmParam', {
-					tag: text.substring(mfmTagIndex + 2, mfmParamIndex),
-					params: mfmParam.split(','),
-				});
-				opened = true;
-			}
-		}
-
-		if (!opened) {
-			this.close();
-		}
-	}
-
-	/**
-	 * サジェストを提示します。
-	 */
-	private async open(type: string, q: any) {
-		if (type !== this.currentType) {
-			this.close();
-		}
-		if (this.opening) return;
-		this.opening = true;
-		this.currentType = type;
-
-		//#region サジェストを表示すべき位置を計算
-		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
-
-		const rect = this.textarea.getBoundingClientRect();
-
-		const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
-		const y = rect.top + caretPosition.top - this.textarea.scrollTop;
-		//#endregion
-
-		if (this.suggestion) {
-			this.suggestion.x.value = x;
-			this.suggestion.y.value = y;
-			this.suggestion.q.value = q;
-
-			this.opening = false;
-		} else {
-			const _x = ref(x);
-			const _y = ref(y);
-			const _q = ref(q);
-
-			const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
-				textarea: this.textarea,
-				close: this.close,
-				type: type,
-				q: _q,
-				x: _x,
-				y: _y,
-			}, {
-				done: (res) => {
-					this.complete(res);
-				},
-			});
-
-			this.suggestion = {
-				q: _q,
-				x: _x,
-				y: _y,
-				close: () => dispose(),
-			};
-
-			this.opening = false;
-		}
-	}
-
-	/**
-	 * サジェストを閉じます。
-	 */
-	private close() {
-		if (this.suggestion == null) return;
-
-		this.suggestion.close();
-		this.suggestion = null;
-
-		this.textarea.focus();
-	}
-
-	/**
-	 * オートコンプリートする
-	 */
-	private complete({ type, value }) {
-		this.close();
-
-		const caret = this.textarea.selectionStart;
-
-		if (type === 'user') {
-			const source = this.text;
-
-			const before = source.substring(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
-			const after = source.substring(caret);
-
-			const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
-
-			// 挿入
-			this.text = `${trimmedBefore}@${acct} ${after}`;
-
-			// キャレットを戻す
-			nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + (acct.length + 2);
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		} else if (type === 'hashtag') {
-			const source = this.text;
-
-			const before = source.substring(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
-			const after = source.substring(caret);
-
-			// 挿入
-			this.text = `${trimmedBefore}#${value} ${after}`;
-
-			// キャレットを戻す
-			nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + (value.length + 2);
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		} else if (type === 'emoji') {
-			const source = this.text;
-
-			const before = source.substring(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
-			const after = source.substring(caret);
-
-			// 挿入
-			this.text = trimmedBefore + value + after;
-
-			// キャレットを戻す
-			nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + value.length;
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		} else if (type === 'mfmTag') {
-			const source = this.text;
-
-			const before = source.substring(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
-			const after = source.substring(caret);
-
-			// 挿入
-			this.text = `${trimmedBefore}$[${value} ]${after}`;
-
-			// キャレットを戻す
-			nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + (value.length + 3);
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		} else if (type === 'mfmParam') {
-			const source = this.text;
-
-			const before = source.substring(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf('.'));
-			const after = source.substring(caret);
-
-			// 挿入
-			this.text = `${trimmedBefore}.${value}${after}`;
-
-			// キャレットを戻す
-			nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + (value.length + 1);
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		}
-	}
-}
diff --git a/packages/frontend/src/scripts/autogen/settings-search-index.ts b/packages/frontend/src/scripts/autogen/settings-search-index.ts
deleted file mode 100644
index 8e2a896b17..0000000000
--- a/packages/frontend/src/scripts/autogen/settings-search-index.ts
+++ /dev/null
@@ -1,822 +0,0 @@
-
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-// This file was automatically generated by create-search-index.
-// Do not edit this file.
-
-import { i18n } from '@/i18n.js';
-
-export type SearchIndexItem = {
-	id: string;
-	path?: string;
-	label: string;
-	keywords: string[];
-	icon?: string;
-	children?: SearchIndexItem[];
-};
-
-export const searchIndexes: SearchIndexItem[] = [
-	{
-		id: 'flXd1LC7r',
-		children: [
-			{
-				id: 'hB11H5oul',
-				label: i18n.ts.syncDeviceDarkMode,
-				keywords: ['sync', 'device', 'dark', 'light', 'mode'],
-			},
-			{
-				id: 'fDbLtIKeo',
-				label: i18n.ts.themeForLightMode,
-				keywords: ['light', 'theme'],
-			},
-			{
-				id: 'eLOwK5Ia2',
-				label: i18n.ts.themeForDarkMode,
-				keywords: ['dark', 'theme'],
-			},
-			{
-				id: 'ujvMfyzUr',
-				label: i18n.ts.setWallpaper,
-				keywords: ['wallpaper'],
-			},
-		],
-		label: i18n.ts.theme,
-		keywords: ['theme'],
-		path: '/settings/theme',
-		icon: 'ti ti-palette',
-	},
-	{
-		id: '6fFIRXUww',
-		children: [
-			{
-				id: 'nO7NnzqiC',
-				label: i18n.ts.notUseSound,
-				keywords: ['mute'],
-			},
-			{
-				id: 'oALW4ja7U',
-				label: i18n.ts.useSoundOnlyWhenActive,
-				keywords: ['active', 'mute'],
-			},
-			{
-				id: 'BbJK2SKT2',
-				label: i18n.ts.masterVolume,
-				keywords: ['volume', 'master'],
-			},
-		],
-		label: i18n.ts.sounds,
-		keywords: ['sounds'],
-		path: '/settings/sounds',
-		icon: 'ti ti-music',
-	},
-	{
-		id: '5BjnxMfYV',
-		children: [
-			{
-				id: '3UqdSCaFw',
-				children: [
-					{
-						id: '75QPEg57v',
-						label: i18n.ts.changePassword,
-						keywords: [],
-					},
-				],
-				label: i18n.ts.password,
-				keywords: ['password'],
-			},
-			{
-				id: '2fa',
-				children: [
-					{
-						id: 'qCXM0HtJ7',
-						label: i18n.ts.totp,
-						keywords: ['totp', 'app', i18n.ts.totpDescription],
-					},
-					{
-						id: '3g1RePuD9',
-						label: i18n.ts.securityKeyAndPasskey,
-						keywords: ['security', 'key', 'passkey'],
-					},
-					{
-						id: 'pFRud5u8k',
-						label: i18n.ts.passwordLessLogin,
-						keywords: ['password', 'less', 'key', 'passkey', 'login', 'signin', i18n.ts.passwordLessLoginDescription],
-					},
-				],
-				label: i18n.ts['2fa'],
-				keywords: ['2fa'],
-			},
-		],
-		label: i18n.ts.security,
-		keywords: ['security'],
-		path: '/settings/security',
-		icon: 'ti ti-lock',
-	},
-	{
-		id: 'w4L6myH61',
-		children: [
-			{
-				id: 'ru8DrOn3J',
-				label: i18n.ts._profile.changeBanner,
-				keywords: ['banner', 'change'],
-			},
-			{
-				id: 'CCnD8Apnu',
-				label: i18n.ts._profile.changeAvatar,
-				keywords: ['avatar', 'icon', 'change'],
-			},
-			{
-				id: 'yFEVCJxFX',
-				label: i18n.ts._profile.name,
-				keywords: ['name'],
-			},
-			{
-				id: '2O1S5reaB',
-				label: i18n.ts._profile.description,
-				keywords: ['description', 'bio'],
-			},
-			{
-				id: 'pWi4OLS8g',
-				label: i18n.ts.location,
-				keywords: ['location', 'locale'],
-			},
-			{
-				id: 'oLO5X6Wtw',
-				label: i18n.ts.birthday,
-				keywords: ['birthday', 'birthdate', 'age'],
-			},
-			{
-				id: 'm2trKwPgq',
-				label: i18n.ts.language,
-				keywords: ['language', 'locale'],
-			},
-			{
-				id: 'kfDZxCDp9',
-				label: i18n.ts._profile.metadataEdit,
-				keywords: ['metadata'],
-			},
-			{
-				id: 'uPt3MFymp',
-				label: i18n.ts._profile.followedMessage,
-				keywords: ['follow', 'message', i18n.ts._profile.followedMessageDescription],
-			},
-			{
-				id: 'wuGg0tBjw',
-				label: i18n.ts.reactionAcceptance,
-				keywords: ['reaction'],
-			},
-			{
-				id: 'EezPpmMnf',
-				children: [
-					{
-						id: 'f2cRLh8ad',
-						label: i18n.ts.flagAsCat,
-						keywords: ['cat'],
-					},
-					{
-						id: 'eVoViiF3h',
-						label: i18n.ts.flagAsBot,
-						keywords: ['bot'],
-					},
-				],
-				label: i18n.ts.advancedSettings,
-				keywords: [],
-			},
-		],
-		label: i18n.ts.profile,
-		keywords: ['profile'],
-		path: '/settings/profile',
-		icon: 'ti ti-user',
-	},
-	{
-		id: '2rp9ka5Ht',
-		children: [
-			{
-				id: 'qBUSKPxLW',
-				label: i18n.ts.makeFollowManuallyApprove,
-				keywords: ['follow', 'lock', i18n.ts.lockedAccountInfo],
-			},
-			{
-				id: '3LZBlZCej',
-				label: i18n.ts.autoAcceptFollowed,
-				keywords: ['follow', 'auto', 'accept'],
-			},
-			{
-				id: '9gOp28wKG',
-				label: i18n.ts.makeReactionsPublic,
-				keywords: ['reaction', 'public', i18n.ts.makeReactionsPublicDescription],
-			},
-			{
-				id: 'CjAkqMhct',
-				label: i18n.ts.followingVisibility,
-				keywords: ['following', 'visibility'],
-			},
-			{
-				id: '4nEwI6LYt',
-				label: i18n.ts.followersVisibility,
-				keywords: ['follower', 'visibility'],
-			},
-			{
-				id: 'naMp37wTL',
-				label: i18n.ts.hideOnlineStatus,
-				keywords: ['online', 'status', i18n.ts.hideOnlineStatusDescription],
-			},
-			{
-				id: 'p0dCVR0UP',
-				label: i18n.ts.noCrawle,
-				keywords: ['crawle', 'index', 'search', i18n.ts.noCrawleDescription],
-			},
-			{
-				id: 'aceURmNPq',
-				label: i18n.ts.preventAiLearning,
-				keywords: ['crawle', 'ai', i18n.ts.preventAiLearningDescription],
-			},
-			{
-				id: 'ahABA0j7u',
-				label: i18n.ts.makeExplorable,
-				keywords: ['explore', i18n.ts.makeExplorableDescription],
-			},
-			{
-				id: 'cyeDbLN8N',
-				children: [
-					{
-						id: 'xEYlOghao',
-						label: i18n.ts._accountSettings.requireSigninToViewContents,
-						keywords: ['login', 'signin'],
-					},
-					{
-						id: 'sMmYFCS60',
-						label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore,
-						keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription],
-					},
-					{
-						id: '2prkeWRSd',
-						label: i18n.ts._accountSettings.makeNotesHiddenBefore,
-						keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription],
-					},
-				],
-				label: i18n.ts.lockdown,
-				keywords: ['lockdown'],
-			},
-		],
-		label: i18n.ts.privacy,
-		keywords: ['privacy'],
-		path: '/settings/privacy',
-		icon: 'ti ti-lock-open',
-	},
-	{
-		id: '3yCAv0IsZ',
-		children: [
-			{
-				id: 'x1GWSQnPw',
-				label: i18n.ts.uiLanguage,
-				keywords: ['language'],
-			},
-			{
-				id: 'EOSa4rtt3',
-				label: i18n.ts.overridedDeviceKind,
-				keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
-			},
-			{
-				id: 'm9LhX8BG8',
-				label: i18n.ts.showFixedPostForm,
-				keywords: ['post', 'form', 'timeline'],
-			},
-			{
-				id: 'snyCQ5oKE',
-				label: i18n.ts.showFixedPostFormInChannel,
-				keywords: ['post', 'form', 'timeline', 'channel'],
-			},
-			{
-				id: '8j36S4Ev6',
-				label: i18n.ts.pinnedList,
-				keywords: ['pinned', 'list'],
-			},
-			{
-				id: 'CWpyT9vLK',
-				label: i18n.ts.enableQuickAddMfmFunction,
-				keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
-			},
-			{
-				id: '1yhown1Xc',
-				label: i18n.ts.rememberNoteVisibility,
-				keywords: ['remember', 'keep', 'note', 'visibility'],
-			},
-			{
-				id: 'wUeAI5QBV',
-				label: i18n.ts.defaultNoteVisibility,
-				keywords: ['default', 'note', 'visibility'],
-			},
-			{
-				id: '6kMj4HVOg',
-				children: [
-					{
-						id: 'DQIcvf64G',
-						label: i18n.ts.collapseRenotes,
-						keywords: ['renote', i18n.ts.collapseRenotesDescription],
-					},
-					{
-						id: 'igFN7RIUa',
-						label: i18n.ts.showNoteActionsOnlyHover,
-						keywords: ['hover', 'show', 'footer', 'action'],
-					},
-					{
-						id: '9uxocbLO0',
-						label: i18n.ts.showClipButtonInNoteFooter,
-						keywords: ['footer', 'action', 'clip', 'show'],
-					},
-					{
-						id: 'eaT1O1Fao',
-						label: i18n.ts.enableAdvancedMfm,
-						keywords: ['mfm', 'enable', 'show', 'advanced'],
-					},
-					{
-						id: 'omxZk3eET',
-						label: i18n.ts.showReactionsCount,
-						keywords: ['reaction', 'count', 'show'],
-					},
-					{
-						id: 'epvi2Nv2G',
-						label: i18n.ts.loadRawImages,
-						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
-					},
-				],
-				label: i18n.ts.note,
-				keywords: ['note'],
-			},
-			{
-				id: 'jb3HUeyrx',
-				children: [
-					{
-						id: 'ykifk3NHS',
-						label: i18n.ts.useGroupedNotifications,
-						keywords: ['group'],
-					},
-				],
-				label: i18n.ts.notifications,
-				keywords: ['notification'],
-			},
-			{
-				id: 'abEAdSpYY',
-				children: [
-					{
-						id: 'lBbtAg0Hm',
-						label: i18n.ts.openImageInNewTab,
-						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
-					},
-					{
-						id: 'E9whefUtX',
-						label: i18n.ts.useReactionPickerForContextMenu,
-						keywords: ['reaction', 'picker', 'contextmenu', 'open'],
-					},
-					{
-						id: 'iQaBbJBva',
-						label: i18n.ts.enableInfiniteScroll,
-						keywords: ['load', 'auto', 'more'],
-					},
-					{
-						id: 'hgEVGgJa1',
-						label: i18n.ts.disableStreamingTimeline,
-						keywords: ['disable', 'streaming', 'timeline'],
-					},
-					{
-						id: 'yxehrHZ6x',
-						label: i18n.ts.alwaysConfirmFollow,
-						keywords: ['follow', 'confirm', 'always'],
-					},
-					{
-						id: 'DdoFLaSG8',
-						label: i18n.ts.confirmWhenRevealingSensitiveMedia,
-						keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
-					},
-					{
-						id: 'uIMCIK7kG',
-						label: i18n.ts.confirmOnReact,
-						keywords: ['reaction', 'confirm'],
-					},
-					{
-						id: 'zvM13vl26',
-						label: i18n.ts.keepCw,
-						keywords: ['remember', 'keep', 'note', 'cw'],
-					},
-					{
-						id: 'm75VEWI3S',
-						label: i18n.ts.whenServerDisconnected,
-						keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
-					},
-					{
-						id: 'bLO9vCyKW',
-						label: i18n.ts.numberOfPageCache,
-						keywords: ['cache', 'page'],
-					},
-					{
-						id: 'iQ7Er89l5',
-						label: i18n.ts.dataSaver,
-						keywords: ['datasaver'],
-					},
-				],
-				label: i18n.ts.behavior,
-				keywords: ['behavior'],
-			},
-			{
-				id: 'C2WYcVM1d',
-				children: [
-					{
-						id: 'Cu7ErCM7C',
-						label: i18n.ts.forceShowAds,
-						keywords: ['ad', 'show'],
-					},
-					{
-						id: 'BBxwy4F6E',
-						label: i18n.ts.hemisphere,
-						keywords: [],
-					},
-					{
-						id: '9YdUwDC8d',
-						label: i18n.ts.additionalEmojiDictionary,
-						keywords: ['emoji', 'dictionary', 'additional', 'extra'],
-					},
-				],
-				label: i18n.ts.other,
-				keywords: [],
-			},
-		],
-		label: i18n.ts.preferences,
-		keywords: ['general', 'preferences'],
-		path: '/settings/preferences',
-		icon: 'ti ti-adjustments',
-	},
-	{
-		id: 'mwkwtw83Y',
-		label: i18n.ts.plugins,
-		keywords: ['plugin'],
-		path: '/settings/plugin',
-		icon: 'ti ti-plug',
-	},
-	{
-		id: 'F1uK9ssiY',
-		children: [
-			{
-				id: 'msAcN6u3S',
-				label: i18n.ts.accountInfo,
-				keywords: ['account', 'info'],
-			},
-			{
-				id: 'ts8DgdnZV',
-				label: i18n.ts.accountMigration,
-				keywords: ['account', 'move', 'migration'],
-			},
-			{
-				id: '4BG7nBECm',
-				label: i18n.ts.closeAccount,
-				keywords: ['account', 'close', 'delete', i18n.ts._accountDelete.requestAccountDelete],
-			},
-			{
-				id: '2qI6ruPgi',
-				label: i18n.ts.experimentalFeatures,
-				keywords: ['experimental', 'feature', 'flags'],
-			},
-			{
-				id: 'cIeaax47o',
-				label: i18n.ts.developer,
-				keywords: ['developer', 'mode', 'debug'],
-			},
-		],
-		label: i18n.ts.other,
-		keywords: ['other'],
-		path: '/settings/other',
-		icon: 'ti ti-dots',
-	},
-	{
-		id: '3icEvyv2D',
-		children: [
-			{
-				id: 'Tyt3gZTy',
-				children: [
-					{
-						id: '9b7ZURyAt',
-						label: i18n.ts.showMutedWord,
-						keywords: ['show'],
-					},
-				],
-				label: i18n.ts.wordMute,
-				keywords: ['note', 'word', 'soft', 'mute', 'hide'],
-			},
-			{
-				id: 'kdMk41II0',
-				label: i18n.ts.hardWordMute,
-				keywords: ['note', 'word', 'hard', 'mute', 'hide'],
-			},
-			{
-				id: 'mjORQamAK',
-				label: i18n.ts.instanceMute,
-				keywords: ['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide'],
-			},
-			{
-				id: '1ZT7S9FZd',
-				label: `${i18n.ts.mutedUsers} (${ i18n.ts.renote })`,
-				keywords: ['renote', 'mute', 'hide', 'user'],
-			},
-			{
-				id: 'ANrPit3kQ',
-				label: i18n.ts.mutedUsers,
-				keywords: ['note', 'mute', 'hide', 'user'],
-			},
-			{
-				id: 'bPAE4lfno',
-				label: i18n.ts.blockedUsers,
-				keywords: ['block', 'user'],
-			},
-		],
-		label: i18n.ts.muteAndBlock,
-		keywords: ['mute', 'block'],
-		path: '/settings/mute-block',
-		icon: 'ti ti-ban',
-	},
-	{
-		id: 'qE2vLlMkF',
-		children: [
-			{
-				id: 'hPPEzjvZC',
-				label: i18n.ts._exportOrImport.allNotes,
-				keywords: ['notes'],
-			},
-			{
-				id: 'AFaeHsCUB',
-				label: i18n.ts._exportOrImport.favoritedNotes,
-				keywords: ['favorite', 'notes'],
-			},
-			{
-				id: 'xyCPmQiRo',
-				label: i18n.ts._exportOrImport.clips,
-				keywords: ['clip', 'notes'],
-			},
-			{
-				id: 'Ch7hWAGUy',
-				label: i18n.ts._exportOrImport.followingList,
-				keywords: ['following', 'users'],
-			},
-			{
-				id: 'AwPgFboEx',
-				label: i18n.ts._exportOrImport.userLists,
-				keywords: ['user', 'lists'],
-			},
-			{
-				id: 'nporiHshC',
-				label: i18n.ts._exportOrImport.muteList,
-				keywords: ['mute', 'users'],
-			},
-			{
-				id: 'BsCzR7vNw',
-				label: i18n.ts._exportOrImport.blockingList,
-				keywords: ['block', 'users'],
-			},
-			{
-				id: 'dvf4IgYrQ',
-				label: i18n.ts.antennas,
-				keywords: ['antennas'],
-			},
-		],
-		label: i18n.ts.importAndExport,
-		keywords: ['import', 'export', 'data'],
-		path: '/settings/import-export',
-		icon: 'ti ti-package',
-	},
-	{
-		id: '3Tcxw4Fwl',
-		children: [
-			{
-				id: 'iIai9O65I',
-				label: i18n.ts.emailAddress,
-				keywords: ['email', 'address'],
-			},
-			{
-				id: 'i6cC6oi0m',
-				label: i18n.ts.receiveAnnouncementFromInstance,
-				keywords: ['announcement', 'email'],
-			},
-			{
-				id: 'C1YTinP11',
-				label: i18n.ts.emailNotification,
-				keywords: ['notification', 'email'],
-			},
-		],
-		label: i18n.ts.email,
-		keywords: ['email'],
-		path: '/settings/email',
-		icon: 'ti ti-mail',
-	},
-	{
-		id: 'tnYoppRiv',
-		children: [
-			{
-				id: 'ncIq6TAR2',
-				label: i18n.ts.usageAmount,
-				keywords: ['capacity', 'usage'],
-			},
-			{
-				id: '2c4CQSvSr',
-				label: i18n.ts.statistics,
-				keywords: ['statistics', 'usage'],
-			},
-			{
-				id: 'pepHELHMt',
-				label: i18n.ts.uploadFolder,
-				keywords: ['default', 'upload', 'folder'],
-			},
-			{
-				id: 'xqOWrABxV',
-				label: i18n.ts.keepOriginalUploading,
-				keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription],
-			},
-			{
-				id: 'D8HUTGWE1',
-				label: i18n.ts.keepOriginalFilename,
-				keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription],
-			},
-			{
-				id: '6xAvsWSZi',
-				label: i18n.ts.alwaysMarkSensitive,
-				keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'],
-			},
-			{
-				id: 'csNNPF1KX',
-				label: i18n.ts.enableAutoSensitive,
-				keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription],
-			},
-		],
-		label: i18n.ts.drive,
-		keywords: ['drive'],
-		path: '/settings/drive',
-		icon: 'ti ti-cloud',
-	},
-	{
-		id: 'gtaOSdIJB',
-		label: i18n.ts.avatarDecorations,
-		keywords: ['avatar', 'icon', 'decoration'],
-		path: '/settings/avatar-decoration',
-		icon: 'ti ti-sparkles',
-	},
-	{
-		id: 'AqPvMgn3A',
-		children: [
-			{
-				id: 'j5gTtuMWP',
-				label: i18n.ts.useBlurEffect,
-				keywords: ['blur'],
-			},
-			{
-				id: 'C05WQNSIJ',
-				label: i18n.ts.useBlurEffectForModal,
-				keywords: ['blur', 'modal'],
-			},
-			{
-				id: 'snVKNr7Bw',
-				label: i18n.ts.highlightSensitiveMedia,
-				keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
-			},
-			{
-				id: 'DsS2CwjYE',
-				label: i18n.ts.squareAvatars,
-				keywords: ['avatar', 'icon', 'square'],
-			},
-			{
-				id: 'xCcTDl651',
-				label: i18n.ts.showAvatarDecorations,
-				keywords: ['avatar', 'icon', 'decoration', 'show'],
-			},
-			{
-				id: '3dHw723VD',
-				label: i18n.ts.showGapBetweenNotesInTimeline,
-				keywords: ['note', 'timeline', 'gap'],
-			},
-			{
-				id: 'AWi72xbrl',
-				label: i18n.ts.seasonalScreenEffect,
-				keywords: ['effect', 'show'],
-			},
-			{
-				id: 'Ces8FsJws',
-				label: i18n.ts.menuStyle,
-				keywords: ['menu', 'style', 'popup', 'drawer'],
-			},
-			{
-				id: 'wDr9xSXCv',
-				label: i18n.ts.emojiStyle,
-				keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
-			},
-			{
-				id: 'vFB0pLzck',
-				label: i18n.ts.fontSize,
-				keywords: ['font', 'size'],
-			},
-			{
-				id: '23BhvYXPC',
-				label: i18n.ts.useSystemFont,
-				keywords: ['font', 'system', 'native'],
-			},
-			{
-				id: 'EeNLndAOa',
-				children: [
-					{
-						id: 'rAAPoaodS',
-						label: i18n.ts.reactionsDisplaySize,
-						keywords: ['reaction', 'size', 'scale', 'display'],
-					},
-					{
-						id: 'qTLAvNWsc',
-						label: i18n.ts.limitWidthOfReaction,
-						keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
-					},
-					{
-						id: '2lWgzAm13',
-						label: i18n.ts.mediaListWithOneImageAppearance,
-						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
-					},
-					{
-						id: 'EU7HbxOR5',
-						label: i18n.ts.instanceTicker,
-						keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
-					},
-					{
-						id: 'AEtM0FAp1',
-						label: i18n.ts.displayOfSensitiveMedia,
-						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
-					},
-				],
-				label: i18n.ts.displayOfNote,
-				keywords: ['note', 'display'],
-			},
-			{
-				id: 'A1FMC2Zon',
-				children: [
-					{
-						id: 'CB37G5ZDo',
-						label: i18n.ts.position,
-						keywords: ['position'],
-					},
-					{
-						id: 'gGS2i19hS',
-						label: i18n.ts.stackAxis,
-						keywords: ['stack', 'axis', 'direction'],
-					},
-				],
-				label: i18n.ts.notificationDisplay,
-				keywords: ['notification', 'display'],
-			},
-		],
-		label: i18n.ts.appearance,
-		keywords: ['appearance'],
-		path: '/settings/appearance',
-		icon: 'ti ti-device-desktop',
-	},
-	{
-		id: 'f08Mi1Uwn',
-		children: [
-			{
-				id: '7ov7ceoij',
-				label: i18n.ts.reduceUiAnimation,
-				keywords: ['animation', 'motion', 'reduce'],
-			},
-			{
-				id: 'cXr3tFdpa',
-				label: i18n.ts.disableShowingAnimatedImages,
-				keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'],
-			},
-			{
-				id: 'Ok1UBwtP',
-				label: i18n.ts.enableAnimatedMfm,
-				keywords: ['mfm', 'enable', 'show', 'animated'],
-			},
-			{
-				id: 'yPEpJigqY',
-				label: i18n.ts.enableHorizontalSwipe,
-				keywords: ['swipe', 'horizontal', 'tab'],
-			},
-			{
-				id: 'h7iZtdTU3',
-				label: i18n.ts.keepScreenOn,
-				keywords: ['keep', 'screen', 'display', 'on'],
-			},
-			{
-				id: 'gP1BY3PDy',
-				label: i18n.ts.useNativeUIForVideoAudioPlayer,
-				keywords: ['native', 'system', 'video', 'audio', 'player', 'media'],
-			},
-			{
-				id: 'jnMK3M6rs',
-				label: i18n.ts._contextMenu.title,
-				keywords: ['contextmenu', 'system', 'native'],
-			},
-		],
-		label: i18n.ts.accessibility,
-		keywords: ['accessibility'],
-		path: '/settings/accessibility',
-		icon: 'ti ti-accessible',
-	},
-] as const;
-
-export type SearchIndex = typeof searchIndexes;
diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts
deleted file mode 100644
index 0fbdf34d5d..0000000000
--- a/packages/frontend/src/scripts/cache.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { ref } from 'vue';
-
-export class Cache<T> {
-	private cachedAt: number | null = null;
-	public value = ref<T | undefined>();
-	private lifetime: number;
-	private fetcher: () => Promise<T>;
-
-	constructor(lifetime: Cache<never>['lifetime'], fetcher: () => Promise<T>) {
-		this.lifetime = lifetime;
-		this.fetcher = fetcher;
-	}
-
-	public set(value: T): void {
-		this.cachedAt = Date.now();
-		this.value.value = value;
-	}
-
-	private get(): T | undefined {
-		if (this.cachedAt == null) return undefined;
-		if ((Date.now() - this.cachedAt) > this.lifetime) {
-			this.value.value = undefined;
-			this.cachedAt = null;
-			return undefined;
-		}
-		return this.value.value;
-	}
-
-	public delete() {
-		this.cachedAt = null;
-	}
-
-	/**
-	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
-	 */
-	public async fetch(): Promise<T> {
-		const cachedValue = this.get();
-		if (cachedValue !== undefined) {
-			// Cache HIT
-			return cachedValue;
-		}
-
-		// Cache MISS
-		const value = await this.fetcher();
-		this.set(value);
-		return value;
-	}
-}
diff --git a/packages/frontend/src/scripts/chart-legend.ts b/packages/frontend/src/scripts/chart-legend.ts
deleted file mode 100644
index e701d18dd2..0000000000
--- a/packages/frontend/src/scripts/chart-legend.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Plugin } from 'chart.js';
-import MkChartLegend from '@/components/MkChartLegend.vue';
-
-export const chartLegend = (legend: InstanceType<typeof MkChartLegend>) => ({
-	id: 'htmlLegend',
-	afterUpdate(chart, args, options) {
-		// Reuse the built-in legendItems generator
-		const items = chart.options.plugins.legend.labels.generateLabels(chart);
-
-		legend.update(chart, items);
-	},
-}) as Plugin;
diff --git a/packages/frontend/src/scripts/chart-vline.ts b/packages/frontend/src/scripts/chart-vline.ts
deleted file mode 100644
index 465ca591c6..0000000000
--- a/packages/frontend/src/scripts/chart-vline.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Plugin } from 'chart.js';
-
-export const chartVLine = (vLineColor: string) => ({
-	id: 'vLine',
-	beforeDraw(chart, args, options) {
-		if (chart.tooltip?._active?.length) {
-			const ctx = chart.ctx;
-			const xs = chart.tooltip._active.map(a => a.element.x);
-			const x = xs.reduce((a, b) => a + b, 0) / xs.length;
-			const topY = chart.scales.y.top;
-			const bottomY = chart.scales.y.bottom;
-
-			ctx.save();
-			ctx.beginPath();
-			ctx.moveTo(x, bottomY);
-			ctx.lineTo(x, topY);
-			ctx.lineWidth = 1;
-			ctx.strokeStyle = vLineColor;
-			ctx.stroke();
-			ctx.restore();
-		}
-	},
-}) as Plugin;
diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/scripts/check-permissions.ts
deleted file mode 100644
index ed86529d5b..0000000000
--- a/packages/frontend/src/scripts/check-permissions.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { instance } from '@/instance.js';
-import { $i } from '@/account.js';
-
-export const notesSearchAvailable = (
-	// FIXME: instance.policies would be null in Vitest
-	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	($i == null && instance.policies != null && instance.policies.canSearchNotes) ||
-	($i != null && $i.policies.canSearchNotes) ||
-	false
-) as boolean;
-
-export const canSearchNonLocalNotes = (
-	instance.noteSearchableScope === 'global'
-);
diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts
deleted file mode 100644
index 281ea2520e..0000000000
--- a/packages/frontend/src/scripts/check-reaction-permissions.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-import type { UnicodeEmojiDef } from '@@/js/emojilist.js';
-
-export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean {
-	if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする;
-	if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能
-
-	const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
-	return !(emoji.localOnly && note.user.host !== me.host)
-      && !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
-      && (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
-}
diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts
deleted file mode 100644
index 98fea1bced..0000000000
--- a/packages/frontend/src/scripts/check-word-mute.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-import * as Misskey from 'misskey-js';
-
-export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
-	// 自分自身
-	if (me && (note.userId === me.id)) return false;
-
-	if (mutedWords.length > 0) {
-		const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
-
-		if (text === '') return false;
-
-		const matched = mutedWords.filter(filter => {
-			if (Array.isArray(filter)) {
-				// Clean up
-				const filteredFilter = filter.filter(keyword => keyword !== '');
-				if (filteredFilter.length === 0) return false;
-
-				return filteredFilter.every(keyword => text.includes(keyword));
-			} else {
-				// represents RegExp
-				const regexp = filter.match(/^\/(.+)\/(.*)$/);
-
-				// This should never happen due to input sanitisation.
-				if (!regexp) return false;
-
-				try {
-					return new RegExp(regexp[1], regexp[2]).test(text);
-				} catch (err) {
-					// This should never happen due to input sanitisation.
-					return false;
-				}
-			}
-		});
-
-		if (matched.length > 0) return matched;
-	}
-
-	return false;
-}
diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts
deleted file mode 100644
index 71d1232710..0000000000
--- a/packages/frontend/src/scripts/clear-cache.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { unisonReload } from '@/scripts/unison-reload.js';
-import * as os from '@/os.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { fetchCustomEmojis } from '@/custom-emojis.js';
-import { fetchInstance } from '@/instance.js';
-
-export async function clearCache() {
-	os.waiting();
-	miLocalStorage.removeItem('instance');
-	miLocalStorage.removeItem('instanceCachedAt');
-	miLocalStorage.removeItem('locale');
-	miLocalStorage.removeItem('localeVersion');
-	miLocalStorage.removeItem('theme');
-	miLocalStorage.removeItem('emojis');
-	miLocalStorage.removeItem('lastEmojisFetchedAt');
-	await fetchInstance(true);
-	await fetchCustomEmojis(true);
-	unisonReload();
-}
diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts
deleted file mode 100644
index f9c4bc1829..0000000000
--- a/packages/frontend/src/scripts/clicker-game.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { ref, computed } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-
-type SaveData = {
-	gameVersion: number;
-	cookies: number;
-	totalCookies: number;
-	totalHandmadeCookies: number;
-	clicked: number;
-	achievements: any[];
-	facilities: any[];
-};
-
-export const saveData = ref<SaveData>();
-export const ready = computed(() => saveData.value != null);
-
-let prev = '';
-
-export async function load() {
-	try {
-		saveData.value = await misskeyApi('i/registry/get', {
-			scope: ['clickerGame'],
-			key: 'saveData',
-		});
-	} catch (err) {
-		if (err.code === 'NO_SUCH_KEY') {
-			saveData.value = {
-				gameVersion: 2,
-				cookies: 0,
-				totalCookies: 0,
-				totalHandmadeCookies: 0,
-				clicked: 0,
-				achievements: [],
-				facilities: [],
-			};
-			save();
-			return;
-		}
-		throw err;
-	}
-
-	// migration
-	if (saveData.value.gameVersion === 1) {
-		saveData.value = {
-			gameVersion: 2,
-			cookies: saveData.value.cookies,
-			totalCookies: saveData.value.cookies,
-			totalHandmadeCookies: saveData.value.cookies,
-			clicked: saveData.value.clicked,
-			achievements: [],
-			facilities: [],
-		};
-		save();
-	}
-}
-
-export async function save() {
-	const current = JSON.stringify(saveData.value);
-	if (current === prev) return;
-
-	await misskeyApi('i/registry/set', {
-		scope: ['clickerGame'],
-		key: 'saveData',
-		value: saveData.value,
-	});
-
-	prev = current;
-}
diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts
deleted file mode 100644
index ea8eea14b5..0000000000
--- a/packages/frontend/src/scripts/clone.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-// structredCloneが遅いため
-// SEE: http://var.blog.jp/archives/86038606.html
-// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
-// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
-
-export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
-
-export function deepClone<T extends Cloneable>(x: T): T {
-	if (typeof x === 'object') {
-		if (x === null) return x;
-		if (Array.isArray(x)) return x.map(deepClone) as T;
-		const obj = {} as Record<string | number | symbol, Cloneable>;
-		for (const [k, v] of Object.entries(x)) {
-			obj[k] = v === undefined ? undefined : deepClone(v);
-		}
-		return obj as T;
-	} else {
-		return x;
-	}
-}
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
deleted file mode 100644
index 4f2aff9d4c..0000000000
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { createHighlighterCore } from 'shiki/core';
-import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
-import darkPlus from 'shiki/themes/dark-plus.mjs';
-import { bundledThemesInfo } from 'shiki/themes';
-import { bundledLanguagesInfo } from 'shiki/langs';
-import lightTheme from '@@/themes/_light.json5';
-import darkTheme from '@@/themes/_dark.json5';
-import defaultLightTheme from '@@/themes/l-light.json5';
-import defaultDarkTheme from '@@/themes/d-green-lime.json5';
-import { unique } from './array.js';
-import { deepClone } from './clone.js';
-import { deepMerge } from './merge.js';
-import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core';
-import { prefer } from '@/preferences.js';
-
-let _highlighter: HighlighterCore | null = null;
-
-export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
-export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
-export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
-	const theme = deepClone(mode === 'light' ? prefer.s.lightTheme ?? defaultLightTheme : prefer.s.darkTheme ?? defaultDarkTheme);
-
-	if (theme.base) {
-		const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
-		if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
-	}
-
-	if (theme.codeHighlighter) {
-		let _res: ThemeRegistration = {};
-		if (theme.codeHighlighter.base === '_none_') {
-			_res = deepClone(theme.codeHighlighter.overrides);
-		} else {
-			const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
-			_res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
-		}
-		if (_res.name == null) {
-			_res.name = theme.id;
-		}
-		_res.type = mode;
-
-		if (getName) {
-			return _res.name;
-		}
-		return _res;
-	}
-
-	if (getName) {
-		return 'dark-plus';
-	}
-	return darkPlus;
-}
-
-export async function getHighlighter(): Promise<HighlighterCore> {
-	if (!_highlighter) {
-		return await initHighlighter();
-	}
-	return _highlighter;
-}
-
-async function initHighlighter() {
-	// テーマの重複を消す
-	const themes = unique([
-		darkPlus,
-		...(await Promise.all([getTheme('light'), getTheme('dark')])),
-	]);
-
-	const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript');
-	const highlighter = await createHighlighterCore({
-		engine: createOnigurumaEngine(() => import('shiki/onig.wasm?init')),
-		themes,
-		langs: [
-			...(jsLangInfo ? [async () => await jsLangInfo.import()] : []),
-			async () => (await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json')).default as unknown as LanguageRegistration,
-		],
-	});
-
-	// TODO
-	//watch('lightTheme', async () => {
-	//	const newTheme = await getTheme('light');
-	//	if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
-	//		highlighter.loadTheme(newTheme);
-	//	}
-	//});
-	//watch('darkTheme', async () => {
-	//	const newTheme = await getTheme('dark');
-	//	if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
-	//		highlighter.loadTheme(newTheme);
-	//	}
-	//});
-
-	_highlighter = highlighter;
-
-	return highlighter;
-}
diff --git a/packages/frontend/src/scripts/collect-page-vars.ts b/packages/frontend/src/scripts/collect-page-vars.ts
deleted file mode 100644
index 5096c0669e..0000000000
--- a/packages/frontend/src/scripts/collect-page-vars.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-interface StringPageVar {
-	name: string,
-	type: 'string',
-	value: string
-}
-
-interface NumberPageVar {
-	name: string,
-	type: 'number',
-	value: number
-}
-
-interface BooleanPageVar {
-	name: string,
-	type: 'boolean',
-	value: boolean
-}
-
-type PageVar = StringPageVar | NumberPageVar | BooleanPageVar;
-
-export function collectPageVars(content): PageVar[] {
-	const pageVars: PageVar[] = [];
-	const collect = (xs: any[]): void => {
-		for (const x of xs) {
-			if (x.type === 'textInput') {
-				pageVars.push({
-					name: x.name,
-					type: 'string',
-					value: x.default || '',
-				});
-			} else if (x.type === 'textareaInput') {
-				pageVars.push({
-					name: x.name,
-					type: 'string',
-					value: x.default || '',
-				});
-			} else if (x.type === 'numberInput') {
-				pageVars.push({
-					name: x.name,
-					type: 'number',
-					value: x.default || 0,
-				});
-			} else if (x.type === 'switch') {
-				pageVars.push({
-					name: x.name,
-					type: 'boolean',
-					value: x.default || false,
-				});
-			} else if (x.type === 'counter') {
-				pageVars.push({
-					name: x.name,
-					type: 'number',
-					value: 0,
-				});
-			} else if (x.type === 'radioButton') {
-				pageVars.push({
-					name: x.name,
-					type: 'string',
-					value: x.default || '',
-				});
-			} else if (x.children) {
-				collect(x.children);
-			}
-		}
-	};
-	collect(content);
-	return pageVars;
-}
diff --git a/packages/frontend/src/scripts/color.ts b/packages/frontend/src/scripts/color.ts
deleted file mode 100644
index a11255ffd1..0000000000
--- a/packages/frontend/src/scripts/color.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export const alpha = (hex: string, a: number): string => {
-	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
-	return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
diff --git a/packages/frontend/src/scripts/confetti.ts b/packages/frontend/src/scripts/confetti.ts
deleted file mode 100644
index 8e53a6ceeb..0000000000
--- a/packages/frontend/src/scripts/confetti.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import _confetti from 'canvas-confetti';
-import * as os from '@/os.js';
-
-export function confetti(options: { duration?: number; } = {}) {
-	const duration = options.duration ?? 1000 * 4;
-	const animationEnd = Date.now() + duration;
-	const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: os.claimZIndex('high') };
-
-	function randomInRange(min, max) {
-		return Math.random() * (max - min) + min;
-	}
-
-	const interval = setInterval(() => {
-		const timeLeft = animationEnd - Date.now();
-
-		if (timeLeft <= 0) {
-			return clearInterval(interval);
-		}
-
-		const particleCount = 50 * (timeLeft / duration);
-		// since particles fall down, start a bit higher than random
-		_confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } }));
-		_confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } }));
-	}, 250);
-}
diff --git a/packages/frontend/src/scripts/contains.ts b/packages/frontend/src/scripts/contains.ts
deleted file mode 100644
index 6137c06e85..0000000000
--- a/packages/frontend/src/scripts/contains.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export default (parent, child, checkSame = true) => {
-	if (checkSame && parent === child) return true;
-	let node = child.parentNode;
-	while (node) {
-		if (node === parent) return true;
-		node = node.parentNode;
-	}
-	return false;
-};
diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts
deleted file mode 100644
index 7e0bb25606..0000000000
--- a/packages/frontend/src/scripts/copy-to-clipboard.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-/**
- * Clipboardに値をコピー(TODO: 文字列以外も対応)
- */
-export function copyToClipboard(input: string | null) {
-	if (input) navigator.clipboard.writeText(input);
-};
diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/scripts/device-kind.ts
deleted file mode 100644
index 7aadb617ca..0000000000
--- a/packages/frontend/src/scripts/device-kind.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
-
-const ua = navigator.userAgent.toLowerCase();
-const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
-const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
-
-export const DEFAULT_DEVICE_KIND: DeviceKind = (
-	isSmartphone
-		? 'smartphone'
-		: isTablet
-			? 'tablet'
-			: 'desktop'
-);
-
-export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
-
-export function updateDeviceKind(kind: DeviceKind | null) {
-	deviceKind = kind ?? DEFAULT_DEVICE_KIND;
-}
diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts
deleted file mode 100644
index 7ff5863243..0000000000
--- a/packages/frontend/src/scripts/emoji-picker.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { defineAsyncComponent, ref } from 'vue';
-import type { Ref } from 'vue';
-import { popup } from '@/os.js';
-import { store } from '@/store.js';
-
-/**
- * 絵文字ピッカーを表示する。
- * 類似の機能として{@link ReactionPicker}が存在しているが、この機能とは動きが異なる。
- * 投稿フォームなどで絵文字を選択する時など、絵文字ピックアップ後でもダイアログが消えずに残り、
- * 一度表示したダイアログを連続で使用できることが望ましいシーンでの利用が想定される。
- */
-class EmojiPicker {
-	private src: Ref<HTMLElement | null> = ref(null);
-	private manualShowing = ref(false);
-	private onChosen?: (emoji: string) => void;
-	private onClosed?: () => void;
-
-	constructor() {
-		// nop
-	}
-
-	public async init() {
-		const emojisRef = store.reactiveState.pinnedEmojis;
-		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
-			src: this.src,
-			pinnedEmojis: emojisRef,
-			asReactionPicker: false,
-			manualShowing: this.manualShowing,
-			choseAndClose: false,
-		}, {
-			done: emoji => {
-				if (this.onChosen) this.onChosen(emoji);
-			},
-			close: () => {
-				this.manualShowing.value = false;
-			},
-			closed: () => {
-				this.src.value = null;
-				if (this.onClosed) this.onClosed();
-			},
-		});
-	}
-
-	public show(
-		src: HTMLElement,
-		onChosen?: EmojiPicker['onChosen'],
-		onClosed?: EmojiPicker['onClosed'],
-	) {
-		this.src.value = src;
-		this.manualShowing.value = true;
-		this.onChosen = onChosen;
-		this.onClosed = onClosed;
-	}
-}
-
-export const emojiPicker = new EmojiPicker();
diff --git a/packages/frontend/src/scripts/extract-mentions.ts b/packages/frontend/src/scripts/extract-mentions.ts
deleted file mode 100644
index d518562053..0000000000
--- a/packages/frontend/src/scripts/extract-mentions.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-// test is located in test/extract-mentions
-
-import * as mfm from 'mfm-js';
-
-export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] {
-	// TODO: 重複を削除
-	const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention');
-	const mentions = mentionNodes.map(x => x.props);
-
-	return mentions;
-}
diff --git a/packages/frontend/src/scripts/extract-url-from-mfm.ts b/packages/frontend/src/scripts/extract-url-from-mfm.ts
deleted file mode 100644
index d5654ba850..0000000000
--- a/packages/frontend/src/scripts/extract-url-from-mfm.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as mfm from 'mfm-js';
-import { unique } from '@/scripts/array.js';
-
-// unique without hash
-// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
-const removeHash = (x: string) => x.replace(/#[^#]*$/, '');
-
-export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] {
-	const urlNodes = mfm.extract(nodes, (node) => {
-		return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent));
-	});
-	const urls: string[] = unique(urlNodes.map(x => x.props.url));
-
-	return urls.reduce((array, url) => {
-		const urlWithoutHash = removeHash(url);
-		if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url);
-		return array;
-	}, [] as string[]);
-}
diff --git a/packages/frontend/src/scripts/file-drop.ts b/packages/frontend/src/scripts/file-drop.ts
deleted file mode 100644
index 4259fe25e9..0000000000
--- a/packages/frontend/src/scripts/file-drop.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export type DroppedItem = DroppedFile | DroppedDirectory;
-
-export type DroppedFile = {
-	isFile: true;
-	path: string;
-	file: File;
-};
-
-export type DroppedDirectory = {
-	isFile: false;
-	path: string;
-	children: DroppedItem[];
-};
-
-export async function extractDroppedItems(ev: DragEvent): Promise<DroppedItem[]> {
-	const dropItems = ev.dataTransfer?.items;
-	if (!dropItems || dropItems.length === 0) {
-		return [];
-	}
-
-	const apiTestItem = dropItems[0];
-	if ('webkitGetAsEntry' in apiTestItem) {
-		return readDataTransferItems(dropItems);
-	} else {
-		// webkitGetAsEntryに対応していない場合はfilesから取得する（ディレクトリのサポートは出来ない）
-		const dropFiles = ev.dataTransfer.files;
-		if (dropFiles.length === 0) {
-			return [];
-		}
-
-		const droppedFiles = Array.of<DroppedFile>();
-		for (let i = 0; i < dropFiles.length; i++) {
-			const file = dropFiles.item(i);
-			if (file) {
-				droppedFiles.push({
-					isFile: true,
-					path: file.name,
-					file,
-				});
-			}
-		}
-
-		return droppedFiles;
-	}
-}
-
-/**
- * ドラッグ＆ドロップされたファイルのリストからディレクトリ構造とファイルへの参照（{@link File}）を取得する。
- */
-export async function readDataTransferItems(itemList: DataTransferItemList): Promise<DroppedItem[]> {
-	async function readEntry(entry: FileSystemEntry): Promise<DroppedItem> {
-		if (entry.isFile) {
-			return {
-				isFile: true,
-				path: entry.fullPath,
-				file: await readFile(entry as FileSystemFileEntry),
-			};
-		} else {
-			return {
-				isFile: false,
-				path: entry.fullPath,
-				children: await readDirectory(entry as FileSystemDirectoryEntry),
-			};
-		}
-	}
-
-	function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise<File> {
-		return new Promise((resolve, reject) => {
-			fileSystemFileEntry.file(resolve, reject);
-		});
-	}
-
-	function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
-		return new Promise(async (resolve) => {
-			const allEntries = Array.of<FileSystemEntry>();
-			const reader = fileSystemDirectoryEntry.createReader();
-			while (true) {
-				const entries = await new Promise<FileSystemEntry[]>((res, rej) => reader.readEntries(res, rej));
-				if (entries.length === 0) {
-					break;
-				}
-				allEntries.push(...entries);
-			}
-
-			resolve(await Promise.all(allEntries.map(readEntry)));
-		});
-	}
-
-	// 扱いにくいので配列に変換
-	const items = Array.of<DataTransferItem>();
-	for (let i = 0; i < itemList.length; i++) {
-		items.push(itemList[i]);
-	}
-
-	return Promise.all(
-		items
-			.map(it => it.webkitGetAsEntry())
-			.filter(it => it)
-			.map(it => readEntry(it!)),
-	);
-}
-
-/**
- * {@link DroppedItem}のリストからディレクトリを再帰的に検索し、ファイルのリストを取得する。
- */
-export function flattenDroppedFiles(items: DroppedItem[]): DroppedFile[] {
-	const result = Array.of<DroppedFile>();
-	for (const item of items) {
-		if (item.isFile) {
-			result.push(item);
-		} else {
-			result.push(...flattenDroppedFiles(item.children));
-		}
-	}
-	return result;
-}
diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts
deleted file mode 100644
index fb7caea830..0000000000
--- a/packages/frontend/src/scripts/focus-trap.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
-
-const focusTrapElements = new Set<HTMLElement>();
-const ignoreElements = [
-	'script',
-	'style',
-];
-
-function containsFocusTrappedElements(el: HTMLElement): boolean {
-	return Array.from(focusTrapElements).some((focusTrapElement) => {
-		return el.contains(focusTrapElement);
-	});
-}
-
-function getZIndex(el: HTMLElement): number {
-	const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10);
-	if (isNaN(zIndex)) {
-		return 0;
-	}
-	return zIndex;
-}
-
-function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null {
-	let highestZIndexElement: HTMLElement | null = null;
-	let highestZIndex = -Infinity;
-
-	focusTrapElements.forEach((el) => {
-		const zIndex = getZIndex(el);
-		if (zIndex > highestZIndex) {
-			highestZIndex = zIndex;
-			highestZIndexElement = el;
-		}
-	});
-
-	return highestZIndexElement == null ? null : {
-		el: highestZIndexElement,
-		zIndex: highestZIndex,
-	};
-}
-
-function releaseFocusTrap(el: HTMLElement): void {
-	focusTrapElements.delete(el);
-	if (el.inert === true) {
-		el.inert = false;
-	}
-
-	const highestZIndexElement = getHighestZIndexElement();
-
-	if (el.parentElement != null && el !== document.body) {
-		el.parentElement.childNodes.forEach((siblingNode) => {
-			const siblingEl = getHTMLElementOrNull(siblingNode);
-			if (!siblingEl) return;
-			if (
-				siblingEl !== el &&
-				(
-					highestZIndexElement == null ||
-					siblingEl === highestZIndexElement.el ||
-					siblingEl.contains(highestZIndexElement.el)
-				)
-			) {
-				siblingEl.inert = false;
-			} else if (
-				highestZIndexElement != null &&
-				siblingEl !== highestZIndexElement.el &&
-				!siblingEl.contains(highestZIndexElement.el) &&
-				!ignoreElements.includes(siblingEl.tagName.toLowerCase())
-			) {
-				siblingEl.inert = true;
-			} else {
-				siblingEl.inert = false;
-			}
-		});
-		releaseFocusTrap(el.parentElement);
-	}
-}
-
-export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void;
-export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; };
-export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void {
-	const highestZIndexElement = getHighestZIndexElement();
-
-	const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex;
-	const zIndex = getZIndex(el);
-
-	// If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead
-	// Focus trapping for this element will be done in the release function
-	if (!parent && zIndex < highestZIndex) {
-		focusTrapElements.add(el);
-		if (highestZIndexElement) {
-			focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls);
-		}
-		return {
-			release: () => {
-				releaseFocusTrap(el);
-			},
-		};
-	}
-
-	if (el.inert === true) {
-		el.inert = false;
-	}
-
-	if (el.parentElement != null && el !== document.body) {
-		el.parentElement.childNodes.forEach((siblingNode) => {
-			const siblingEl = getHTMLElementOrNull(siblingNode);
-			if (!siblingEl) return;
-			if (
-				siblingEl !== el &&
-				(
-					hasInteractionWithOtherFocusTrappedEls === false ||
-					(!focusTrapElements.has(siblingEl) && !containsFocusTrappedElements(siblingEl))
-				) &&
-				!ignoreElements.includes(siblingEl.tagName.toLowerCase())
-			) {
-				siblingEl.inert = true;
-			}
-		});
-		focusTrap(el.parentElement, hasInteractionWithOtherFocusTrappedEls, true);
-	}
-
-	if (!parent) {
-		focusTrapElements.add(el);
-
-		return {
-			release: () => {
-				releaseFocusTrap(el);
-			},
-		};
-	}
-}
diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts
deleted file mode 100644
index 81278b17ea..0000000000
--- a/packages/frontend/src/scripts/focus.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js';
-import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
-
-type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement;
-
-export const isFocusable = (input: MaybeHTMLElement | null | undefined): input is HTMLElement => {
-	if (input == null || !(input instanceof HTMLElement)) return false;
-
-	if (input.tabIndex < 0) return false;
-	if ('disabled' in input && input.disabled === true) return false;
-	if ('readonly' in input && input.readonly === true) return false;
-
-	if (!input.ownerDocument.contains(input)) return false;
-
-	const style = window.getComputedStyle(input);
-	if (style.display === 'none') return false;
-	if (style.visibility === 'hidden') return false;
-	if (style.opacity === '0') return false;
-	if (style.pointerEvents === 'none') return false;
-
-	return true;
-};
-
-export const focusPrev = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
-	const element = self ? input : getElementOrNull(input)?.previousElementSibling;
-	if (element == null) return;
-	if (isFocusable(element)) {
-		focusOrScroll(element, scroll);
-	} else {
-		focusPrev(element, false, scroll);
-	}
-};
-
-export const focusNext = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
-	const element = self ? input : getElementOrNull(input)?.nextElementSibling;
-	if (element == null) return;
-	if (isFocusable(element)) {
-		focusOrScroll(element, scroll);
-	} else {
-		focusNext(element, false, scroll);
-	}
-};
-
-export const focusParent = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
-	const element = self ? input : getNodeOrNull(input)?.parentElement;
-	if (element == null) return;
-	if (isFocusable(element)) {
-		focusOrScroll(element, scroll);
-	} else {
-		focusParent(element, false, scroll);
-	}
-};
-
-const focusOrScroll = (element: HTMLElement, scroll: boolean) => {
-	if (scroll) {
-		const scrollContainer = getScrollContainer(element) ?? document.documentElement;
-		const scrollContainerTop = getScrollPosition(scrollContainer);
-		const stickyTop = getStickyTop(element, scrollContainer);
-		const stickyBottom = getStickyBottom(element, scrollContainer);
-		const top = element.getBoundingClientRect().top;
-		const bottom = element.getBoundingClientRect().bottom;
-
-		let scrollTo = scrollContainerTop;
-		if (top < stickyTop) {
-			scrollTo += top - stickyTop;
-		} else if (bottom > window.innerHeight - stickyBottom) {
-			scrollTo += bottom - window.innerHeight + stickyBottom;
-		}
-		scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' });
-	}
-
-	if (document.activeElement !== element) {
-		element.focus({ preventScroll: true });
-	}
-};
diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts
deleted file mode 100644
index 1032e97ac9..0000000000
--- a/packages/frontend/src/scripts/form.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-
-type EnumItem = string | {
-	label: string;
-	value: string;
-};
-
-type Hidden = boolean | ((v: any) => boolean);
-
-export type FormItem = {
-	label?: string;
-	type: 'string';
-	default?: string | null;
-	description?: string;
-	required?: boolean;
-	hidden?: Hidden;
-	multiline?: boolean;
-	treatAsMfm?: boolean;
-} | {
-	label?: string;
-	type: 'number';
-	default?: number | null;
-	description?: string;
-	required?: boolean;
-	hidden?: Hidden;
-	step?: number;
-} | {
-	label?: string;
-	type: 'boolean';
-	default?: boolean | null;
-	description?: string;
-	hidden?: Hidden;
-} | {
-	label?: string;
-	type: 'enum';
-	default?: string | null;
-	required?: boolean;
-	hidden?: Hidden;
-	enum: EnumItem[];
-} | {
-	label?: string;
-	type: 'radio';
-	default?: unknown | null;
-	required?: boolean;
-	hidden?: Hidden;
-	options: {
-		label: string;
-		value: unknown;
-	}[];
-} | {
-	label?: string;
-	type: 'range';
-	default?: number | null;
-	description?: string;
-	required?: boolean;
-	step?: number;
-	min: number;
-	max: number;
-	textConverter?: (value: number) => string;
-	hidden?: Hidden;
-} | {
-	label?: string;
-	type: 'object';
-	default?: Record<string, unknown> | null;
-	hidden: Hidden;
-} | {
-	label?: string;
-	type: 'array';
-	default?: unknown[] | null;
-	hidden: Hidden;
-} | {
-	type: 'button';
-	content?: string;
-	hidden?: Hidden;
-	action: (ev: MouseEvent, v: any) => void;
-} | {
-	type: 'drive-file';
-	defaultFileId?: string | null;
-	hidden?: Hidden;
-	validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>;
-};
-
-export type Form = Record<string, FormItem>;
-
-type GetItemType<Item extends FormItem> =
-	Item['type'] extends 'string' ? string :
-	Item['type'] extends 'number' ? number :
-	Item['type'] extends 'boolean' ? boolean :
-	Item['type'] extends 'radio' ? unknown :
-	Item['type'] extends 'range' ? number :
-	Item['type'] extends 'enum' ? string :
-	Item['type'] extends 'array' ? unknown[] :
-	Item['type'] extends 'object' ? Record<string, unknown> :
-	Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined :
-	never;
-
-export type GetFormResultType<F extends Form> = {
-	[P in keyof F]: GetItemType<F[P]>;
-};
diff --git a/packages/frontend/src/scripts/format-time-string.ts b/packages/frontend/src/scripts/format-time-string.ts
deleted file mode 100644
index d383f143e1..0000000000
--- a/packages/frontend/src/scripts/format-time-string.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-const defaultLocaleStringFormats: { [index: string]: string } = {
-	'weekday': 'narrow',
-	'era': 'narrow',
-	'year': 'numeric',
-	'month': 'numeric',
-	'day': 'numeric',
-	'hour': 'numeric',
-	'minute': 'numeric',
-	'second': 'numeric',
-	'timeZoneName': 'short',
-};
-
-function formatLocaleString(date: Date, format: string): string {
-	return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => {
-		if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) {
-			return date.toLocaleString(window.navigator.language, { [kind]: option ? option : defaultLocaleStringFormats[kind] });
-		} else {
-			return match;
-		}
-	});
-}
-
-export function formatDateTimeString(date: Date, format: string): string {
-	return format
-		.replace(/yyyy/g, date.getFullYear().toString())
-		.replace(/yy/g, date.getFullYear().toString().slice(-2))
-		.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long' }))
-		.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short' }))
-		.replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2))
-		.replace(/M/g, (date.getMonth() + 1).toString())
-		.replace(/dd/g, (`0${date.getDate()}`).slice(-2))
-		.replace(/d/g, date.getDate().toString())
-		.replace(/HH/g, (`0${date.getHours()}`).slice(-2))
-		.replace(/H/g, date.getHours().toString())
-		.replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2))
-		.replace(/h/g, ((date.getHours() % 12) || 12).toString())
-		.replace(/mm/g, (`0${date.getMinutes()}`).slice(-2))
-		.replace(/m/g, date.getMinutes().toString())
-		.replace(/ss/g, (`0${date.getSeconds()}`).slice(-2))
-		.replace(/s/g, date.getSeconds().toString())
-		.replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM');
-}
-
-export function formatTimeString(date: Date, format: string): string {
-	return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
-		if (localeformat) return formatLocaleString(date, localeformat);
-		if (datetimeformat) return formatDateTimeString(date, datetimeformat);
-		return match;
-	});
-}
diff --git a/packages/frontend/src/scripts/fullscreen.ts b/packages/frontend/src/scripts/fullscreen.ts
deleted file mode 100644
index 7a0a018ef3..0000000000
--- a/packages/frontend/src/scripts/fullscreen.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
-
-type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
-	webkitEnterFullscreen?(): void;
-	webkitExitFullscreen?(): void;
-};
-
-type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
-
-type RequestFullscreenProps = {
-	readonly videoEl: VideoEl;
-	readonly playerEl: PlayerEl;
-	readonly options?: FullscreenOptions | null;
-};
-
-type ExitFullscreenProps = {
-	readonly videoEl: VideoEl;
-};
-
-export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
-	if (playerEl.requestFullscreen != null) {
-		playerEl.requestFullscreen(options ?? undefined);
-		return;
-	}
-	if (videoEl.webkitEnterFullscreen != null) {
-		videoEl.webkitEnterFullscreen();
-		return;
-	}
-};
-
-export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
-	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	if (document.exitFullscreen != null) {
-		document.exitFullscreen();
-		return;
-	}
-	if (videoEl.webkitExitFullscreen != null) {
-		videoEl.webkitExitFullscreen();
-		return;
-	}
-};
diff --git a/packages/frontend/src/scripts/get-account-from-id.ts b/packages/frontend/src/scripts/get-account-from-id.ts
deleted file mode 100644
index 40afa10f2d..0000000000
--- a/packages/frontend/src/scripts/get-account-from-id.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { get } from '@/scripts/idb-proxy.js';
-
-export async function getAccountFromId(id: string) {
-	const accounts = await get('accounts') as { token: string; id: string; }[];
-	if (!accounts) console.log('Accounts are not recorded');
-	return accounts.find(account => account.id === id);
-}
diff --git a/packages/frontend/src/scripts/get-appear-note.ts b/packages/frontend/src/scripts/get-appear-note.ts
deleted file mode 100644
index 40ce80eac9..0000000000
--- a/packages/frontend/src/scripts/get-appear-note.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-
-export function getAppearNote(note: Misskey.entities.Note) {
-	return Misskey.note.isPureRenote(note) ? note.renote : note;
-}
diff --git a/packages/frontend/src/scripts/get-bg-color.ts b/packages/frontend/src/scripts/get-bg-color.ts
deleted file mode 100644
index ccf60b454f..0000000000
--- a/packages/frontend/src/scripts/get-bg-color.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import tinycolor from 'tinycolor2';
-
-export const getBgColor = (elem?: Element | null | undefined): string | null => {
-	if (elem == null) return null;
-
-	const { backgroundColor: bg } = window.getComputedStyle(elem);
-
-	if (bg && tinycolor(bg).getAlpha() !== 0) {
-		return bg;
-	}
-
-	return getBgColor(elem.parentElement);
-};
diff --git a/packages/frontend/src/scripts/get-dom-node-or-null.ts b/packages/frontend/src/scripts/get-dom-node-or-null.ts
deleted file mode 100644
index fbf54675fd..0000000000
--- a/packages/frontend/src/scripts/get-dom-node-or-null.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export const getNodeOrNull = (input: unknown): Node | null => {
-	if (input instanceof Node) return input;
-	return null;
-};
-
-export const getElementOrNull = (input: unknown): Element | null => {
-	if (input instanceof Element) return input;
-	return null;
-};
-
-export const getHTMLElementOrNull = (input: unknown): HTMLElement | null => {
-	if (input instanceof HTMLElement) return input;
-	return null;
-};
diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts
deleted file mode 100644
index 6ac0684170..0000000000
--- a/packages/frontend/src/scripts/get-drive-file-menu.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-import { defineAsyncComponent } from 'vue';
-import type { MenuItem } from '@/types/menu.js';
-import { i18n } from '@/i18n.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { prefer } from '@/preferences.js';
-
-function rename(file: Misskey.entities.DriveFile) {
-	os.inputText({
-		title: i18n.ts.renameFile,
-		placeholder: i18n.ts.inputNewFileName,
-		default: file.name,
-	}).then(({ canceled, result: name }) => {
-		if (canceled) return;
-		misskeyApi('drive/files/update', {
-			fileId: file.id,
-			name: name,
-		});
-	});
-}
-
-function describe(file: Misskey.entities.DriveFile) {
-	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
-		default: file.comment ?? '',
-		file: file,
-	}, {
-		done: caption => {
-			misskeyApi('drive/files/update', {
-				fileId: file.id,
-				comment: caption.length === 0 ? null : caption,
-			});
-		},
-		closed: () => dispose(),
-	});
-}
-
-function move(file: Misskey.entities.DriveFile) {
-	os.selectDriveFolder(false).then(folder => {
-		misskeyApi('drive/files/update', {
-			fileId: file.id,
-			folderId: folder[0] ? folder[0].id : null,
-		});
-	});
-}
-
-function toggleSensitive(file: Misskey.entities.DriveFile) {
-	misskeyApi('drive/files/update', {
-		fileId: file.id,
-		isSensitive: !file.isSensitive,
-	}).catch(err => {
-		os.alert({
-			type: 'error',
-			title: i18n.ts.error,
-			text: err.message,
-		});
-	});
-}
-
-function copyUrl(file: Misskey.entities.DriveFile) {
-	copyToClipboard(file.url);
-	os.success();
-}
-
-/*
-function addApp() {
-	alert('not implemented yet');
-}
-*/
-async function deleteFile(file: Misskey.entities.DriveFile) {
-	const { canceled } = await os.confirm({
-		type: 'warning',
-		text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
-	});
-
-	if (canceled) return;
-	misskeyApi('drive/files/delete', {
-		fileId: file.id,
-	});
-}
-
-export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] {
-	const isImage = file.type.startsWith('image/');
-
-	const menuItems: MenuItem[] = [];
-
-	menuItems.push({
-		type: 'link',
-		to: `/my/drive/file/${file.id}`,
-		text: i18n.ts._fileViewer.title,
-		icon: 'ti ti-info-circle',
-	}, { type: 'divider' }, {
-		text: i18n.ts.rename,
-		icon: 'ti ti-forms',
-		action: () => rename(file),
-	}, {
-		text: i18n.ts.move,
-		icon: 'ti ti-folder-symlink',
-		action: () => move(file),
-	}, {
-		text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
-		icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
-		action: () => toggleSensitive(file),
-	}, {
-		text: i18n.ts.describeFile,
-		icon: 'ti ti-text-caption',
-		action: () => describe(file),
-	});
-
-	if (isImage) {
-		menuItems.push({
-			text: i18n.ts.cropImage,
-			icon: 'ti ti-crop',
-			action: () => os.cropImage(file, {
-				aspectRatio: NaN,
-				uploadFolder: folder ? folder.id : folder,
-			}),
-		});
-	}
-
-	menuItems.push({ type: 'divider' }, {
-		text: i18n.ts.createNoteFromTheFile,
-		icon: 'ti ti-pencil',
-		action: () => os.post({
-			initialFiles: [file],
-		}),
-	}, {
-		text: i18n.ts.copyUrl,
-		icon: 'ti ti-link',
-		action: () => copyUrl(file),
-	}, {
-		type: 'a',
-		href: file.url,
-		target: '_blank',
-		text: i18n.ts.download,
-		icon: 'ti ti-download',
-		download: file.name,
-	}, { type: 'divider' }, {
-		text: i18n.ts.delete,
-		icon: 'ti ti-trash',
-		danger: true,
-		action: () => deleteFile(file),
-	});
-
-	if (prefer.s.devMode) {
-		menuItems.push({ type: 'divider' }, {
-			icon: 'ti ti-id',
-			text: i18n.ts.copyFileId,
-			action: () => {
-				copyToClipboard(file.id);
-			},
-		});
-	}
-
-	return menuItems;
-}
diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts
deleted file mode 100644
index 158ab9c7f8..0000000000
--- a/packages/frontend/src/scripts/get-embed-code.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-import { defineAsyncComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js';
-import { url } from '@@/js/config.js';
-import * as os from '@/os.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js';
-
-const MOBILE_THRESHOLD = 500;
-
-/**
- * パラメータを正規化する（埋め込みコード作成用）
- * @param params パラメータ
- * @returns 正規化されたパラメータ
- */
-export function normalizeEmbedParams(params: EmbedParams): Record<string, string> {
-	// paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す
-	const normalizedParams: Record<string, string> = {};
-	for (const key in params) {
-		// デフォルトの値と同じならparamsに含めない
-		if (params[key] == null || params[key] === defaultEmbedParams[key]) {
-			continue;
-		}
-		switch (typeof params[key]) {
-			case 'number':
-				normalizedParams[key] = params[key].toString();
-				break;
-			case 'boolean':
-				normalizedParams[key] = params[key] ? 'true' : 'false';
-				break;
-			default:
-				normalizedParams[key] = params[key];
-				break;
-		}
-	}
-	return normalizedParams;
-}
-
-/**
- * 埋め込みコードを生成（iframe IDの発番もやる）
- */
-export function getEmbedCode(path: string, params?: EmbedParams): string {
-	const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく
-
-	let paramString = '';
-	if (params) {
-		const searchParams = new URLSearchParams(normalizeEmbedParams(params));
-		paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString();
-	}
-
-	const iframeCode = [
-		`<iframe src="${url + path + paramString}" data-misskey-embed-id="${iframeId}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" style="border: none; width: 100%; max-width: 500px; height: 300px; color-scheme: light dark;"></iframe>`,
-		`<script defer src="${url}/embed.js"></script>`,
-	];
-	return iframeCode.join('\n');
-}
-
-/**
- * 埋め込みコードを生成してコピーする（カスタマイズ機能つき）
- *
- * カスタマイズ機能がいらない場合（事前にパラメータを指定する場合）は getEmbedCode を直接使ってください
- */
-export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) {
-	const _params = { ...params };
-
-	if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) {
-		_params.maxHeight = 700;
-	}
-
-	// PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー
-	if (window.innerWidth < MOBILE_THRESHOLD) {
-		copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params));
-		os.success();
-	} else {
-		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), {
-			entity,
-			id,
-			params: _params,
-		}, {
-			closed: () => dispose(),
-		});
-	}
-}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
deleted file mode 100644
index 8ce4a81bd4..0000000000
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ /dev/null
@@ -1,685 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { defineAsyncComponent } from 'vue';
-import type { Ref, ShallowRef } from 'vue';
-import * as Misskey from 'misskey-js';
-import { url } from '@@/js/config.js';
-import { claimAchievement } from './achievements.js';
-import type { MenuItem } from '@/types/menu.js';
-import { $i } from '@/account.js';
-import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { store, noteActions } from '@/store.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { getUserMenu } from '@/scripts/get-user-menu.js';
-import { clipsCache, favoritedChannelsCache } from '@/cache.js';
-import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { getAppearNote } from '@/scripts/get-appear-note.js';
-import { genEmbedCode } from '@/scripts/get-embed-code.js';
-import { prefer } from '@/preferences.js';
-
-export async function getNoteClipMenu(props: {
-	note: Misskey.entities.Note;
-	isDeleted: Ref<boolean>;
-	currentClip?: Misskey.entities.Clip;
-}) {
-	function getClipName(clip: Misskey.entities.Clip) {
-		if ($i && clip.userId === $i.id && clip.notesCount != null) {
-			return `${clip.name} (${clip.notesCount}/${$i.policies.noteEachClipsLimit})`;
-		} else {
-			return clip.name;
-		}
-	}
-
-	const appearNote = getAppearNote(props.note);
-
-	const clips = await clipsCache.fetch();
-	const menu: MenuItem[] = [...clips.map(clip => ({
-		text: getClipName(clip),
-		action: () => {
-			claimAchievement('noteClipped1');
-			os.promiseDialog(
-				misskeyApi('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
-				null,
-				async (err) => {
-					if (err.id === '734806c4-542c-463a-9311-15c512803965') {
-						const confirm = await os.confirm({
-							type: 'warning',
-							text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }),
-						});
-						if (!confirm.canceled) {
-							os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }).then(() => {
-								clipsCache.set(clips.map(c => {
-									if (c.id === clip.id) {
-										return {
-											...c,
-											notesCount: Math.max(0, ((c.notesCount ?? 0) - 1)),
-										};
-									} else {
-										return c;
-									}
-								}));
-							});
-							if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
-						}
-					} else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') {
-						os.alert({
-							type: 'error',
-							text: i18n.ts.clipNoteLimitExceeded,
-						});
-					} else {
-						os.alert({
-							type: 'error',
-							text: err.message + '\n' + err.id,
-						});
-					}
-				},
-			).then(() => {
-				clipsCache.set(clips.map(c => {
-					if (c.id === clip.id) {
-						return {
-							...c,
-							notesCount: (c.notesCount ?? 0) + 1,
-						};
-					} else {
-						return c;
-					}
-				}));
-			});
-		},
-	})), { type: 'divider' }, {
-		icon: 'ti ti-plus',
-		text: i18n.ts.createNew,
-		action: async () => {
-			const { canceled, result } = await os.form(i18n.ts.createNewClip, {
-				name: {
-					type: 'string',
-					default: null,
-					label: i18n.ts.name,
-				},
-				description: {
-					type: 'string',
-					required: false,
-					default: null,
-					multiline: true,
-					label: i18n.ts.description,
-				},
-				isPublic: {
-					type: 'boolean',
-					label: i18n.ts.public,
-					default: false,
-				},
-			});
-			if (canceled) return;
-
-			const clip = await os.apiWithDialog('clips/create', result);
-
-			clipsCache.delete();
-
-			claimAchievement('noteClipped1');
-			os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
-		},
-	}];
-
-	return menu;
-}
-
-export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem {
-	return {
-		icon: 'ti ti-exclamation-circle',
-		text,
-		action: (): void => {
-			const localUrl = `${url}/notes/${note.id}`;
-			let noteInfo = '';
-			if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`;
-			noteInfo += `Local Note: ${localUrl}\n`;
-			const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
-				user: note.user,
-				initialComment: `${noteInfo}-----\n`,
-			}, {
-				closed: () => dispose(),
-			});
-		},
-	};
-}
-
-export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem {
-	return {
-		icon: 'ti ti-link',
-		text,
-		action: (): void => {
-			copyToClipboard(`${url}/notes/${note.id}`);
-			os.success();
-		},
-	};
-}
-
-function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined {
-	if (note.url != null || note.uri != null) return undefined;
-	if (['specified', 'followers'].includes(note.visibility)) return undefined;
-
-	return {
-		icon: 'ti ti-code',
-		text,
-		action: (): void => {
-			genEmbedCode('notes', note.id);
-		},
-	};
-}
-
-export function getNoteMenu(props: {
-	note: Misskey.entities.Note;
-	translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
-	translating: Ref<boolean>;
-	isDeleted: Ref<boolean>;
-	currentClip?: Misskey.entities.Clip;
-}) {
-	const appearNote = getAppearNote(props.note);
-
-	const cleanups = [] as (() => void)[];
-
-	function del(): void {
-		os.confirm({
-			type: 'warning',
-			text: i18n.ts.noteDeleteConfirm,
-		}).then(({ canceled }) => {
-			if (canceled) return;
-
-			misskeyApi('notes/delete', {
-				noteId: appearNote.id,
-			});
-
-			if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
-				claimAchievement('noteDeletedWithin1min');
-			}
-		});
-	}
-
-	function delEdit(): void {
-		os.confirm({
-			type: 'warning',
-			text: i18n.ts.deleteAndEditConfirm,
-		}).then(({ canceled }) => {
-			if (canceled) return;
-
-			misskeyApi('notes/delete', {
-				noteId: appearNote.id,
-			});
-
-			os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
-
-			if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
-				claimAchievement('noteDeletedWithin1min');
-			}
-		});
-	}
-
-	function toggleFavorite(favorite: boolean): void {
-		claimAchievement('noteFavorited1');
-		os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
-			noteId: appearNote.id,
-		});
-	}
-
-	function toggleThreadMute(mute: boolean): void {
-		os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
-			noteId: appearNote.id,
-		});
-	}
-
-	function copyContent(): void {
-		copyToClipboard(appearNote.text);
-		os.success();
-	}
-
-	function togglePin(pin: boolean): void {
-		os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
-			noteId: appearNote.id,
-		}, undefined, {
-			'72dab508-c64d-498f-8740-a8eec1ba385a': {
-				text: i18n.ts.pinLimitExceeded,
-			},
-		});
-	}
-
-	async function unclip(): Promise<void> {
-		if (!props.currentClip) return;
-		os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
-		props.isDeleted.value = true;
-	}
-
-	async function promote(): Promise<void> {
-		const { canceled, result: days } = await os.inputNumber({
-			title: i18n.ts.numberOfDays,
-		});
-
-		if (canceled || days == null) return;
-
-		os.apiWithDialog('admin/promo/create', {
-			noteId: appearNote.id,
-			expiresAt: Date.now() + (86400000 * days),
-		});
-	}
-
-	function share(): void {
-		navigator.share({
-			title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }),
-			text: appearNote.text ?? '',
-			url: `${url}/notes/${appearNote.id}`,
-		});
-	}
-
-	function openDetail(): void {
-		os.pageWindow(`/notes/${appearNote.id}`);
-	}
-
-	async function translate(): Promise<void> {
-		if (props.translation.value != null) return;
-		props.translating.value = true;
-		const res = await misskeyApi('notes/translate', {
-			noteId: appearNote.id,
-			targetLang: miLocalStorage.getItem('lang') ?? navigator.language,
-		});
-		props.translating.value = false;
-		props.translation.value = res;
-	}
-
-	const menuItems: MenuItem[] = [];
-
-	if ($i) {
-		const statePromise = misskeyApi('notes/state', {
-			noteId: appearNote.id,
-		});
-
-		if (props.currentClip?.userId === $i.id) {
-			menuItems.push({
-				icon: 'ti ti-backspace',
-				text: i18n.ts.unclip,
-				danger: true,
-				action: unclip,
-			}, { type: 'divider' });
-		}
-
-		menuItems.push({
-			icon: 'ti ti-info-circle',
-			text: i18n.ts.details,
-			action: openDetail,
-		}, {
-			icon: 'ti ti-copy',
-			text: i18n.ts.copyContent,
-			action: copyContent,
-		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
-
-		if (appearNote.url || appearNote.uri) {
-			menuItems.push({
-				icon: 'ti ti-link',
-				text: i18n.ts.copyRemoteLink,
-				action: () => {
-					copyToClipboard(appearNote.url ?? appearNote.uri);
-					os.success();
-				},
-			}, {
-				icon: 'ti ti-external-link',
-				text: i18n.ts.showOnRemote,
-				action: () => {
-					window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
-				},
-			});
-		} else {
-			menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
-		}
-
-		if (isSupportShare()) {
-			menuItems.push({
-				icon: 'ti ti-share',
-				text: i18n.ts.share,
-				action: share,
-			});
-		}
-
-		if ($i.policies.canUseTranslator && instance.translatorAvailable) {
-			menuItems.push({
-				icon: 'ti ti-language-hiragana',
-				text: i18n.ts.translate,
-				action: translate,
-			});
-		}
-
-		menuItems.push({ type: 'divider' });
-
-		menuItems.push(statePromise.then(state => state.isFavorited ? {
-			icon: 'ti ti-star-off',
-			text: i18n.ts.unfavorite,
-			action: () => toggleFavorite(false),
-		} : {
-			icon: 'ti ti-star',
-			text: i18n.ts.favorite,
-			action: () => toggleFavorite(true),
-		}));
-
-		menuItems.push({
-			type: 'parent',
-			icon: 'ti ti-paperclip',
-			text: i18n.ts.clip,
-			children: () => getNoteClipMenu(props),
-		});
-
-		menuItems.push(statePromise.then(state => state.isMutedThread ? {
-			icon: 'ti ti-message-off',
-			text: i18n.ts.unmuteThread,
-			action: () => toggleThreadMute(false),
-		} : {
-			icon: 'ti ti-message-off',
-			text: i18n.ts.muteThread,
-			action: () => toggleThreadMute(true),
-		}));
-
-		if (appearNote.userId === $i.id) {
-			if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) {
-				menuItems.push({
-					icon: 'ti ti-pinned-off',
-					text: i18n.ts.unpin,
-					action: () => togglePin(false),
-				});
-			} else {
-				menuItems.push({
-					icon: 'ti ti-pin',
-					text: i18n.ts.pin,
-					action: () => togglePin(true),
-				});
-			}
-		}
-
-		menuItems.push({
-			type: 'parent',
-			icon: 'ti ti-user',
-			text: i18n.ts.user,
-			children: async () => {
-				const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
-				const { menu, cleanup } = getUserMenu(user);
-				cleanups.push(cleanup);
-				return menu;
-			},
-		});
-
-		if (appearNote.userId !== $i.id) {
-			menuItems.push({ type: 'divider' });
-			menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse));
-		}
-
-		if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) {
-			menuItems.push({ type: 'divider' });
-			menuItems.push({
-				type: 'parent',
-				icon: 'ti ti-device-tv',
-				text: i18n.ts.channel,
-				children: async () => {
-					const channelChildMenu = [] as MenuItem[];
-
-					const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id });
-
-					if (channel.pinnedNoteIds.includes(appearNote.id)) {
-						channelChildMenu.push({
-							icon: 'ti ti-pinned-off',
-							text: i18n.ts.unpin,
-							action: () => os.apiWithDialog('channels/update', {
-								channelId: appearNote.channel!.id,
-								pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id),
-							}),
-						});
-					} else {
-						channelChildMenu.push({
-							icon: 'ti ti-pin',
-							text: i18n.ts.pin,
-							action: () => os.apiWithDialog('channels/update', {
-								channelId: appearNote.channel!.id,
-								pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id],
-							}),
-						});
-					}
-					return channelChildMenu;
-				},
-			});
-		}
-
-		if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) {
-			menuItems.push({ type: 'divider' });
-			if (appearNote.userId === $i.id) {
-				menuItems.push({
-					icon: 'ti ti-edit',
-					text: i18n.ts.deleteAndEdit,
-					action: delEdit,
-				});
-			}
-			menuItems.push({
-				icon: 'ti ti-trash',
-				text: i18n.ts.delete,
-				danger: true,
-				action: del,
-			});
-		}
-	} else {
-		menuItems.push({
-			icon: 'ti ti-info-circle',
-			text: i18n.ts.details,
-			action: openDetail,
-		}, {
-			icon: 'ti ti-copy',
-			text: i18n.ts.copyContent,
-			action: copyContent,
-		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
-
-		if (appearNote.url || appearNote.uri) {
-			menuItems.push({
-				icon: 'ti ti-link',
-				text: i18n.ts.copyRemoteLink,
-				action: () => {
-					copyToClipboard(appearNote.url ?? appearNote.uri);
-					os.success();
-				},
-			}, {
-				icon: 'ti ti-external-link',
-				text: i18n.ts.showOnRemote,
-				action: () => {
-					window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
-				},
-			});
-		} else {
-			menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
-		}
-	}
-
-	if (noteActions.length > 0) {
-		menuItems.push({ type: 'divider' });
-
-		menuItems.push(...noteActions.map(action => ({
-			icon: 'ti ti-plug',
-			text: action.title,
-			action: () => {
-				action.handler(appearNote);
-			},
-		})));
-	}
-
-	if (prefer.s.devMode) {
-		menuItems.push({ type: 'divider' }, {
-			icon: 'ti ti-id',
-			text: i18n.ts.copyNoteId,
-			action: () => {
-				copyToClipboard(appearNote.id);
-				os.success();
-			},
-		});
-	}
-
-	const cleanup = () => {
-		if (_DEV_) console.log('note menu cleanup', cleanups);
-		for (const cl of cleanups) {
-			cl();
-		}
-	};
-
-	return {
-		menu: menuItems,
-		cleanup,
-	};
-}
-
-type Visibility = (typeof Misskey.noteVisibilities)[number];
-
-function smallerVisibility(a: Visibility, b: Visibility): Visibility {
-	if (a === 'specified' || b === 'specified') return 'specified';
-	if (a === 'followers' || b === 'followers') return 'followers';
-	if (a === 'home' || b === 'home') return 'home';
-	// if (a === 'public' || b === 'public')
-	return 'public';
-}
-
-export function getRenoteMenu(props: {
-	note: Misskey.entities.Note;
-	renoteButton: ShallowRef<HTMLElement | undefined>;
-	mock?: boolean;
-}) {
-	const appearNote = getAppearNote(props.note);
-
-	const channelRenoteItems: MenuItem[] = [];
-	const normalRenoteItems: MenuItem[] = [];
-	const normalExternalChannelRenoteItems: MenuItem[] = [];
-
-	if (appearNote.channel) {
-		channelRenoteItems.push(...[{
-			text: i18n.ts.inChannelRenote,
-			icon: 'ti ti-repeat',
-			action: () => {
-				const el = props.renoteButton.value;
-				if (el && prefer.s.animation) {
-					const rect = el.getBoundingClientRect();
-					const x = rect.left + (el.offsetWidth / 2);
-					const y = rect.top + (el.offsetHeight / 2);
-					const { dispose } = os.popup(MkRippleEffect, { x, y }, {
-						end: () => dispose(),
-					});
-				}
-
-				if (!props.mock) {
-					misskeyApi('notes/create', {
-						renoteId: appearNote.id,
-						channelId: appearNote.channelId,
-					}).then(() => {
-						os.toast(i18n.ts.renoted);
-					});
-				}
-			},
-		}, {
-			text: i18n.ts.inChannelQuote,
-			icon: 'ti ti-quote',
-			action: () => {
-				if (!props.mock) {
-					os.post({
-						renote: appearNote,
-						channel: appearNote.channel,
-					});
-				}
-			},
-		}]);
-	}
-
-	if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) {
-		normalRenoteItems.push(...[{
-			text: i18n.ts.renote,
-			icon: 'ti ti-repeat',
-			action: () => {
-				const el = props.renoteButton.value;
-				if (el && prefer.s.animation) {
-					const rect = el.getBoundingClientRect();
-					const x = rect.left + (el.offsetWidth / 2);
-					const y = rect.top + (el.offsetHeight / 2);
-					const { dispose } = os.popup(MkRippleEffect, { x, y }, {
-						end: () => dispose(),
-					});
-				}
-
-				const configuredVisibility = prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility;
-				const localOnly = prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly;
-
-				let visibility = appearNote.visibility;
-				visibility = smallerVisibility(visibility, configuredVisibility);
-				if (appearNote.channel?.isSensitive) {
-					visibility = smallerVisibility(visibility, 'home');
-				}
-
-				if (!props.mock) {
-					misskeyApi('notes/create', {
-						localOnly,
-						visibility,
-						renoteId: appearNote.id,
-					}).then(() => {
-						os.toast(i18n.ts.renoted);
-					});
-				}
-			},
-		}, (props.mock) ? undefined : {
-			text: i18n.ts.quote,
-			icon: 'ti ti-quote',
-			action: () => {
-				os.post({
-					renote: appearNote,
-				});
-			},
-		}]);
-
-		normalExternalChannelRenoteItems.push({
-			type: 'parent',
-			icon: 'ti ti-repeat',
-			text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel,
-			children: async () => {
-				const channels = await favoritedChannelsCache.fetch();
-				return channels.filter((channel) => {
-					if (!appearNote.channelId) return true;
-					return channel.id !== appearNote.channelId;
-				}).map((channel) => ({
-					text: channel.name,
-					action: () => {
-						const el = props.renoteButton.value;
-						if (el && prefer.s.animation) {
-							const rect = el.getBoundingClientRect();
-							const x = rect.left + (el.offsetWidth / 2);
-							const y = rect.top + (el.offsetHeight / 2);
-							const { dispose } = os.popup(MkRippleEffect, { x, y }, {
-								end: () => dispose(),
-							});
-						}
-
-						if (!props.mock) {
-							misskeyApi('notes/create', {
-								renoteId: appearNote.id,
-								channelId: channel.id,
-							}).then(() => {
-								os.toast(i18n.tsx.renotedToX({ name: channel.name }));
-							});
-						}
-					},
-				}));
-			},
-		});
-	}
-
-	const renoteItems = [
-		...normalRenoteItems,
-		...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [],
-		...channelRenoteItems,
-		...(normalExternalChannelRenoteItems.length > 0 && (normalRenoteItems.length > 0 || channelRenoteItems.length > 0)) ? [{ type: 'divider' }] as MenuItem[] : [],
-		...normalExternalChannelRenoteItems,
-	];
-
-	return {
-		menu: renoteItems,
-	};
-}
diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts
deleted file mode 100644
index 6fd9947ac1..0000000000
--- a/packages/frontend/src/scripts/get-note-summary.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-import { i18n } from '@/i18n.js';
-
-/**
- * 投稿を表す文字列を取得します。
- * @param {*} note (packされた)投稿
- */
-export const getNoteSummary = (note?: Misskey.entities.Note | null): string => {
-	if (note == null) {
-		return '';
-	}
-
-	if (note.deletedAt) {
-		return `(${i18n.ts.deletedNote})`;
-	}
-
-	if (note.isHidden) {
-		return `(${i18n.ts.invisibleNote})`;
-	}
-
-	let summary = '';
-
-	// 本文
-	if (note.cw != null) {
-		summary += note.cw;
-	} else {
-		summary += note.text ? note.text : '';
-	}
-
-	// ファイルが添付されているとき
-	if ((note.files || []).length !== 0) {
-		summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`;
-	}
-
-	// 投票が添付されているとき
-	if (note.poll) {
-		summary += ` (${i18n.ts.poll})`;
-	}
-
-	// 返信のとき
-	if (note.replyId) {
-		if (note.reply) {
-			summary += `\n\nRE: ${getNoteSummary(note.reply)}`;
-		} else {
-			summary += '\n\nRE: ...';
-		}
-	}
-
-	// Renoteのとき
-	if (note.renoteId) {
-		if (note.renote) {
-			summary += `\n\nRN: ${getNoteSummary(note.renote)}`;
-		} else {
-			summary += '\n\nRN: ...';
-		}
-	}
-
-	return summary.trim();
-};
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
deleted file mode 100644
index 6892c3a4e4..0000000000
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ /dev/null
@@ -1,441 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { toUnicode } from 'punycode.js';
-import { defineAsyncComponent, ref, watch } from 'vue';
-import * as Misskey from 'misskey-js';
-import { host, url } from '@@/js/config.js';
-import type { IRouter } from '@/nirax.js';
-import type { MenuItem } from '@/types/menu.js';
-import { i18n } from '@/i18n.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { userActions } from '@/store.js';
-import { $i, iAmModerator } from '@/account.js';
-import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
-import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
-import { mainRouter } from '@/router/main.js';
-import { genEmbedCode } from '@/scripts/get-embed-code.js';
-import { prefer } from '@/preferences.js';
-
-export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
-	const meId = $i ? $i.id : null;
-
-	const cleanups = [] as (() => void)[];
-
-	async function toggleMute() {
-		if (user.isMuted) {
-			os.apiWithDialog('mute/delete', {
-				userId: user.id,
-			}).then(() => {
-				user.isMuted = false;
-			});
-		} else {
-			const { canceled, result: period } = await os.select({
-				title: i18n.ts.mutePeriod,
-				items: [{
-					value: 'indefinitely', text: i18n.ts.indefinitely,
-				}, {
-					value: 'tenMinutes', text: i18n.ts.tenMinutes,
-				}, {
-					value: 'oneHour', text: i18n.ts.oneHour,
-				}, {
-					value: 'oneDay', text: i18n.ts.oneDay,
-				}, {
-					value: 'oneWeek', text: i18n.ts.oneWeek,
-				}],
-				default: 'indefinitely',
-			});
-			if (canceled) return;
-
-			const expiresAt = period === 'indefinitely' ? null
-				: period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
-				: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
-				: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
-				: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
-				: null;
-
-			os.apiWithDialog('mute/create', {
-				userId: user.id,
-				expiresAt,
-			}).then(() => {
-				user.isMuted = true;
-			});
-		}
-	}
-
-	async function toggleRenoteMute() {
-		os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', {
-			userId: user.id,
-		}).then(() => {
-			user.isRenoteMuted = !user.isRenoteMuted;
-		});
-	}
-
-	async function toggleBlock() {
-		if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
-
-		os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', {
-			userId: user.id,
-		}).then(() => {
-			user.isBlocking = !user.isBlocking;
-		});
-	}
-
-	async function toggleNotify() {
-		os.apiWithDialog('following/update', {
-			userId: user.id,
-			notify: user.notify === 'normal' ? 'none' : 'normal',
-		}).then(() => {
-			user.notify = user.notify === 'normal' ? 'none' : 'normal';
-		});
-	}
-
-	function reportAbuse() {
-		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
-			user: user,
-		}, {
-			closed: () => dispose(),
-		});
-	}
-
-	async function getConfirmed(text: string): Promise<boolean> {
-		const confirm = await os.confirm({
-			type: 'warning',
-			title: 'confirm',
-			text,
-		});
-
-		return !confirm.canceled;
-	}
-
-	async function userInfoUpdate() {
-		os.apiWithDialog('federation/update-remote-user', {
-			userId: user.id,
-		});
-	}
-
-	async function invalidateFollow() {
-		if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return;
-
-		os.apiWithDialog('following/invalidate', {
-			userId: user.id,
-		}).then(() => {
-			user.isFollowed = !user.isFollowed;
-		});
-	}
-
-	async function editMemo(): Promise<void> {
-		const userDetailed = await misskeyApi('users/show', {
-			userId: user.id,
-		});
-		const { canceled, result } = await os.form(i18n.ts.editMemo, {
-			memo: {
-				type: 'string',
-				required: true,
-				multiline: true,
-				label: i18n.ts.memo,
-				default: userDetailed.memo,
-			},
-		});
-		if (canceled) return;
-
-		os.apiWithDialog('users/update-memo', {
-			memo: result.memo,
-			userId: user.id,
-		});
-	}
-
-	const menuItems: MenuItem[] = [];
-
-	menuItems.push({
-		icon: 'ti ti-at',
-		text: i18n.ts.copyUsername,
-		action: () => {
-			copyToClipboard(`@${user.username}@${user.host ?? host}`);
-		},
-	});
-
-	if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) {
-		menuItems.push({
-			icon: 'ti ti-search',
-			text: i18n.ts.searchThisUsersNotes,
-			action: () => {
-				router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
-			},
-		});
-	}
-
-	if (iAmModerator) {
-		menuItems.push({
-			icon: 'ti ti-user-exclamation',
-			text: i18n.ts.moderation,
-			action: () => {
-				router.push(`/admin/user/${user.id}`);
-			},
-		});
-	}
-
-	menuItems.push({
-		icon: 'ti ti-rss',
-		text: i18n.ts.copyRSS,
-		action: () => {
-			copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
-		},
-	});
-
-	if (user.host != null && user.url != null) {
-		menuItems.push({
-			icon: 'ti ti-external-link',
-			text: i18n.ts.showOnRemote,
-			action: () => {
-				if (user.url == null) return;
-				window.open(user.url, '_blank', 'noopener');
-			},
-		});
-	} else {
-		menuItems.push({
-			icon: 'ti ti-code',
-			text: i18n.ts.genEmbedCode,
-			type: 'parent',
-			children: [{
-				text: i18n.ts.noteOfThisUser,
-				action: () => {
-					genEmbedCode('user-timeline', user.id);
-				},
-			}], // TODO: ユーザーカードの埋め込みなど
-		});
-	}
-
-	menuItems.push({
-		icon: 'ti ti-share',
-		text: i18n.ts.copyProfileUrl,
-		action: () => {
-			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
-			copyToClipboard(`${url}/${canonical}`);
-		},
-	});
-
-	if ($i) {
-		menuItems.push({
-			icon: 'ti ti-mail',
-			text: i18n.ts.sendMessage,
-			action: () => {
-				const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
-				os.post({ specified: user, initialText: `${canonical} ` });
-			},
-		}, { type: 'divider' }, {
-			icon: 'ti ti-pencil',
-			text: i18n.ts.editMemo,
-			action: editMemo,
-		}, {
-			type: 'parent',
-			icon: 'ti ti-list',
-			text: i18n.ts.addToList,
-			children: async () => {
-				const lists = await userListsCache.fetch();
-				return lists.map(list => {
-					const isListed = ref(list.userIds?.includes(user.id) ?? false);
-					cleanups.push(watch(isListed, () => {
-						if (isListed.value) {
-							os.apiWithDialog('users/lists/push', {
-								listId: list.id,
-								userId: user.id,
-							}).then(() => {
-								list.userIds?.push(user.id);
-							});
-						} else {
-							os.apiWithDialog('users/lists/pull', {
-								listId: list.id,
-								userId: user.id,
-							}).then(() => {
-								list.userIds?.splice(list.userIds.indexOf(user.id), 1);
-							});
-						}
-					}));
-
-					return {
-						type: 'switch',
-						text: list.name,
-						ref: isListed,
-					};
-				});
-			},
-		}, {
-			type: 'parent',
-			icon: 'ti ti-antenna',
-			text: i18n.ts.addToAntenna,
-			children: async () => {
-				const antennas = await antennasCache.fetch();
-				const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
-				return antennas.filter((a) => a.src === 'users').map(antenna => ({
-					text: antenna.name,
-					action: async () => {
-						await os.apiWithDialog('antennas/update', {
-							antennaId: antenna.id,
-							name: antenna.name,
-							keywords: antenna.keywords,
-							excludeKeywords: antenna.excludeKeywords,
-							src: antenna.src,
-							userListId: antenna.userListId,
-							users: [...antenna.users, canonical],
-							caseSensitive: antenna.caseSensitive,
-							withReplies: antenna.withReplies,
-							withFile: antenna.withFile,
-							notify: antenna.notify,
-						});
-						antennasCache.delete();
-					},
-				}));
-			},
-		});
-	}
-
-	if ($i && meId !== user.id) {
-		if (iAmModerator) {
-			menuItems.push({
-				type: 'parent',
-				icon: 'ti ti-badges',
-				text: i18n.ts.roles,
-				children: async () => {
-					const roles = await rolesCache.fetch();
-
-					return roles.filter(r => r.target === 'manual').map(r => ({
-						text: r.name,
-						action: async () => {
-							const { canceled, result: period } = await os.select({
-								title: i18n.ts.period + ': ' + r.name,
-								items: [{
-									value: 'indefinitely', text: i18n.ts.indefinitely,
-								}, {
-									value: 'oneHour', text: i18n.ts.oneHour,
-								}, {
-									value: 'oneDay', text: i18n.ts.oneDay,
-								}, {
-									value: 'oneWeek', text: i18n.ts.oneWeek,
-								}, {
-									value: 'oneMonth', text: i18n.ts.oneMonth,
-								}],
-								default: 'indefinitely',
-							});
-							if (canceled) return;
-
-							const expiresAt = period === 'indefinitely' ? null
-								: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
-								: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
-								: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
-								: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
-								: null;
-
-							os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt });
-						},
-					}));
-				},
-			});
-		}
-
-		// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
-		//if (user.isFollowing) {
-		const withRepliesRef = ref(user.withReplies ?? false);
-
-		menuItems.push({
-			type: 'switch',
-			icon: 'ti ti-messages',
-			text: i18n.ts.showRepliesToOthersInTimeline,
-			ref: withRepliesRef,
-		}, {
-			icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off',
-			text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes,
-			action: toggleNotify,
-		});
-
-		watch(withRepliesRef, (withReplies) => {
-			misskeyApi('following/update', {
-				userId: user.id,
-				withReplies,
-			}).then(() => {
-				user.withReplies = withReplies;
-			});
-		});
-		//}
-
-		menuItems.push({ type: 'divider' }, {
-			icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
-			text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
-			action: toggleMute,
-		}, {
-			icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off',
-			text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute,
-			action: toggleRenoteMute,
-		}, {
-			icon: 'ti ti-ban',
-			text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
-			action: toggleBlock,
-		});
-
-		if (user.isFollowed) {
-			menuItems.push({
-				icon: 'ti ti-link-off',
-				text: i18n.ts.breakFollow,
-				action: invalidateFollow,
-			});
-		}
-
-		menuItems.push({ type: 'divider' }, {
-			icon: 'ti ti-exclamation-circle',
-			text: i18n.ts.reportAbuse,
-			action: reportAbuse,
-		});
-	}
-
-	if (user.host !== null) {
-		menuItems.push({ type: 'divider' }, {
-			icon: 'ti ti-refresh',
-			text: i18n.ts.updateRemoteUser,
-			action: userInfoUpdate,
-		});
-	}
-
-	if (prefer.s.devMode) {
-		menuItems.push({ type: 'divider' }, {
-			icon: 'ti ti-id',
-			text: i18n.ts.copyUserId,
-			action: () => {
-				copyToClipboard(user.id);
-			},
-		});
-	}
-
-	if ($i && meId === user.id) {
-		menuItems.push({ type: 'divider' }, {
-			icon: 'ti ti-pencil',
-			text: i18n.ts.editProfile,
-			action: () => {
-				router.push('/settings/profile');
-			},
-		});
-	}
-
-	if (userActions.length > 0) {
-		menuItems.push({ type: 'divider' }, ...userActions.map(action => ({
-			icon: 'ti ti-plug',
-			text: action.title,
-			action: () => {
-				action.handler(user);
-			},
-		})));
-	}
-
-	return {
-		menu: menuItems,
-		cleanup: () => {
-			if (_DEV_) console.log('user menu cleanup', cleanups);
-			for (const cl of cleanups) {
-				cl();
-			}
-		},
-	};
-}
diff --git a/packages/frontend/src/scripts/get-user-name.ts b/packages/frontend/src/scripts/get-user-name.ts
deleted file mode 100644
index 56e91abba0..0000000000
--- a/packages/frontend/src/scripts/get-user-name.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export default function(user: { name?: string | null, username: string }): string {
-	return user.name === '' ? user.username : user.name ?? user.username;
-}
diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts
deleted file mode 100644
index 04fb235694..0000000000
--- a/packages/frontend/src/scripts/hotkey.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-import { getHTMLElementOrNull } from "@/scripts/get-dom-node-or-null.js";
-
-//#region types
-export type Keymap = Record<string, CallbackFunction | CallbackObject>;
-
-type CallbackFunction = (ev: KeyboardEvent) => unknown;
-
-type CallbackObject = {
-	callback: CallbackFunction;
-	allowRepeat?: boolean;
-};
-
-type Pattern = {
-	which: string[];
-	ctrl: boolean;
-	alt: boolean;
-	shift: boolean;
-};
-
-type Action = {
-	patterns: Pattern[];
-	callback: CallbackFunction;
-	options: Required<Omit<CallbackObject, 'callback'>>;
-};
-//#endregion
-
-//#region consts
-const KEY_ALIASES = {
-	'esc': 'Escape',
-	'enter': 'Enter',
-	'space': ' ',
-	'up': 'ArrowUp',
-	'down': 'ArrowDown',
-	'left': 'ArrowLeft',
-	'right': 'ArrowRight',
-	'plus': ['+', ';'],
-};
-
-const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
-
-const IGNORE_ELEMENTS = ['input', 'textarea'];
-//#endregion
-
-//#region store
-let latestHotkey: Pattern & { callback: CallbackFunction } | null = null;
-//#endregion
-
-//#region impl
-export const makeHotkey = (keymap: Keymap) => {
-	const actions = parseKeymap(keymap);
-	return (ev: KeyboardEvent) => {
-		if ('pswp' in window && window.pswp != null) return;
-		if (document.activeElement != null) {
-			if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return;
-			if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return;
-		}
-		for (const action of actions) {
-			if (matchPatterns(ev, action)) {
-				ev.preventDefault();
-				ev.stopPropagation();
-				action.callback(ev);
-				storePattern(ev, action.callback);
-			}
-		}
-	};
-};
-
-const parseKeymap = (keymap: Keymap) => {
-	return Object.entries(keymap).map(([rawPatterns, rawCallback]) => {
-		const patterns = parsePatterns(rawPatterns);
-		const callback = parseCallback(rawCallback);
-		const options = parseOptions(rawCallback);
-		return { patterns, callback, options } as const satisfies Action;
-	});
-};
-
-const parsePatterns = (rawPatterns: keyof Keymap) => {
-	return rawPatterns.split('|').map(part => {
-		const keys = part.split('+').map(trimLower);
-		const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x)));
-		const ctrl = keys.includes('ctrl');
-		const alt = keys.includes('alt');
-		const shift = keys.includes('shift');
-		return { which, ctrl, alt, shift } as const satisfies Pattern;
-	});
-};
-
-const parseCallback = (rawCallback: Keymap[keyof Keymap]) => {
-	if (typeof rawCallback === 'object') {
-		return rawCallback.callback;
-	}
-	return rawCallback;
-};
-
-const parseOptions = (rawCallback: Keymap[keyof Keymap]) => {
-	const defaultOptions = {
-		allowRepeat: false,
-	} as const satisfies Action['options'];
-	if (typeof rawCallback === 'object') {
-		const { callback, ...rawOptions } = rawCallback;
-		const options = { ...defaultOptions, ...rawOptions };
-		return { ...options } as const satisfies Action['options'];
-	}
-	return { ...defaultOptions } as const satisfies Action['options'];
-};
-
-const matchPatterns = (ev: KeyboardEvent, action: Action) => {
-	const { patterns, options, callback } = action;
-	if (ev.repeat && !options.allowRepeat) return false;
-	const key = ev.key.toLowerCase();
-	return patterns.some(({ which, ctrl, shift, alt }) => {
-		if (
-			options.allowRepeat === false &&
-			latestHotkey != null &&
-			latestHotkey.which.includes(key) &&
-			latestHotkey.ctrl === ctrl &&
-			latestHotkey.alt === alt &&
-			latestHotkey.shift === shift &&
-			latestHotkey.callback === callback
-		) {
-			return false;
-		}
-		if (!which.includes(key)) return false;
-		if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false;
-		if (alt !== ev.altKey) return false;
-		if (shift !== ev.shiftKey) return false;
-		return true;
-	});
-};
-
-let lastHotKeyStoreTimer: number | null = null;
-
-const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => {
-	if (lastHotKeyStoreTimer != null) {
-		clearTimeout(lastHotKeyStoreTimer);
-	}
-
-	latestHotkey = {
-		which: [ev.key.toLowerCase()],
-		ctrl: ev.ctrlKey || ev.metaKey,
-		alt: ev.altKey,
-		shift: ev.shiftKey,
-		callback,
-	};
-
-	lastHotKeyStoreTimer = window.setTimeout(() => {
-		latestHotkey = null;
-	}, 500);
-};
-
-const parseKeyCode = (input?: string | null) => {
-	if (input == null) return [];
-	const raw = getValueByKey(KEY_ALIASES, input);
-	if (raw == null) return [input];
-	if (typeof raw === 'string') return [trimLower(raw)];
-	return raw.map(trimLower);
-};
-
-const getValueByKey = <
-	T extends Record<keyof any, unknown>,
-	K extends keyof T | keyof any,
-	R extends K extends keyof T ? T[K] : T[keyof T] | undefined,
->(obj: T, key: K) => {
-	return obj[key] as R;
-};
-
-const trimLower = (str: string) => str.trim().toLowerCase();
-//#endregion
diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts
deleted file mode 100644
index 20f51660c7..0000000000
--- a/packages/frontend/src/scripts/idb-proxy.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、
-// indexedDBが使えない環境ではlocalStorageを使う
-import {
-	get as iget,
-	set as iset,
-	del as idel,
-} from 'idb-keyval';
-import { miLocalStorage } from '@/local-storage.js';
-
-const PREFIX = 'idbfallback::';
-
-let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true;
-
-// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
-// バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと
-// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-expect-error
-if (window.Cypress) {
-	idbAvailable = false;
-	console.log('Cypress detected. It will use localStorage.');
-}
-
-if (idbAvailable) {
-	await iset('idb-test', 'test')
-		.catch(err => {
-			console.error('idb error', err);
-			console.error('indexedDB is unavailable. It will use localStorage.');
-			idbAvailable = false;
-		});
-} else {
-	console.error('indexedDB is unavailable. It will use localStorage.');
-}
-
-export async function get(key: string) {
-	if (idbAvailable) return iget(key);
-	return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
-}
-
-export async function set(key: string, val: any) {
-	if (idbAvailable) return iset(key, val);
-	return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
-}
-
-export async function del(key: string) {
-	if (idbAvailable) return idel(key);
-	return miLocalStorage.removeItem(`${PREFIX}${key}`);
-}
diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/scripts/idle-render.ts
deleted file mode 100644
index 6adfedcb9f..0000000000
--- a/packages/frontend/src/scripts/idle-render.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => {
-	const start = performance.now();
-	const timeoutId = setTimeout(() => {
-		callback({
-			didTimeout: false, // polyfill でタイムアウト発火することはない
-			timeRemaining() {
-				const diff = performance.now() - start;
-				return Math.max(0, 50 - diff); // <https://www.w3.org/TR/requestidlecallback/#idle-periods>
-			},
-		});
-	});
-	return timeoutId;
-});
-const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => {
-	clearTimeout(timeoutId);
-});
-
-class IdlingRenderScheduler {
-	#renderers: Set<FrameRequestCallback>;
-	#rafId: number;
-	#ricId: number;
-
-	constructor() {
-		this.#renderers = new Set();
-		this.#rafId = 0;
-		this.#ricId = requestIdleCallback((deadline) => this.#schedule(deadline));
-	}
-
-	#schedule(deadline: IdleDeadline): void {
-		if (deadline.timeRemaining()) {
-			this.#rafId = requestAnimationFrame((time) => {
-				for (const renderer of this.#renderers) {
-					renderer(time);
-				}
-			});
-		}
-		this.#ricId = requestIdleCallback((arg) => this.#schedule(arg));
-	}
-
-	add(renderer: FrameRequestCallback): void {
-		this.#renderers.add(renderer);
-	}
-
-	delete(renderer: FrameRequestCallback): void {
-		this.#renderers.delete(renderer);
-	}
-
-	dispose(): void {
-		this.#renderers.clear();
-		cancelAnimationFrame(this.#rafId);
-		cancelIdleCallback(this.#ricId);
-	}
-}
-
-export const defaultIdlingRenderScheduler = new IdlingRenderScheduler();
diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts
deleted file mode 100644
index 037b0d9567..0000000000
--- a/packages/frontend/src/scripts/init-chart.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import {
-	Chart,
-	ArcElement,
-	LineElement,
-	BarElement,
-	PointElement,
-	BarController,
-	LineController,
-	DoughnutController,
-	CategoryScale,
-	LinearScale,
-	TimeScale,
-	Legend,
-	Title,
-	Tooltip,
-	SubTitle,
-	Filler,
-} from 'chart.js';
-import gradient from 'chartjs-plugin-gradient';
-import zoomPlugin from 'chartjs-plugin-zoom';
-import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
-import { store } from '@/store.js';
-import 'chartjs-adapter-date-fns';
-
-export function initChart() {
-	Chart.register(
-		ArcElement,
-		LineElement,
-		BarElement,
-		PointElement,
-		BarController,
-		LineController,
-		DoughnutController,
-		CategoryScale,
-		LinearScale,
-		TimeScale,
-		Legend,
-		Title,
-		Tooltip,
-		SubTitle,
-		Filler,
-		MatrixController, MatrixElement,
-		zoomPlugin,
-		gradient,
-	);
-
-	// フォントカラー
-	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
-
-	Chart.defaults.borderColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
-	Chart.defaults.animation = false;
-}
diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts
deleted file mode 100644
index 867ebf19ed..0000000000
--- a/packages/frontend/src/scripts/initialize-sw.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { lang } from '@@/js/config.js';
-
-export async function initializeSw() {
-	if (!('serviceWorker' in navigator)) return;
-
-	navigator.serviceWorker.register('/sw.js', { scope: '/', type: 'classic' });
-	navigator.serviceWorker.ready.then(registration => {
-		registration.active?.postMessage({
-			msg: 'initialize',
-			lang,
-		});
-	});
-}
diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts
deleted file mode 100644
index 385f59ec39..0000000000
--- a/packages/frontend/src/scripts/intl-const.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { lang } from '@@/js/config.js';
-
-export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
-
-let _dateTimeFormat: Intl.DateTimeFormat;
-try {
-	_dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
-		year: 'numeric',
-		month: 'numeric',
-		day: 'numeric',
-		hour: 'numeric',
-		minute: 'numeric',
-		second: 'numeric',
-	});
-} catch (err) {
-	console.warn(err);
-	if (_DEV_) console.log('[Intl] Fallback to en-US');
-
-	// Fallback to en-US
-	_dateTimeFormat = new Intl.DateTimeFormat('en-US', {
-		year: 'numeric',
-		month: 'numeric',
-		day: 'numeric',
-		hour: 'numeric',
-		minute: 'numeric',
-		second: 'numeric',
-	});
-}
-export const dateTimeFormat = _dateTimeFormat;
-
-export const timeZone = dateTimeFormat.resolvedOptions().timeZone;
-
-export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N';
-
-let _numberFormat: Intl.NumberFormat;
-try {
-	_numberFormat = new Intl.NumberFormat(versatileLang);
-} catch (err) {
-	console.warn(err);
-	if (_DEV_) console.log('[Intl] Fallback to en-US');
-
-	// Fallback to en-US
-	_numberFormat = new Intl.NumberFormat('en-US');
-}
-export const numberFormat = _numberFormat;
diff --git a/packages/frontend/src/scripts/intl-string.ts b/packages/frontend/src/scripts/intl-string.ts
deleted file mode 100644
index a5b5bbb592..0000000000
--- a/packages/frontend/src/scripts/intl-string.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { versatileLang } from '@@/js/intl-const.js';
-import type { toHiragana as toHiraganaType } from 'wanakana';
-
-let toHiragana: typeof toHiraganaType = (str?: string) => str ?? '';
-let isWanakanaLoaded = false;
-
-/** 
- * ローマ字変換のセットアップ（日本語以外の環境で読み込まないのでlazy-loading）
- * 
- * ここの比較系関数を使う際は事前に呼び出す必要がある
- */
-export async function initIntlString(forceWanakana = false) {
-	if ((!versatileLang.includes('ja') && !forceWanakana) || isWanakanaLoaded) return;
-	const { toHiragana: _toHiragana } = await import('wanakana');
-	toHiragana = _toHiragana;
-	isWanakanaLoaded = true;
-}
-
-/**
- * - 全角英数字を半角に
- * - 半角カタカナを全角に
- * - 濁点・半濁点がリガチャになっている（例: `か` ＋ `゛` ）ひらがな・カタカナを結合
- * - 異体字を正規化
- * - 小文字に揃える
- * - 文字列のトリム
- */
-export function normalizeString(str: string) {
-	const segmenter = new Intl.Segmenter(versatileLang, { granularity: 'grapheme' });
-	return [...segmenter.segment(str)].map(({ segment }) => segment.normalize('NFKC')).join('').toLowerCase().trim();
-}
-
-// https://qiita.com/non-caffeine/items/77360dda05c8ce510084
-const hyphens = [
-	0x002d, // hyphen-minus
-	0x02d7, // modifier letter minus sign
-	0x1173, // hangul jongseong eu
-	0x1680, // ogham space mark
-	0x1b78, // balinese musical symbol left-hand open pang
-	0x2010, // hyphen
-	0x2011, // non-breaking hyphen
-	0x2012, // figure dash
-	0x2013, // en dash
-	0x2014, // em dash
-	0x2015, // horizontal bar
-	0x2043, // hyphen bullet
-	0x207b, // superscript minus
-	0x2212, // minus sign
-	0x25ac, // black rectangle
-	0x2500, // box drawings light horizontal
-	0x2501, // box drawings heavy horizontal
-	0x2796, // heavy minus sign
-	0x30fc, // katakana-hiragana prolonged sound mark
-	0x3161, // hangul letter eu
-	0xfe58, // small em dash
-	0xfe63, // small hyphen-minus
-	0xff0d, // fullwidth hyphen-minus
-	0xff70, // halfwidth katakana-hiragana prolonged sound mark
-	0x10110, // aegean number ten
-	0x10191, // roman uncia sign
-];
-
-const hyphensCodePoints = hyphens.map(code => `\\u{${code.toString(16).padStart(4, '0')}}`);
-
-/** ハイフンを統一（ローマ字半角入力時に`ー`と`-`が判定できない問題の調整） */
-export function normalizeHyphens(str: string) {
-	return str.replace(new RegExp(`[${hyphensCodePoints.join('')}]`, 'ug'), '\u002d');
-}
-
-/**
- * `normalizeString` に加えて、カタカナ・ローマ字をひらがなに揃え、ハイフンを統一
- *
- * （ローマ字じゃないものもローマ字として認識され変換されるので、文字列比較の際は `normalizeString` を併用する必要あり）
- */
-export function normalizeStringWithHiragana(str: string) {
-	return normalizeHyphens(toHiragana(normalizeString(str), { convertLongVowelMark: false }));
-}
-
-/** aとbが同じかどうか */
-export function compareStringEquals(a: string, b: string) {
-	return (
-		normalizeString(a) === normalizeString(b) ||
-		normalizeStringWithHiragana(a) === normalizeStringWithHiragana(b)
-	);
-}
-
-/** baseにqueryが含まれているかどうか */
-export function compareStringIncludes(base: string, query: string) {
-	return (
-		normalizeString(base).includes(normalizeString(query)) ||
-		normalizeStringWithHiragana(base).includes(normalizeStringWithHiragana(query))
-	);
-}
diff --git a/packages/frontend/src/scripts/is-device-darkmode.ts b/packages/frontend/src/scripts/is-device-darkmode.ts
deleted file mode 100644
index 4f487c7cb9..0000000000
--- a/packages/frontend/src/scripts/is-device-darkmode.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function isDeviceDarkmode() {
-	return window.matchMedia('(prefers-color-scheme: dark)').matches;
-}
diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts
deleted file mode 100644
index e28e5725bc..0000000000
--- a/packages/frontend/src/scripts/isFfVisibleForMe.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-import { $i } from '@/account.js';
-
-export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
-	if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
-
-	if (user.followingVisibility === 'private') return false;
-	if (user.followingVisibility === 'followers' && !user.isFollowing) return false;
-
-	return true;
-}
-export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
-	if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
-
-	if (user.followersVisibility === 'private') return false;
-	if (user.followersVisibility === 'followers' && !user.isFollowing) return false;
-
-	return true;
-}
diff --git a/packages/frontend/src/scripts/key-event.ts b/packages/frontend/src/scripts/key-event.ts
deleted file mode 100644
index 020a6c2174..0000000000
--- a/packages/frontend/src/scripts/key-event.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-/**
- * {@link KeyboardEvent.code} の値を表す文字列。不足分は適宜追加する
- * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
- */
-export type KeyCode = (
-	| 'Backspace'
-	| 'Tab'
-	| 'Enter'
-	| 'Shift'
-	| 'Control'
-	| 'Alt'
-	| 'Pause'
-	| 'CapsLock'
-	| 'Escape'
-	| 'Space'
-	| 'PageUp'
-	| 'PageDown'
-	| 'End'
-	| 'Home'
-	| 'ArrowLeft'
-	| 'ArrowUp'
-	| 'ArrowRight'
-	| 'ArrowDown'
-	| 'Insert'
-	| 'Delete'
-	| 'Digit0'
-	| 'Digit1'
-	| 'Digit2'
-	| 'Digit3'
-	| 'Digit4'
-	| 'Digit5'
-	| 'Digit6'
-	| 'Digit7'
-	| 'Digit8'
-	| 'Digit9'
-	| 'KeyA'
-	| 'KeyB'
-	| 'KeyC'
-	| 'KeyD'
-	| 'KeyE'
-	| 'KeyF'
-	| 'KeyG'
-	| 'KeyH'
-	| 'KeyI'
-	| 'KeyJ'
-	| 'KeyK'
-	| 'KeyL'
-	| 'KeyM'
-	| 'KeyN'
-	| 'KeyO'
-	| 'KeyP'
-	| 'KeyQ'
-	| 'KeyR'
-	| 'KeyS'
-	| 'KeyT'
-	| 'KeyU'
-	| 'KeyV'
-	| 'KeyW'
-	| 'KeyX'
-	| 'KeyY'
-	| 'KeyZ'
-	| 'MetaLeft'
-	| 'MetaRight'
-	| 'ContextMenu'
-	| 'F1'
-	| 'F2'
-	| 'F3'
-	| 'F4'
-	| 'F5'
-	| 'F6'
-	| 'F7'
-	| 'F8'
-	| 'F9'
-	| 'F10'
-	| 'F11'
-	| 'F12'
-	| 'NumLock'
-	| 'ScrollLock'
-	| 'Semicolon'
-	| 'Equal'
-	| 'Comma'
-	| 'Minus'
-	| 'Period'
-	| 'Slash'
-	| 'Backquote'
-	| 'BracketLeft'
-	| 'Backslash'
-	| 'BracketRight'
-	| 'Quote'
-	| 'Meta'
-	| 'AltGraph'
-);
-
-/**
- * 修飾キーを表す文字列。不足分は適宜追加する。
- */
-export type KeyModifier = (
-	| 'Shift'
-	| 'Control'
-	| 'Alt'
-	| 'Meta'
-);
-
-/**
- * 押下されたキー以外の状態を表す文字列。不足分は適宜追加する。
- */
-export type KeyState = (
-	| 'composing'
-	| 'repeat'
-);
-
-export type KeyEventHandler = {
-	modifiers?: KeyModifier[];
-	states?: KeyState[];
-	code: KeyCode | 'any';
-	handler: (event: KeyboardEvent) => void;
-};
-
-export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) {
-	function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) {
-		if (modifiers) {
-			return modifiers.every(modifier => ev.getModifierState(modifier));
-		}
-		return true;
-	}
-
-	function checkState(ev: KeyboardEvent, states?: KeyState[]) {
-		if (states) {
-			return states.every(state => ev.getModifierState(state));
-		}
-		return true;
-	}
-
-	let hit = false;
-	for (const handler of handlers.filter(it => it.code === event.code)) {
-		if (checkModifier(event, handler.modifiers) && checkState(event, handler.states)) {
-			handler.handler(event);
-			hit = true;
-			break;
-		}
-	}
-
-	if (!hit) {
-		for (const handler of handlers.filter(it => it.code === 'any')) {
-			handler.handler(event);
-		}
-	}
-}
diff --git a/packages/frontend/src/scripts/langmap.ts b/packages/frontend/src/scripts/langmap.ts
deleted file mode 100644
index b32de15963..0000000000
--- a/packages/frontend/src/scripts/langmap.ts
+++ /dev/null
@@ -1,671 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-// TODO: sharedに置いてバックエンドのと統合したい
-export const langmap = {
-	'ach': {
-		nativeName: 'Lwo',
-	},
-	'ady': {
-		nativeName: 'Адыгэбзэ',
-	},
-	'af': {
-		nativeName: 'Afrikaans',
-	},
-	'af-NA': {
-		nativeName: 'Afrikaans (Namibia)',
-	},
-	'af-ZA': {
-		nativeName: 'Afrikaans (South Africa)',
-	},
-	'ak': {
-		nativeName: 'Tɕɥi',
-	},
-	'ar': {
-		nativeName: 'العربية',
-	},
-	'ar-AR': {
-		nativeName: 'العربية',
-	},
-	'ar-MA': {
-		nativeName: 'العربية',
-	},
-	'ar-SA': {
-		nativeName: 'العربية (السعودية)',
-	},
-	'ay-BO': {
-		nativeName: 'Aymar aru',
-	},
-	'az': {
-		nativeName: 'Azərbaycan dili',
-	},
-	'az-AZ': {
-		nativeName: 'Azərbaycan dili',
-	},
-	'be-BY': {
-		nativeName: 'Беларуская',
-	},
-	'bg': {
-		nativeName: 'Български',
-	},
-	'bg-BG': {
-		nativeName: 'Български',
-	},
-	'bn': {
-		nativeName: 'বাংলা',
-	},
-	'bn-IN': {
-		nativeName: 'বাংলা (ভারত)',
-	},
-	'bn-BD': {
-		nativeName: 'বাংলা(বাংলাদেশ)',
-	},
-	'br': {
-		nativeName: 'Brezhoneg',
-	},
-	'bs-BA': {
-		nativeName: 'Bosanski',
-	},
-	'ca': {
-		nativeName: 'Català',
-	},
-	'ca-ES': {
-		nativeName: 'Català',
-	},
-	'cak': {
-		nativeName: 'Maya Kaqchikel',
-	},
-	'ck-US': {
-		nativeName: 'ᏣᎳᎩ (tsalagi)',
-	},
-	'cs': {
-		nativeName: 'Čeština',
-	},
-	'cs-CZ': {
-		nativeName: 'Čeština',
-	},
-	'cy': {
-		nativeName: 'Cymraeg',
-	},
-	'cy-GB': {
-		nativeName: 'Cymraeg',
-	},
-	'da': {
-		nativeName: 'Dansk',
-	},
-	'da-DK': {
-		nativeName: 'Dansk',
-	},
-	'de': {
-		nativeName: 'Deutsch',
-	},
-	'de-AT': {
-		nativeName: 'Deutsch (Österreich)',
-	},
-	'de-DE': {
-		nativeName: 'Deutsch (Deutschland)',
-	},
-	'de-CH': {
-		nativeName: 'Deutsch (Schweiz)',
-	},
-	'dsb': {
-		nativeName: 'Dolnoserbšćina',
-	},
-	'el': {
-		nativeName: 'Ελληνικά',
-	},
-	'el-GR': {
-		nativeName: 'Ελληνικά',
-	},
-	'en': {
-		nativeName: 'English',
-	},
-	'en-GB': {
-		nativeName: 'English (UK)',
-	},
-	'en-AU': {
-		nativeName: 'English (Australia)',
-	},
-	'en-CA': {
-		nativeName: 'English (Canada)',
-	},
-	'en-IE': {
-		nativeName: 'English (Ireland)',
-	},
-	'en-IN': {
-		nativeName: 'English (India)',
-	},
-	'en-PI': {
-		nativeName: 'English (Pirate)',
-	},
-	'en-SG': {
-		nativeName: 'English (Singapore)',
-	},
-	'en-UD': {
-		nativeName: 'English (Upside Down)',
-	},
-	'en-US': {
-		nativeName: 'English (US)',
-	},
-	'en-ZA': {
-		nativeName: 'English (South Africa)',
-	},
-	'en@pirate': {
-		nativeName: 'English (Pirate)',
-	},
-	'eo': {
-		nativeName: 'Esperanto',
-	},
-	'eo-EO': {
-		nativeName: 'Esperanto',
-	},
-	'es': {
-		nativeName: 'Español',
-	},
-	'es-AR': {
-		nativeName: 'Español (Argentine)',
-	},
-	'es-419': {
-		nativeName: 'Español (Latinoamérica)',
-	},
-	'es-CL': {
-		nativeName: 'Español (Chile)',
-	},
-	'es-CO': {
-		nativeName: 'Español (Colombia)',
-	},
-	'es-EC': {
-		nativeName: 'Español (Ecuador)',
-	},
-	'es-ES': {
-		nativeName: 'Español (España)',
-	},
-	'es-LA': {
-		nativeName: 'Español (Latinoamérica)',
-	},
-	'es-NI': {
-		nativeName: 'Español (Nicaragua)',
-	},
-	'es-MX': {
-		nativeName: 'Español (México)',
-	},
-	'es-US': {
-		nativeName: 'Español (Estados Unidos)',
-	},
-	'es-VE': {
-		nativeName: 'Español (Venezuela)',
-	},
-	'et': {
-		nativeName: 'eesti keel',
-	},
-	'et-EE': {
-		nativeName: 'Eesti (Estonia)',
-	},
-	'eu': {
-		nativeName: 'Euskara',
-	},
-	'eu-ES': {
-		nativeName: 'Euskara',
-	},
-	'fa': {
-		nativeName: 'فارسی',
-	},
-	'fa-IR': {
-		nativeName: 'فارسی',
-	},
-	'fb-LT': {
-		nativeName: 'Leet Speak',
-	},
-	'ff': {
-		nativeName: 'Fulah',
-	},
-	'fi': {
-		nativeName: 'Suomi',
-	},
-	'fi-FI': {
-		nativeName: 'Suomi',
-	},
-	'fo': {
-		nativeName: 'Føroyskt',
-	},
-	'fo-FO': {
-		nativeName: 'Føroyskt (Færeyjar)',
-	},
-	'fr': {
-		nativeName: 'Français',
-	},
-	'fr-CA': {
-		nativeName: 'Français (Canada)',
-	},
-	'fr-FR': {
-		nativeName: 'Français (France)',
-	},
-	'fr-BE': {
-		nativeName: 'Français (Belgique)',
-	},
-	'fr-CH': {
-		nativeName: 'Français (Suisse)',
-	},
-	'fy-NL': {
-		nativeName: 'Frysk',
-	},
-	'ga': {
-		nativeName: 'Gaeilge',
-	},
-	'ga-IE': {
-		nativeName: 'Gaeilge',
-	},
-	'gd': {
-		nativeName: 'Gàidhlig',
-	},
-	'gl': {
-		nativeName: 'Galego',
-	},
-	'gl-ES': {
-		nativeName: 'Galego',
-	},
-	'gn-PY': {
-		nativeName: 'Avañe\'ẽ',
-	},
-	'gu-IN': {
-		nativeName: 'ગુજરાતી',
-	},
-	'gv': {
-		nativeName: 'Gaelg',
-	},
-	'gx-GR': {
-		nativeName: 'Ἑλληνική ἀρχαία',
-	},
-	'he': {
-		nativeName: 'עברית‏',
-	},
-	'he-IL': {
-		nativeName: 'עברית‏',
-	},
-	'hi': {
-		nativeName: 'हिन्दी',
-	},
-	'hi-IN': {
-		nativeName: 'हिन्दी',
-	},
-	'hr': {
-		nativeName: 'Hrvatski',
-	},
-	'hr-HR': {
-		nativeName: 'Hrvatski',
-	},
-	'hsb': {
-		nativeName: 'Hornjoserbšćina',
-	},
-	'ht': {
-		nativeName: 'Kreyòl',
-	},
-	'hu': {
-		nativeName: 'Magyar',
-	},
-	'hu-HU': {
-		nativeName: 'Magyar',
-	},
-	'hy': {
-		nativeName: 'Հայերեն',
-	},
-	'hy-AM': {
-		nativeName: 'Հայերեն (Հայաստան)',
-	},
-	'id': {
-		nativeName: 'Bahasa Indonesia',
-	},
-	'id-ID': {
-		nativeName: 'Bahasa Indonesia',
-	},
-	'is': {
-		nativeName: 'Íslenska',
-	},
-	'is-IS': {
-		nativeName: 'Íslenska (Iceland)',
-	},
-	'it': {
-		nativeName: 'Italiano',
-	},
-	'it-IT': {
-		nativeName: 'Italiano',
-	},
-	'ja': {
-		nativeName: '日本語',
-	},
-	'ja-JP': {
-		nativeName: '日本語 (日本)',
-	},
-	'jv-ID': {
-		nativeName: 'Basa Jawa',
-	},
-	'ka-GE': {
-		nativeName: 'ქართული',
-	},
-	'kk-KZ': {
-		nativeName: 'Қазақша',
-	},
-	'km': {
-		nativeName: 'ភាសាខ្មែរ',
-	},
-	'kl': {
-		nativeName: 'kalaallisut',
-	},
-	'km-KH': {
-		nativeName: 'ភាសាខ្មែរ',
-	},
-	'kab': {
-		nativeName: 'Taqbaylit',
-	},
-	'kn': {
-		nativeName: 'ಕನ್ನಡ',
-	},
-	'kn-IN': {
-		nativeName: 'ಕನ್ನಡ (India)',
-	},
-	'ko': {
-		nativeName: '한국어',
-	},
-	'ko-KR': {
-		nativeName: '한국어 (한국)',
-	},
-	'ku-TR': {
-		nativeName: 'Kurdî',
-	},
-	'kw': {
-		nativeName: 'Kernewek',
-	},
-	'la': {
-		nativeName: 'Latin',
-	},
-	'la-VA': {
-		nativeName: 'Latin',
-	},
-	'lb': {
-		nativeName: 'Lëtzebuergesch',
-	},
-	'li-NL': {
-		nativeName: 'Lèmbörgs',
-	},
-	'lt': {
-		nativeName: 'Lietuvių',
-	},
-	'lt-LT': {
-		nativeName: 'Lietuvių',
-	},
-	'lv': {
-		nativeName: 'Latviešu',
-	},
-	'lv-LV': {
-		nativeName: 'Latviešu',
-	},
-	'mai': {
-		nativeName: 'मैथिली, মৈথিলী',
-	},
-	'mg-MG': {
-		nativeName: 'Malagasy',
-	},
-	'mk': {
-		nativeName: 'Македонски',
-	},
-	'mk-MK': {
-		nativeName: 'Македонски (Македонски)',
-	},
-	'ml': {
-		nativeName: 'മലയാളം',
-	},
-	'ml-IN': {
-		nativeName: 'മലയാളം',
-	},
-	'mn-MN': {
-		nativeName: 'Монгол',
-	},
-	'mr': {
-		nativeName: 'मराठी',
-	},
-	'mr-IN': {
-		nativeName: 'मराठी',
-	},
-	'ms': {
-		nativeName: 'Bahasa Melayu',
-	},
-	'ms-MY': {
-		nativeName: 'Bahasa Melayu',
-	},
-	'mt': {
-		nativeName: 'Malti',
-	},
-	'mt-MT': {
-		nativeName: 'Malti',
-	},
-	'my': {
-		nativeName: 'ဗမာစကာ',
-	},
-	'no': {
-		nativeName: 'Norsk',
-	},
-	'nb': {
-		nativeName: 'Norsk (bokmål)',
-	},
-	'nb-NO': {
-		nativeName: 'Norsk (bokmål)',
-	},
-	'ne': {
-		nativeName: 'नेपाली',
-	},
-	'ne-NP': {
-		nativeName: 'नेपाली',
-	},
-	'nl': {
-		nativeName: 'Nederlands',
-	},
-	'nl-BE': {
-		nativeName: 'Nederlands (België)',
-	},
-	'nl-NL': {
-		nativeName: 'Nederlands (Nederland)',
-	},
-	'nn-NO': {
-		nativeName: 'Norsk (nynorsk)',
-	},
-	'oc': {
-		nativeName: 'Occitan',
-	},
-	'or-IN': {
-		nativeName: 'ଓଡ଼ିଆ',
-	},
-	'pa': {
-		nativeName: 'ਪੰਜਾਬੀ',
-	},
-	'pa-IN': {
-		nativeName: 'ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)',
-	},
-	'pl': {
-		nativeName: 'Polski',
-	},
-	'pl-PL': {
-		nativeName: 'Polski',
-	},
-	'ps-AF': {
-		nativeName: 'پښتو',
-	},
-	'pt': {
-		nativeName: 'Português',
-	},
-	'pt-BR': {
-		nativeName: 'Português (Brasil)',
-	},
-	'pt-PT': {
-		nativeName: 'Português (Portugal)',
-	},
-	'qu-PE': {
-		nativeName: 'Qhichwa',
-	},
-	'rm-CH': {
-		nativeName: 'Rumantsch',
-	},
-	'ro': {
-		nativeName: 'Română',
-	},
-	'ro-RO': {
-		nativeName: 'Română',
-	},
-	'ru': {
-		nativeName: 'Русский',
-	},
-	'ru-RU': {
-		nativeName: 'Русский',
-	},
-	'sa-IN': {
-		nativeName: 'संस्कृतम्',
-	},
-	'se-NO': {
-		nativeName: 'Davvisámegiella',
-	},
-	'sh': {
-		nativeName: 'српскохрватски',
-	},
-	'si-LK': {
-		nativeName: 'සිංහල',
-	},
-	'sk': {
-		nativeName: 'Slovenčina',
-	},
-	'sk-SK': {
-		nativeName: 'Slovenčina (Slovakia)',
-	},
-	'sl': {
-		nativeName: 'Slovenščina',
-	},
-	'sl-SI': {
-		nativeName: 'Slovenščina',
-	},
-	'so-SO': {
-		nativeName: 'Soomaaliga',
-	},
-	'sq': {
-		nativeName: 'Shqip',
-	},
-	'sq-AL': {
-		nativeName: 'Shqip',
-	},
-	'sr': {
-		nativeName: 'Српски',
-	},
-	'sr-RS': {
-		nativeName: 'Српски (Serbia)',
-	},
-	'su': {
-		nativeName: 'Basa Sunda',
-	},
-	'sv': {
-		nativeName: 'Svenska',
-	},
-	'sv-SE': {
-		nativeName: 'Svenska',
-	},
-	'sw': {
-		nativeName: 'Kiswahili',
-	},
-	'sw-KE': {
-		nativeName: 'Kiswahili',
-	},
-	'ta': {
-		nativeName: 'தமிழ்',
-	},
-	'ta-IN': {
-		nativeName: 'தமிழ்',
-	},
-	'te': {
-		nativeName: 'తెలుగు',
-	},
-	'te-IN': {
-		nativeName: 'తెలుగు',
-	},
-	'tg': {
-		nativeName: 'забо́ни тоҷикӣ́',
-	},
-	'tg-TJ': {
-		nativeName: 'тоҷикӣ',
-	},
-	'th': {
-		nativeName: 'ภาษาไทย',
-	},
-	'th-TH': {
-		nativeName: 'ภาษาไทย (ประเทศไทย)',
-	},
-	'fil': {
-		nativeName: 'Filipino',
-	},
-	'tlh': {
-		nativeName: 'tlhIngan-Hol',
-	},
-	'tr': {
-		nativeName: 'Türkçe',
-	},
-	'tr-TR': {
-		nativeName: 'Türkçe',
-	},
-	'tt-RU': {
-		nativeName: 'татарча',
-	},
-	'uk': {
-		nativeName: 'Українська',
-	},
-	'uk-UA': {
-		nativeName: 'Українська',
-	},
-	'ur': {
-		nativeName: 'اردو',
-	},
-	'ur-PK': {
-		nativeName: 'اردو',
-	},
-	'uz': {
-		nativeName: 'O\'zbek',
-	},
-	'uz-UZ': {
-		nativeName: 'O\'zbek',
-	},
-	'vi': {
-		nativeName: 'Tiếng Việt',
-	},
-	'vi-VN': {
-		nativeName: 'Tiếng Việt',
-	},
-	'xh-ZA': {
-		nativeName: 'isiXhosa',
-	},
-	'yi': {
-		nativeName: 'ייִדיש',
-	},
-	'yi-DE': {
-		nativeName: 'ייִדיש (German)',
-	},
-	'zh': {
-		nativeName: '中文',
-	},
-	'zh-Hans': {
-		nativeName: '中文简体',
-	},
-	'zh-Hant': {
-		nativeName: '中文繁體',
-	},
-	'zh-CN': {
-		nativeName: '中文（中国大陆）',
-	},
-	'zh-HK': {
-		nativeName: '中文（香港）',
-	},
-	'zh-SG': {
-		nativeName: '中文（新加坡）',
-	},
-	'zh-TW': {
-		nativeName: '中文（台灣）',
-	},
-	'zu-ZA': {
-		nativeName: 'isiZulu',
-	},
-};
diff --git a/packages/frontend/src/scripts/login-id.ts b/packages/frontend/src/scripts/login-id.ts
deleted file mode 100644
index b52735caa0..0000000000
--- a/packages/frontend/src/scripts/login-id.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function getUrlWithLoginId(url: string, loginId: string) {
-	const u = new URL(url, origin);
-	u.searchParams.append('loginId', loginId);
-	return u.toString();
-}
-
-export function getUrlWithoutLoginId(url: string) {
-	const u = new URL(url);
-	u.searchParams.delete('loginId');
-	return u.toString();
-}
diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts
deleted file mode 100644
index 02f589c7ca..0000000000
--- a/packages/frontend/src/scripts/lookup.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { i18n } from '@/i18n.js';
-import { Router } from '@/nirax.js';
-import { mainRouter } from '@/router/main.js';
-
-export async function lookup(router?: Router) {
-	const _router = router ?? mainRouter;
-
-	const { canceled, result: temp } = await os.inputText({
-		title: i18n.ts.lookup,
-	});
-	const query = temp ? temp.trim() : '';
-	if (canceled || query.length <= 1) return;
-
-	if (query.startsWith('@') && !query.includes(' ')) {
-		_router.push(`/${query}`);
-		return;
-	}
-
-	if (query.startsWith('#')) {
-		_router.push(`/tags/${encodeURIComponent(query.substring(1))}`);
-		return;
-	}
-
-	if (query.startsWith('https://')) {
-		const res = await apLookup(query);
-
-		if (res.type === 'User') {
-			_router.push(`/@${res.object.username}@${res.object.host}`);
-		} else if (res.type === 'Note') {
-			_router.push(`/notes/${res.object.id}`);
-		}
-
-		return;
-	}
-}
-
-export async function apLookup(query: string) {
-	const promise = misskeyApi('ap/show', {
-		uri: query,
-	});
-
-	os.promiseDialog(promise, null, (err) => {
-		let title = i18n.ts.somethingHappened;
-		let text = err.message + '\n' + err.id;
-
-		switch (err.id) {
-			case '974b799e-1a29-4889-b706-18d4dd93e266':
-				title = i18n.ts._remoteLookupErrors._federationNotAllowed.title;
-				text = i18n.ts._remoteLookupErrors._federationNotAllowed.description;
-				break;
-			case '1a5eab56-e47b-48c2-8d5e-217b897d70db':
-				title = i18n.ts._remoteLookupErrors._uriInvalid.title;
-				text = i18n.ts._remoteLookupErrors._uriInvalid.description;
-				break;
-			case '81b539cf-4f57-4b29-bc98-032c33c0792e':
-				title = i18n.ts._remoteLookupErrors._requestFailed.title;
-				text = i18n.ts._remoteLookupErrors._requestFailed.description;
-				break;
-			case '70193c39-54f3-4813-82f0-70a680f7495b':
-				title = i18n.ts._remoteLookupErrors._responseInvalid.title;
-				text = i18n.ts._remoteLookupErrors._responseInvalid.description;
-				break;
-			case 'dc94d745-1262-4e63-a17d-fecaa57efc82':
-				title = i18n.ts._remoteLookupErrors._noSuchObject.title;
-				text = i18n.ts._remoteLookupErrors._noSuchObject.description;
-				break;
-		}
-
-		os.alert({
-			type: 'error',
-			title,
-			text,
-		});
-	}, i18n.ts.fetchingAsApObject);
-
-	return await promise;
-}
diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/scripts/media-has-audio.ts
deleted file mode 100644
index 4bf3ee5d97..0000000000
--- a/packages/frontend/src/scripts/media-has-audio.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export default async function hasAudio(media: HTMLMediaElement) {
-	const cloned = media.cloneNode() as HTMLMediaElement;
-	cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true;
-	cloned.play();
-	await new Promise((resolve) => cloned.addEventListener('playing', resolve));
-	const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount;
-	cloned.remove();
-	return result;
-}
diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts
deleted file mode 100644
index 78eba35ead..0000000000
--- a/packages/frontend/src/scripts/media-proxy.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { MediaProxy } from '@@/js/media-proxy.js';
-import { url } from '@@/js/config.js';
-import { instance } from '@/instance.js';
-
-let _mediaProxy: MediaProxy | null = null;
-
-export function getProxiedImageUrl(...args: Parameters<MediaProxy['getProxiedImageUrl']>): string {
-	if (_mediaProxy == null) {
-		_mediaProxy = new MediaProxy(instance, url);
-	}
-
-	return _mediaProxy.getProxiedImageUrl(...args);
-}
-
-export function getProxiedImageUrlNullable(...args: Parameters<MediaProxy['getProxiedImageUrlNullable']>): string | null {
-	if (_mediaProxy == null) {
-		_mediaProxy = new MediaProxy(instance, url);
-	}
-
-	return _mediaProxy.getProxiedImageUrlNullable(...args);
-}
-
-export function getStaticImageUrl(...args: Parameters<MediaProxy['getStaticImageUrl']>): string {
-	if (_mediaProxy == null) {
-		_mediaProxy = new MediaProxy(instance, url);
-	}
-
-	return _mediaProxy.getStaticImageUrl(...args);
-}
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts
deleted file mode 100644
index 004b6d42a4..0000000000
--- a/packages/frontend/src/scripts/merge.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { deepClone } from './clone.js';
-import type { Cloneable } from './clone.js';
-
-export type DeepPartial<T> = {
-	[P in keyof T]?: T[P] extends Record<PropertyKey, unknown> ? DeepPartial<T[P]> : T[P];
-};
-
-function isPureObject(value: unknown): value is Record<PropertyKey, unknown> {
-	return typeof value === 'object' && value !== null && !Array.isArray(value);
-}
-
-/**
- * valueにないキーをdefからもらう（再帰的）\
- * nullはそのまま、undefinedはdefの値
- **/
-export function deepMerge<X extends Record<PropertyKey, unknown>>(value: DeepPartial<X>, def: X): X {
-	if (isPureObject(value) && isPureObject(def)) {
-		const result = deepClone(value as Cloneable) as X;
-		for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
-			if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
-				result[k] = v;
-			} else if (isPureObject(v) && isPureObject(result[k])) {
-				const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<PropertyKey, unknown>>;
-				result[k] = deepMerge<typeof v>(child, v);
-			}
-		}
-		return result;
-	}
-	throw new Error('deepMerge: value and def must be pure objects');
-}
diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts
deleted file mode 100644
index a2f777f623..0000000000
--- a/packages/frontend/src/scripts/mfm-function-picker.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { nextTick } from 'vue';
-import type { Ref } from 'vue';
-import * as os from '@/os.js';
-import { i18n } from '@/i18n.js';
-import { MFM_TAGS } from '@@/js/const.js';
-import type { MenuItem } from '@/types/menu.js';
-
-/**
- * MFMの装飾のリストを表示する
- */
-export function mfmFunctionPicker(src: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
-	os.popupMenu([{
-		text: i18n.ts.addMfmFunction,
-		type: 'label',
-	}, ...getFunctionList(textArea, textRef)], src);
-}
-
-function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>): MenuItem[] {
-	return MFM_TAGS.map(tag => ({
-		text: tag,
-		icon: 'ti ti-icons',
-		action: () => add(textArea, textRef, tag),
-	}));
-}
-
-function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, type: string) {
-	const caretStart: number = textArea.selectionStart as number;
-	const caretEnd: number = textArea.selectionEnd as number;
-
-	MFM_TAGS.forEach(tag => {
-		if (type === tag) {
-			if (caretStart === caretEnd) {
-				// 単純にFunctionを追加
-				const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ]${textRef.value.substring(caretEnd)}`;
-				textRef.value = trimmedText;
-			} else {
-				// 選択範囲を囲むようにFunctionを追加
-				const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ${textRef.value.substring(caretStart, caretEnd)}]${textRef.value.substring(caretEnd)}`;
-				textRef.value = trimmedText;
-			}
-		}
-	});
-
-	const nextCaretStart: number = caretStart + 3 + type.length;
-	const nextCaretEnd: number = caretEnd + 3 + type.length;
-
-	// キャレットを戻す
-	nextTick(() => {
-		textArea.focus();
-		textArea.setSelectionRange(nextCaretStart, nextCaretEnd);
-	});
-}
diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts
deleted file mode 100644
index dc07ad477b..0000000000
--- a/packages/frontend/src/scripts/misskey-api.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-import { ref } from 'vue';
-import { apiUrl } from '@@/js/config.js';
-import { $i } from '@/account.js';
-export const pendingApiRequestsCount = ref(0);
-
-export type Endpoint = keyof Misskey.Endpoints;
-
-export type Request<E extends Endpoint> = Misskey.Endpoints[E]['req'];
-
-export type AnyRequest<E extends Endpoint | (string & unknown)> =
-	(E extends Endpoint ? Request<E> : never) | object;
-
-export type Response<E extends Endpoint | (string & unknown), P extends AnyRequest<E>> =
-	E extends Endpoint
-	? P extends Request<E> ? Misskey.api.SwitchCaseResponseType<E, P> : never
-	: object;
-
-// Implements Misskey.api.ApiClient.request
-export function misskeyApi<
-	ResT = void,
-	E extends Endpoint | NonNullable<string> = Endpoint,
-	P extends AnyRequest<E> = E extends Endpoint ? Request<E> : never,
-	_ResT = ResT extends void ? Response<E, P> : ResT,
->(
-	endpoint: E,
-	data: P & { i?: string | null; } = {} as any,
-	token?: string | null | undefined,
-	signal?: AbortSignal,
-): Promise<_ResT> {
-	if (endpoint.includes('://')) throw new Error('invalid endpoint');
-	pendingApiRequestsCount.value++;
-
-	const onFinally = () => {
-		pendingApiRequestsCount.value--;
-	};
-
-	const promise = new Promise<_ResT>((resolve, reject) => {
-		// Append a credential
-		if ($i) data.i = $i.token;
-		if (token !== undefined) data.i = token;
-
-		// Send request
-		window.fetch(`${apiUrl}/${endpoint}`, {
-			method: 'POST',
-			body: JSON.stringify(data),
-			credentials: 'omit',
-			cache: 'no-cache',
-			headers: {
-				'Content-Type': 'application/json',
-			},
-			signal,
-		}).then(async (res) => {
-			const body = res.status === 204 ? null : await res.json();
-
-			if (res.status === 200) {
-				resolve(body);
-			} else if (res.status === 204) {
-				resolve(undefined as _ResT); // void -> undefined
-			} else {
-				reject(body.error);
-			}
-		}).catch(reject);
-	});
-
-	promise.then(onFinally, onFinally);
-
-	return promise;
-}
-
-// Implements Misskey.api.ApiClient.request
-export function misskeyApiGet<
-	ResT = void,
-	E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
-	P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
-	_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
->(
-	endpoint: E,
-	data: P = {} as any,
-): Promise<_ResT> {
-	pendingApiRequestsCount.value++;
-
-	const onFinally = () => {
-		pendingApiRequestsCount.value--;
-	};
-
-	const query = new URLSearchParams(data as any);
-
-	const promise = new Promise<_ResT>((resolve, reject) => {
-		// Send request
-		window.fetch(`${apiUrl}/${endpoint}?${query}`, {
-			method: 'GET',
-			credentials: 'omit',
-			cache: 'default',
-		}).then(async (res) => {
-			const body = res.status === 204 ? null : await res.json();
-
-			if (res.status === 200) {
-				resolve(body);
-			} else if (res.status === 204) {
-				resolve(undefined as _ResT); // void -> undefined
-			} else {
-				reject(body.error);
-			}
-		}).catch(reject);
-	});
-
-	promise.then(onFinally, onFinally);
-
-	return promise;
-}
diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/scripts/navigator.ts
deleted file mode 100644
index ffc0a457f4..0000000000
--- a/packages/frontend/src/scripts/navigator.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function isSupportShare(): boolean {
-	return 'share' in navigator;
-}
diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts
deleted file mode 100644
index 671751147c..0000000000
--- a/packages/frontend/src/scripts/page-metadata.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-import { inject, isRef, onActivated, onBeforeUnmount, provide, ref, toValue, watch } from 'vue';
-import type { MaybeRefOrGetter, Ref } from 'vue';
-
-export type PageMetadata = {
-	title: string;
-	subtitle?: string;
-	icon?: string | null;
-	avatar?: Misskey.entities.User | null;
-	userName?: Misskey.entities.User | null;
-	needWideArea?: boolean;
-};
-
-type PageMetadataGetter = () => PageMetadata;
-type PageMetadataReceiver = (getter: PageMetadataGetter) => void;
-
-const RECEIVER_KEY = Symbol('ReceiverKey');
-const setReceiver = (v: PageMetadataReceiver): void => {
-	provide<PageMetadataReceiver>(RECEIVER_KEY, v);
-};
-const getReceiver = (): PageMetadataReceiver | undefined => {
-	return inject<PageMetadataReceiver>(RECEIVER_KEY);
-};
-
-const METADATA_KEY = Symbol('MetadataKey');
-const setMetadata = (v: Ref<PageMetadata | null>): void => {
-	provide<Ref<PageMetadata | null>>(METADATA_KEY, v);
-};
-const getMetadata = (): Ref<PageMetadata | null> | undefined => {
-	return inject<Ref<PageMetadata | null>>(METADATA_KEY);
-};
-
-export const definePageMetadata = (maybeRefOrGetterMetadata: MaybeRefOrGetter<PageMetadata>): void => {
-	const metadataRef = ref(toValue(maybeRefOrGetterMetadata));
-	const metadataGetter = () => metadataRef.value;
-	const receiver = getReceiver();
-
-	// setup handler
-	receiver?.(metadataGetter);
-
-	// update handler
-	onBeforeUnmount(watch(
-		() => toValue(maybeRefOrGetterMetadata),
-		(metadata) => {
-			metadataRef.value = metadata;
-			receiver?.(metadataGetter);
-		},
-		{ deep: true },
-	));
-	onActivated(() => {
-		receiver?.(metadataGetter);
-	});
-};
-
-export const provideMetadataReceiver = (receiver: PageMetadataReceiver): void => {
-	setReceiver(receiver);
-};
-
-export const provideReactiveMetadata = (metadataRef: Ref<PageMetadata | null>): void => {
-	setMetadata(metadataRef);
-};
-
-export const injectReactiveMetadata = (): Ref<PageMetadata | null> => {
-	const metadataRef = getMetadata();
-	return isRef(metadataRef) ? metadataRef : ref(null);
-};
diff --git a/packages/frontend/src/scripts/physics.ts b/packages/frontend/src/scripts/physics.ts
deleted file mode 100644
index 8a4e9319b3..0000000000
--- a/packages/frontend/src/scripts/physics.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Matter from 'matter-js';
-
-export function physics(container: HTMLElement) {
-	const containerWidth = container.offsetWidth;
-	const containerHeight = container.offsetHeight;
-	const containerCenterX = containerWidth / 2;
-
-	// サイズ固定化(要らないかも？)
-	container.style.position = 'relative';
-	container.style.boxSizing = 'border-box';
-	container.style.width = `${containerWidth}px`;
-	container.style.height = `${containerHeight}px`;
-
-	// create engine
-	const engine = Matter.Engine.create({
-		constraintIterations: 4,
-		positionIterations: 8,
-		velocityIterations: 8,
-	});
-
-	const world = engine.world;
-
-	// create renderer
-	const render = Matter.Render.create({
-		engine: engine,
-		//element: document.getElementById('debug'),
-		options: {
-			width: containerWidth,
-			height: containerHeight,
-			background: 'transparent', // transparent to hide
-			wireframeBackground: 'transparent', // transparent to hide
-		},
-	});
-
-	// Disable to hide debug
-	Matter.Render.run(render);
-
-	// create runner
-	const runner = Matter.Runner.create();
-	Matter.Runner.run(runner, engine);
-
-	const groundThickness = 1024;
-	const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, {
-		isStatic: true,
-		restitution: 0.1,
-		friction: 2,
-	});
-
-	//const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts);
-	//const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts);
-
-	Matter.World.add(world, [
-		ground,
-		//wallRight,
-		//wallLeft,
-	]);
-
-	const objEls = Array.from(container.children) as HTMLElement[];
-	const objs: Matter.Body[] = [];
-	for (const objEl of objEls) {
-		const left = objEl.dataset.physicsX ? parseInt(objEl.dataset.physicsX) : objEl.offsetLeft;
-		const top = objEl.dataset.physicsY ? parseInt(objEl.dataset.physicsY) : objEl.offsetTop;
-
-		let obj: Matter.Body;
-		if (objEl.classList.contains('_physics_circle_')) {
-			obj = Matter.Bodies.circle(
-				left + (objEl.offsetWidth / 2),
-				top + (objEl.offsetHeight / 2),
-				Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2,
-				{
-					restitution: 0.5,
-				},
-			);
-		} else {
-			const style = window.getComputedStyle(objEl);
-			obj = Matter.Bodies.rectangle(
-				left + (objEl.offsetWidth / 2),
-				top + (objEl.offsetHeight / 2),
-				objEl.offsetWidth,
-				objEl.offsetHeight,
-				{
-					chamfer: { radius: parseInt(style.borderRadius || '0', 10) },
-					restitution: 0.5,
-				},
-			);
-		}
-		objEl.id = obj.id.toString();
-		objs.push(obj);
-	}
-
-	Matter.World.add(engine.world, objs);
-
-	// Add mouse control
-
-	const mouse = Matter.Mouse.create(container);
-	const mouseConstraint = Matter.MouseConstraint.create(engine, {
-		mouse: mouse,
-		constraint: {
-			stiffness: 0.1,
-			render: {
-				visible: false,
-			},
-		},
-	});
-
-	Matter.World.add(engine.world, mouseConstraint);
-
-	// keep the mouse in sync with rendering
-	render.mouse = mouse;
-
-	for (const objEl of objEls) {
-		objEl.style.position = 'absolute';
-		objEl.style.top = '0';
-		objEl.style.left = '0';
-		objEl.style.margin = '0';
-	}
-
-	window.requestAnimationFrame(update);
-
-	let stop = false;
-
-	function update() {
-		for (const objEl of objEls) {
-			const obj = objs.find(obj => obj.id.toString() === objEl.id.toString());
-			if (obj == null) continue;
-
-			const x = (obj.position.x - objEl.offsetWidth / 2);
-			const y = (obj.position.y - objEl.offsetHeight / 2);
-			const angle = obj.angle;
-			objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`;
-		}
-
-		if (!stop) {
-			window.requestAnimationFrame(update);
-		}
-	}
-
-	// 奈落に落ちたオブジェクトは消す
-	const intervalId = window.setInterval(() => {
-		for (const obj of objs) {
-			if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj);
-		}
-	}, 1000 * 10);
-
-	return {
-		stop: () => {
-			stop = true;
-			Matter.Runner.stop(runner);
-			window.clearInterval(intervalId);
-		},
-	};
-}
diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts
deleted file mode 100644
index 39c6df6500..0000000000
--- a/packages/frontend/src/scripts/player-url-transform.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-import { hostname } from '@@/js/config.js';
-
-export function transformPlayerUrl(url: string): string {
-	const urlObj = new URL(url);
-	if (!['https:', 'http:'].includes(urlObj.protocol)) throw new Error('Invalid protocol');
-
-	const urlParams = new URLSearchParams(urlObj.search);
-
-	if (urlObj.hostname === 'player.twitch.tv') {
-		// TwitchはCSPの制約あり
-		// https://dev.twitch.tv/docs/embed/video-and-clips/
-		urlParams.set('parent', hostname);
-		urlParams.set('allowfullscreen', '');
-		urlParams.set('autoplay', 'true');
-	} else {
-		urlParams.set('autoplay', '1');
-		urlParams.set('auto_play', '1');
-	}
-	urlObj.search = urlParams.toString();
-
-	return urlObj.toString();
-}
diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts
deleted file mode 100644
index a8a330eb6d..0000000000
--- a/packages/frontend/src/scripts/please-login.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { defineAsyncComponent } from 'vue';
-import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
-import { i18n } from '@/i18n.js';
-import { popup } from '@/os.js';
-
-export type OpenOnRemoteOptions = {
-	/**
-	 * 外部のMisskey Webで特定のパスを開く
-	 */
-	type: 'web';
-
-	/**
-	 * 内部パス（例: `/settings`）
-	 */
-	path: string;
-} | {
-	/**
-	 * 外部のMisskey Webで照会する
-	 */
-	type: 'lookup';
-
-	/**
-	 * 照会したいエンティティのURL
-	 *
-	 * （例: `https://misskey.example.com/notes/abcdexxxxyz`）
-	 */
-	url: string;
-} | {
-	/**
-	 * 外部のMisskeyでノートする
-	 */
-	type: 'share';
-
-	/**
-	 * `/share` ページに渡すクエリストリング
-	 *
-	 * @see https://go.misskey-hub.net/spec/share/
-	 */
-	params: Record<string, string>;
-};
-
-export function pleaseLogin(opts: {
-	path?: string;
-	message?: string;
-	openOnRemote?: OpenOnRemoteOptions;
-} = {}) {
-	if ($i) return;
-
-	let _openOnRemote: OpenOnRemoteOptions | undefined = undefined;
-
-	// 連合できる場合と、（連合ができなくても）共有する場合は外部連携オプションを設定
-	if (opts.openOnRemote != null && (instance.federation !== 'none' || opts.openOnRemote.type === 'share')) {
-		_openOnRemote = opts.openOnRemote;
-	}
-
-	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
-		autoSet: true,
-		message: opts.message ?? (_openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
-		openOnRemote: _openOnRemote,
-	}, {
-		cancelled: () => {
-			if (opts.path) {
-				window.location.href = opts.path;
-			}
-		},
-		closed: () => dispose(),
-	});
-
-	throw new Error('signin required');
-}
diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts
deleted file mode 100644
index 5b141222e8..0000000000
--- a/packages/frontend/src/scripts/popout.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { appendQuery } from '@@/js/url.js';
-import * as config from '@@/js/config.js';
-
-export function popout(path: string, w?: HTMLElement) {
-	let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
-	url = appendQuery(url, 'zen');
-	if (w) {
-		const position = w.getBoundingClientRect();
-		const width = parseInt(getComputedStyle(w, '').width, 10);
-		const height = parseInt(getComputedStyle(w, '').height, 10);
-		const x = window.screenX + position.left;
-		const y = window.screenY + position.top;
-		window.open(url, url,
-			`width=${width}, height=${height}, top=${y}, left=${x}`);
-	} else {
-		const width = 400;
-		const height = 500;
-		const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2);
-		const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2);
-		window.open(url, url,
-			`width=${width}, height=${height}, top=${x}, left=${y}`);
-	}
-}
diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts
deleted file mode 100644
index 3dad41a8b3..0000000000
--- a/packages/frontend/src/scripts/popup-position.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function calcPopupPosition(el: HTMLElement, props: {
-	anchorElement?: HTMLElement | null;
-	innerMargin: number;
-	direction: 'top' | 'bottom' | 'left' | 'right';
-	align: 'top' | 'bottom' | 'left' | 'right' | 'center';
-	alignOffset?: number;
-	x?: number;
-	y?: number;
-}): { top: number; left: number; transformOrigin: string; } {
-	const contentWidth = el.offsetWidth;
-	const contentHeight = el.offsetHeight;
-
-	let rect: DOMRect;
-
-	if (props.anchorElement) {
-		rect = props.anchorElement.getBoundingClientRect();
-	}
-
-	const calcPosWhenTop = () => {
-		let left: number;
-		let top: number;
-
-		if (props.anchorElement) {
-			left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
-			top = (rect.top + window.scrollY - contentHeight) - props.innerMargin;
-		} else {
-			left = props.x;
-			top = (props.y - contentHeight) - props.innerMargin;
-		}
-
-		left -= (el.offsetWidth / 2);
-
-		if (left + contentWidth - window.scrollX > window.innerWidth) {
-			left = window.innerWidth - contentWidth + window.scrollX - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenBottom = () => {
-		let left: number;
-		let top: number;
-
-		if (props.anchorElement) {
-			left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
-			top = (rect.top + window.scrollY + props.anchorElement.offsetHeight) + props.innerMargin;
-		} else {
-			left = props.x;
-			top = (props.y) + props.innerMargin;
-		}
-
-		left -= (el.offsetWidth / 2);
-
-		if (left + contentWidth - window.scrollX > window.innerWidth) {
-			left = window.innerWidth - contentWidth + window.scrollX - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenLeft = () => {
-		let left: number;
-		let top: number;
-
-		if (props.anchorElement) {
-			left = (rect.left + window.scrollX - contentWidth) - props.innerMargin;
-			top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
-		} else {
-			left = (props.x - contentWidth) - props.innerMargin;
-			top = props.y;
-		}
-
-		top -= (el.offsetHeight / 2);
-
-		if (top + contentHeight - window.scrollY > window.innerHeight) {
-			top = window.innerHeight - contentHeight + window.scrollY - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calcPosWhenRight = () => {
-		let left: number;
-		let top: number;
-
-		if (props.anchorElement) {
-			left = (rect.left + props.anchorElement.offsetWidth + window.scrollX) + props.innerMargin;
-
-			if (props.align === 'top') {
-				top = rect.top + window.scrollY;
-				if (props.alignOffset != null) top += props.alignOffset;
-			} else if (props.align === 'bottom') {
-				// TODO
-			} else { // center
-				top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
-				top -= (el.offsetHeight / 2);
-			}
-		} else {
-			left = props.x + props.innerMargin;
-			top = props.y;
-			top -= (el.offsetHeight / 2);
-		}
-
-		if (top + contentHeight - window.scrollY > window.innerHeight) {
-			top = window.innerHeight - contentHeight + window.scrollY - 1;
-		}
-
-		return [left, top];
-	};
-
-	const calc = (): {
-		left: number;
-		top: number;
-		transformOrigin: string;
-	} => {
-		switch (props.direction) {
-			case 'top': {
-				const [left, top] = calcPosWhenTop();
-
-				// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
-				if (top - window.scrollY < 0) {
-					const [left, top] = calcPosWhenBottom();
-					return { left, top, transformOrigin: 'center top' };
-				}
-
-				return { left, top, transformOrigin: 'center bottom' };
-			}
-
-			case 'bottom': {
-				const [left, top] = calcPosWhenBottom();
-				// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
-				return { left, top, transformOrigin: 'center top' };
-			}
-
-			case 'left': {
-				const [left, top] = calcPosWhenLeft();
-
-				// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
-				if (left - window.scrollX < 0) {
-					const [left, top] = calcPosWhenRight();
-					return { left, top, transformOrigin: 'left center' };
-				}
-
-				return { left, top, transformOrigin: 'right center' };
-			}
-
-			case 'right': {
-				const [left, top] = calcPosWhenRight();
-				// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
-				return { left, top, transformOrigin: 'left center' };
-			}
-		}
-	};
-
-	return calc();
-}
diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts
deleted file mode 100644
index 11b6f52ddd..0000000000
--- a/packages/frontend/src/scripts/post-message.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export const postMessageEventTypes = [
-	'misskey:shareForm:shareCompleted',
-] as const;
-
-export type PostMessageEventType = typeof postMessageEventTypes[number];
-
-export type MiPostMessageEvent = {
-	type: PostMessageEventType;
-	payload?: any;
-};
-
-/**
- * 親フレームにイベントを送信
- */
-export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
-	window.parent.postMessage({
-		type,
-		payload,
-	}, '*');
-}
diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts
deleted file mode 100644
index 81f6c02dcf..0000000000
--- a/packages/frontend/src/scripts/reaction-picker.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Misskey from 'misskey-js';
-import { defineAsyncComponent, ref } from 'vue';
-import type { Ref } from 'vue';
-import { popup } from '@/os.js';
-import { store } from '@/store.js';
-
-class ReactionPicker {
-	private src: Ref<HTMLElement | null> = ref(null);
-	private manualShowing = ref(false);
-	private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
-	private onChosen?: (reaction: string) => void;
-	private onClosed?: () => void;
-
-	constructor() {
-		// nop
-	}
-
-	public async init() {
-		const reactionsRef = store.reactiveState.reactions;
-		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
-			src: this.src,
-			pinnedEmojis: reactionsRef,
-			asReactionPicker: true,
-			targetNote: this.targetNote,
-			manualShowing: this.manualShowing,
-		}, {
-			done: reaction => {
-				if (this.onChosen) this.onChosen(reaction);
-			},
-			close: () => {
-				this.manualShowing.value = false;
-			},
-			closed: () => {
-				this.src.value = null;
-				if (this.onClosed) this.onClosed();
-			},
-		});
-	}
-
-	public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
-		this.src.value = src;
-		this.targetNote.value = targetNote;
-		this.manualShowing.value = true;
-		this.onChosen = onChosen;
-		this.onClosed = onClosed;
-	}
-}
-
-export const reactionPicker = new ReactionPicker();
diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/scripts/reload-ask.ts
deleted file mode 100644
index 733d91b85a..0000000000
--- a/packages/frontend/src/scripts/reload-ask.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { i18n } from '@/i18n.js';
-import * as os from '@/os.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
-
-let isReloadConfirming = false;
-
-export async function reloadAsk(opts: {
-	unison?: boolean;
-	reason?: string;
-}) {
-	if (isReloadConfirming) {
-		return;
-	}
-
-	isReloadConfirming = true;
-
-	const { canceled } = await os.confirm(opts.reason == null ? {
-		type: 'info',
-		text: i18n.ts.reloadConfirm,
-	} : {
-		type: 'info',
-		title: i18n.ts.reloadConfirm,
-		text: opts.reason,
-	}).finally(() => {
-		isReloadConfirming = false;
-	});
-
-	if (canceled) return;
-
-	if (opts.unison) {
-		unisonReload();
-	} else {
-		location.reload();
-	}
-}
diff --git a/packages/frontend/src/scripts/search-emoji.ts b/packages/frontend/src/scripts/search-emoji.ts
deleted file mode 100644
index 371f69b9a7..0000000000
--- a/packages/frontend/src/scripts/search-emoji.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export type EmojiDef = {
-	emoji: string;
-	name: string;
-	url: string;
-	aliasOf?: string;
-} | {
-	emoji: string;
-	name: string;
-	aliasOf?: string;
-	isCustomEmoji?: true;
-};
-type EmojiScore = { emoji: EmojiDef, score: number };
-
-export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
-	if (!query) {
-		return [];
-	}
-
-	const matched = new Map<string, EmojiScore>();
-	// 完全一致（エイリアスなし）
-	emojiDb.some(x => {
-		if (x.name === query && !x.aliasOf) {
-			matched.set(x.name, { emoji: x, score: query.length + 3 });
-		}
-		return matched.size === max;
-	});
-
-	// 完全一致（エイリアス込み）
-	if (matched.size < max) {
-		emojiDb.some(x => {
-			if (x.name === query && !matched.has(x.aliasOf ?? x.name)) {
-				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
-			}
-			return matched.size === max;
-		});
-	}
-
-	// 前方一致（エイリアスなし）
-	if (matched.size < max) {
-		emojiDb.some(x => {
-			if (x.name.startsWith(query) && !x.aliasOf && !matched.has(x.name)) {
-				matched.set(x.name, { emoji: x, score: query.length + 1 });
-			}
-			return matched.size === max;
-		});
-	}
-
-	// 前方一致（エイリアス込み）
-	if (matched.size < max) {
-		emojiDb.some(x => {
-			if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
-				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
-			}
-			return matched.size === max;
-		});
-	}
-
-	// 部分一致（エイリアス込み）
-	if (matched.size < max) {
-		emojiDb.some(x => {
-			if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) {
-				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
-			}
-			return matched.size === max;
-		});
-	}
-
-	// 簡易あいまい検索（3文字以上）
-	if (matched.size < max && query.length > 3) {
-		const queryChars = [...query];
-		const hitEmojis = new Map<string, EmojiScore>();
-
-		for (const x of emojiDb) {
-			// 文字列の位置を進めながら、クエリの文字を順番に探す
-
-			let pos = 0;
-			let hit = 0;
-			for (const c of queryChars) {
-				pos = x.name.indexOf(c, pos);
-				if (pos <= -1) break;
-				hit++;
-			}
-
-			// 半分以上の文字が含まれていればヒットとする
-			if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
-				hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
-			}
-		}
-
-		// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく（6件＝オートコンプリートのポップアップのサイズ分）
-		[...hitEmojis.values()]
-			.sort((x, y) => y.score - x.score)
-			.slice(0, 6)
-			.forEach(it => matched.set(it.emoji.name, it));
-	}
-
-	return [...matched.values()]
-		.sort((x, y) => y.score - x.score)
-		.slice(0, max)
-		.map(it => it.emoji);
-}
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
deleted file mode 100644
index 42b34f54f5..0000000000
--- a/packages/frontend/src/scripts/select-file.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { ref } from 'vue';
-import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useStream } from '@/stream.js';
-import { i18n } from '@/i18n.js';
-import { uploadFile } from '@/scripts/upload.js';
-import { prefer } from '@/preferences.js';
-
-export function chooseFileFromPc(
-	multiple: boolean,
-	options?: {
-		uploadFolder?: string | null;
-		keepOriginal?: boolean;
-		nameConverter?: (file: File) => string | undefined;
-	},
-): Promise<Misskey.entities.DriveFile[]> {
-	const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder;
-	const keepOriginal = options?.keepOriginal ?? prefer.s.keepOriginalUploading;
-	const nameConverter = options?.nameConverter ?? (() => undefined);
-
-	return new Promise((res, rej) => {
-		const input = document.createElement('input');
-		input.type = 'file';
-		input.multiple = multiple;
-		input.onchange = () => {
-			if (!input.files) return res([]);
-			const promises = Array.from(
-				input.files,
-				file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal),
-			);
-
-			Promise.all(promises).then(driveFiles => {
-				res(driveFiles);
-			}).catch(err => {
-				// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
-			});
-
-			// 一応廃棄
-			(window as any).__misskey_input_ref__ = null;
-		};
-
-		// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
-		// iOS Safari で正常に動かす為のおまじない
-		(window as any).__misskey_input_ref__ = input;
-
-		input.click();
-	});
-}
-
-export function chooseFileFromDrive(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
-	return new Promise((res, rej) => {
-		os.selectDriveFile(multiple).then(files => {
-			res(files);
-		});
-	});
-}
-
-export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
-	return new Promise((res, rej) => {
-		os.inputText({
-			title: i18n.ts.uploadFromUrl,
-			type: 'url',
-			placeholder: i18n.ts.uploadFromUrlDescription,
-		}).then(({ canceled, result: url }) => {
-			if (canceled) return;
-
-			const marker = Math.random().toString(); // TODO: UUIDとか使う
-
-			const connection = useStream().useChannel('main');
-			connection.on('urlUploadFinished', urlResponse => {
-				if (urlResponse.marker === marker) {
-					res(urlResponse.file);
-					connection.dispose();
-				}
-			});
-
-			misskeyApi('drive/files/upload-from-url', {
-				url: url,
-				folderId: prefer.s.uploadFolder,
-				marker,
-			});
-
-			os.alert({
-				title: i18n.ts.uploadFromUrlRequested,
-				text: i18n.ts.uploadFromUrlMayTakeTime,
-			});
-		});
-	});
-}
-
-function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
-	return new Promise((res, rej) => {
-		const keepOriginal = ref(prefer.s.keepOriginalUploading);
-
-		os.popupMenu([label ? {
-			text: label,
-			type: 'label',
-		} : undefined, {
-			type: 'switch',
-			text: i18n.ts.keepOriginalUploading,
-			ref: keepOriginal,
-		}, {
-			text: i18n.ts.upload,
-			icon: 'ti ti-upload',
-			action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)),
-		}, {
-			text: i18n.ts.fromDrive,
-			icon: 'ti ti-cloud',
-			action: () => chooseFileFromDrive(multiple).then(files => res(files)),
-		}, {
-			text: i18n.ts.fromUrl,
-			icon: 'ti ti-link',
-			action: () => chooseFileFromUrl().then(file => res([file])),
-		}], src);
-	});
-}
-
-export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
-	return select(src, label, false).then(files => files[0]);
-}
-
-export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
-	return select(src, label, true);
-}
diff --git a/packages/frontend/src/scripts/show-moved-dialog.ts b/packages/frontend/src/scripts/show-moved-dialog.ts
deleted file mode 100644
index 35b3ef79d8..0000000000
--- a/packages/frontend/src/scripts/show-moved-dialog.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as os from '@/os.js';
-import { $i } from '@/account.js';
-import { i18n } from '@/i18n.js';
-
-export function showMovedDialog() {
-	if (!$i) return;
-	if (!$i.movedTo) return;
-
-	os.alert({
-		type: 'error',
-		title: i18n.ts.accountMovedShort,
-		text: i18n.ts.operationForbidden,
-	});
-
-	throw new Error('account moved');
-}
diff --git a/packages/frontend/src/scripts/show-suspended-dialog.ts b/packages/frontend/src/scripts/show-suspended-dialog.ts
deleted file mode 100644
index 8b89dbb936..0000000000
--- a/packages/frontend/src/scripts/show-suspended-dialog.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as os from '@/os.js';
-import { i18n } from '@/i18n.js';
-
-export function showSuspendedDialog() {
-	return os.alert({
-		type: 'error',
-		title: i18n.ts.yourAccountSuspendedTitle,
-		text: i18n.ts.yourAccountSuspendedDescription,
-	});
-}
diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts
deleted file mode 100644
index 1f6ef1928c..0000000000
--- a/packages/frontend/src/scripts/shuffle.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-/**
- * 配列をシャッフル (破壊的)
- */
-export function shuffle<T extends unknown[]>(array: T): T {
-	let currentIndex = array.length;
-	let randomIndex: number;
-
-	// While there remain elements to shuffle.
-	while (currentIndex !== 0) {
-		// Pick a remaining element.
-		randomIndex = Math.floor(Math.random() * currentIndex);
-		currentIndex--;
-
-		// And swap it with the current element.
-		[array[currentIndex], array[randomIndex]] = [
-			array[randomIndex], array[currentIndex]];
-	}
-
-	return array;
-}
diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts
deleted file mode 100644
index d88bdb6660..0000000000
--- a/packages/frontend/src/scripts/snowfall-effect.ts
+++ /dev/null
@@ -1,490 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export class SnowfallEffect {
-	private VERTEX_SOURCE = `#version 300 es
-		in vec4 a_position;
-		in vec4 a_color;
-		in vec3 a_rotation;
-		in vec3 a_speed;
-		in float a_size;
-		out vec4 v_color;
-		out float v_rotation;
-		uniform float u_time;
-		uniform mat4 u_projection;
-		uniform vec3 u_worldSize;
-		uniform float u_gravity;
-		uniform float u_wind;
-		uniform float u_spin_factor;
-		uniform float u_turbulence;
-
-		void main() {
-			v_color = a_color;
-			v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y;
-
-			vec3 pos = a_position.xyz;
-
-			pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
-			pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
-
-			pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z;
-			pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z;
-
-			gl_Position = u_projection * vec4(pos.xyz, a_position.w);
-			gl_PointSize = (a_size / gl_Position.w) * 100.0;
-		}
-	`;
-
-	private FRAGMENT_SOURCE = `#version 300 es
-		precision highp float;
-
-		in vec4 v_color;
-		in float v_rotation;
-		uniform sampler2D u_texture;
-		out vec4 out_color;
-
-		void main() {
-			vec2 rotated = vec2(
-				cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5,
-				cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5
-			);
-
-			vec4 snowflake = texture(u_texture, rotated);
-
-			out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a);
-		}
-	`;
-
-	private gl: WebGLRenderingContext;
-	private program: WebGLProgram;
-	private canvas: HTMLCanvasElement;
-	private buffers: Record<string, {
-		size: number;
-		value: number[] | Float32Array;
-		location: number;
-		ref: WebGLBuffer;
-	}>;
-	private uniforms: Record<string, {
-		type: string;
-		value: number[] | Float32Array;
-		location: WebGLUniformLocation;
-	}>;
-	private texture: WebGLTexture;
-	private camera: {
-		fov: number;
-		near: number;
-		far: number;
-		aspect: number;
-		z: number;
-	};
-	private wind: {
-		current: number;
-		force: number;
-		target: number;
-		min: number;
-		max: number;
-		easing: number;
-	};
-	private time: {
-		start: number;
-		previous: number;
-	} = {
-			start: 0,
-			previous: 0,
-		};
-	private raf = 0;
-
-	private density: number = 1 / 90;
-	private depth = 100;
-	private count = 1000;
-	private gravity = 100;
-	private speed: number = 1 / 10000;
-	private color: number[] = [1, 1, 1];
-	private opacity = 1;
-	private size = 4;
-	private snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAErRJREFUeAHdmgnYlmPax5MShaxRKRElPmXJXpaSsRxDU0bTZ+kt65RloiRDltEMQsxYKmS+zzYjxCCamCzV2LchResMIxFRQ1G93+93Pdf5dL9v7zuf4/hm0fc/jt9znddy3/e1nNd53c/7vHXq/AtVWVnZA/bzkaQjoWG298DeMdvrmP6/EIOqC4fBsbAx7Arz4TaYBPXgWVDnO2jSBrB2T0IMIA9mCmmoE8aonPkR6WPZHlp9xSlfeyeBzq9bHBD5feEdUGfDXBgBqnde+a2wvw/dYdNctvZNAp1PnTaFttA6JgP7eVgBM0CNzgO9HNvy0AcYDda6SaDTdXOnz8X+IkZDugAGQmOYA+ob6Ah/MIOMDRPhJjgJ6uV7pXtWt81/50SnY/Wvwn4ZDHAvwJ9ATYcxyaqsnEnqZCyCPaE80BgYZXG/5A3VyyP/b08LHa11z9KmFUwA5eqruRBHYX1s8WSI1Xcbme8Mt8PWUCU+kF8XbFN+dtH+p06OD4IU8EjD/VOZ5bnezq0XHcHuC2oV7BDlkVIWq56uIX8UjAO31GRIMYW0Vo/xXtSXJyTuXVO6xk1qalRTmQ9AfqzEvog2XYpllnsd6Qr4unCPT7NtByu0uU7vuAaOoy1JuvfXpJdTvSX0gI1gCXwGZdFmEFxoQb7Wid8s7lNu+I8wuHGsTqz2zpQ9DAa5R6HC55A2gvCMXthvwi25bjx26H0M9/9f4Rnok9s0zulFlC2HzzP9cnld8nH/p7DVrbmuIfYs6JLz9U3/z+KGadDeCDsmwre7GyEifn/su8HVSsL2HeBn8CK8AW+B7u9R5yrPgyOjvSn5DWAaXAG2UU7CE9Ayt4k4sR1lX4LaLdd9gn2ftsL+Vtuh1Dp/elH1C8lvCdUj8kDK3gbP8XdhCnSC86rcsNSR9pQvhc/gVlB9bUfqoFNAy/mLrUROrpMwCtpBxBbTtLqkF4K6IF9rf57I9pnYekx5AS0P1VhopXso9pR5buC7+kewU86nFcB+BT4EXdIvNO73sRBubGTXLZtTtgp+DEb++bACdqBuJOlAaMMzLVM3whegNznQDtCb+pW5b8YY76euB5+7pxm0IbzCfS8m3Zf2q4T8/+4JNArXGoptpxz8LqDmQJq0Qnostt/sfIn5GygD4/Zeq7B7wljQO2yjB/QGj0Pjxz4wGdqXrkjXtCT/ISyDa6EPpHrSraFjvnecFpMoMx40Br3xSlD262rYObevddHTs2kYwWUG9uP5It/f1eU5Xw9btwoXPALbwYXcg+unG/KB3Rq8n9ddAOpn4Kr8BAaBcltcDo9D7Ouavig1o34x7F94xqPk74eLQH0MH8HvwS3SLPe9iheEG6f70KiuLpZv6sxG/Va5bFJOabaO7ucAvGEbeAH+AN1hV7iDOidQFz4A2oJb6D1YDhXZHkTqpL8EbqHDYRtwW20AsdIb8syl5N2e6dTAPB2mWYa+hE4Qk7I59iMwFZ70GlJlfyuTVfygs7Hyw7HbwI0w3Tak14BqEtdg7wVdIx8pZbtBUbrjZeA3vUPBANkU+sEehev8O4Db6QpwYm+D8II0KPKHwUFeQ3oLDIMN4WgID1yOPQ+MAXMhNAtju3ztmtuAypiAw7EXwo/Am+0NfUG5mknYc6GfGVIjsoFNuyuoh8COuDcd2LmwA9jWE8bB3Q7N4XrwWAz5XOXR+Tx4n6FgdHeB6sF/w2QwhlSXdXvl/jixx4NH8GW5LDzb7GrR4ES4F5QddB99CieAwStOAPegdUZ2B71F3AXbQSn3vJ1bYaYWrayh3NUPTcbYFExVW3CfXwlvgfoavMbnDAY9dxGo6dCt0LeaB54H4UydDEPA2R4PDlrFLB9XuNmTlO+Xr7X9ZNBr9J4+EN8AMcv6ButpMND9FM6EnTOHkLrSnvtzwbbq3vwMB2ow/qWFSC8ZC++ZQaldbquH2afQWbl8TdcvVtC6LtipifAuOKt6gA9Tzqgzb5R2gP1hX3DVtZVHVvdklY5DA5beIkVPuZn8LOgAnWEfeAaUkxCan/voBNkfF+U5cFu5z5XlxZU20OmZtgm1K45VO4naNCukrcBZVk/CD+E/YBjoYjXJY8Zg9DxsDrbbBHTRotxOrug4eBs+hHgWZtKzfHrdXHBi9gDvqzxFHNA5KVfyBCf0ExgB7nkXStLLEKkniNf0AzUs5+ublkVFKiC9FBZAvGxshT0NnN3zoSUYSJQPcjAvm0HmjcIPemNS96F6E36drFLwugx7EEzNZV/l9IjoEPkW4B7eFtYH9QKcBcfA/aCWgpPQOT+zMbb9fS3nDbYR2MdgV0S5aVlUhLs0w45IHi7sqnnGJ2E7CXqHWgZXgJ1y8KqpDUmfSLmSV5yB/XrpDqVP8ofmehNdOv7I0ShfP4yyJdl2a4SchI1gCXgkHgljYfvc1i3cs/SU1A9jQRpfri/b0Sal1RrtSj4ULyHprY5C6+6E1+EBULq0E+DK7A96iwqX0z4td8B3dCdob5gD3UB3j9fUcNuDKFOvgc+bZAZFf4Zgu/q/AGPMgfm+5ShPWay+k6I31BwAvVDRYL2cuqfUVTkfnTqvVFx5ai7/MXn3tp1UrtRkDWRsaAMjzaD08uJ1irz7+8ps/6ZYj90V3FKrQBkvmubULbN7vs7tZRyJV9w0ePLbQ4PcJspqXnkbhbgoGk/AVptZRxpB0hU7Mpc1x34cdgKPm1dzeTts9XPwlFAO5Au4BDbO7ZycO7J9A/Zh2b4A2+ucALefWpTrflDKVq4kHQBOoi9PO1qvsDeGd6AxXAJbQ5VxlFrW8EnDcJlTsOPcjElxL7WNy7AduC4f2+A/rSN/Hyg7YMBTxgqPUT3F2HAqtIb58GvQW86GqyG+ff4UWz0FBuH4UhaTal1vmAGfg98dfP4d4HPGwmwYAg+D2/J7uU0ap/YaolHZVbBj5d1DaSK8ADsmqiH2JIhgNRhbPZrbhSdZ5heVJGw7477VfYuaagMK2sM8iMloga1HXAt/AeWELgQnR/0Z7k3W6pe3xTn/JamTFPGnPMZSj6p90rA8YOziwHcnH/EgTovJlJ0LPSHkyrTKmZNJ+8KrYKBsCQeB0pWdBFNleieMgzjL44jejTK1CPSY0CiMdyOT09g6ni5O3Ceg51U4VNLaPSA3SDNEwwiKFdgHgANNrpjb7UVejYTYCuZ92DR42HYh8gfDJfAMqBi4dqxk+RrKGkD0YXNsA6AT5qCUXhBe5CR0gPCC4dhqKFwI1m1qX0hr94CotDE4aAd3PCyBX4Jyn+sNL5tBDsRAp3S7b5KVYwa2A0nHaO5AXBeDtnlMxizsW+HomLh8zX9R5sTeBSEn/cqc2Tvak9eDXCyP2PgbYWzn2gefHxT7+0Qu/h18DO7XmPWYcYqSXuHz2myb6G7RNs7meLgeMxXugbiPA3clQx0xtgNPGN819L7+oCzvm6zSx+EkI+Du3Pe0LbOd/jqc7dhG9Wib+mJ5jaJBuL8e4B5aAMpAomKlb8d+KZWUVnw+dgzKSdDtvKaLDyJ1ReZB7O0J2EV5Xwd8OsTJExNpu7Q1SJ8zgy7K93UCX4P4mr4udoyhPGDKygOP+tomIFarMw2d+cfgF2DnDVAGoBvzw33YTHgPDoXQ7Fx/Wy6YkdMrcrmrehO4Pz3WvP90cIVPgonwITg4973yu0XTZK0+ZQaQd+K816twVAwKO71ZRj9zeg7lcVzXHghpVN4n2G3BAHQ1NILx4MBjoppgLwL3Ww8IHZsf6vGk3O8fwx9heK7rhD0o2zdg75JtT6GzQQ8KzcZwElSr3M5J85ktYCzEG+Gx2NNzm/Cm5pSp+K2gfLrZbg3RcB2IQcZN1qPM3+l06SjbAltX/TiXe1wtg7+AdR+AcgIs7xUPw94XxuTrnOD4E1bEoe9Rptw+DWGOGeQi7JOs1SfKKfk+epcakPNxbI8uFVdem8vT6aJdq7jASYjOFPdQDP4Q6t+Em8HVutmbkbYH9Tv4LcQW+H6ujy9Wrtxc6A7vQnznb5TbHUPZ0mw7CeoaOBAegmfBIKw8WZzs34M/oNiPGPzB2KHdrVMUlD29VFLLpw2jMWmnaIbdDNxXur+dWgVumTMglI4zMgbUEV5LmjqW7XnRkDS9qhbu/xZlZ8LWuc3UfM22Of80aVcYDJ/lstdIWxXu0TGXm/TO19vveHWuOglUxOo6iMfyBe7JOEp01ech9puuuBCMA8pVcUUNUB5lqgMYwJyE1oXOGTh9v1gO6kmogKEwHtREMHYofz5zAl3lJ2AWqJfgfohJiKB8HWWfg54YA9Zr1fn5Xmm80SdvHhNwVmq2umF8vWxA+WRwwE9BPNhOulrq0nxz97j6Go6DF8HYcBfYyer6MwWuoINeDG6roq4iE97QCtsJuxWc2JrkCeKEbgX7waOgnLiavxdQEWfohtgRwCrygIoxoQv1K0FNgR7gAKPTB+dr5lAWMliqmbAb7AzbgCs42vYK21NmOiwHJ9atpdxqDlhdA75QdYJT4XUYDfbBiVRe5ySoZTAbBpeekp6T4lo5uFnBz0fpJ6P8E9SJufEdXHipdRA/mw2hzmvfhrfgfjCKPwJnwn2g3igldb4hNaD5a6/fz7eHVuAb2wPwPs+4DB7E/hTagd64BbgoC6Ab9IAfgn+OX0p/ppAaGxZjnw6+Ep8DK8Cj0IDrmHw3GaeN9EZ/AlxFfk1RuVGUYu8K00D9Fa6EvrAUVKzO29gXg9vC1VW3g540w0xBcU2hKJnz+FxYvTCXWaduK/StuTZlLcD6JjnfEvsb6A56m32z78q4FMGw1gA4lEa60WmwMeiSnsljIBSDmEOBE3RdfvggbMuMIbNhItgJtbyUpE9ddjA0Bid1sderXDaQ1OdPAO9zH6hDcpuG2Ml7SQfArHRx6Xpf3JTluySrsrIP6Seg9/iMqsEvF6YZoXIDeAZCRmpneAHEnnLQnaEuXATX53schR3n/e7YyuvOT1bpnyV107Io3xZ6QWs4EirAyXkEqqvK3xa9CQ0c5C5xQ+zN8kWjcr2xZxTsBHfmsipbP671ZmW3wHYA58DdEPobhtwVF2HfBE9H3pT8xjkdja3iiDK4PQBO8Dx4B9wiH8JKeANcKTUW9IITwKNMeYrcArfDhVDsb1pVyty26le5D97/zWzrzVUGXyVjI0WjHUgq4CjoAuGiRuuJkN7mSJX7cn+uaZNyfBBgDHZqXvqsU2cZ6aPwChgE/ap8M9wLbSH+0DKOaw18z8N12GPAyf4BfADbwBmwCbxAHY9NvxQXx2GgVLZXPvurZDE0rqk5+NmAm8U2aIbdH9yDalgpSS80ltlB29fPqW9c8XLUHnsIuGquqt8gN7edwtazrOsAn4MysLryX8BD4Ap3y+0dZROIwPsl9h/hHjgit4lXdrdvHN8dc91wyk7JdvIS7VpF46Jb2ZGz4WJIRyBpBKQW3oR8lZuSvwQMhKtAfQUpYuf27cgbNx6EEeDAzgMHPwYMYi2gEcSfxC7B9qicDMoo/1vQI8p9IG88WAY/yeVpYrJdHpf5vytu4Ky7X46xIamrvjDb52OrG3K+HrZt4xq9wYEZPGPVfp7bhsdE2os2ylV6J1n5mbYPUX4S7AkGX+OAk2t6mm1Iw3PtQ+O4LuooK26RYvW3s7nBLZDiAGlbUHYiRV/S5AWk28DTEFqB4eo+B+n1M55Ivhu4kspj92uYCm6Px0Gv61lor0fcDQNBrQQnOr71lVeYsm894L/bkBuFe/u93eBngJtJMlwTDIDKyfDt6n3se8Dt8jHoNU0o70waq34obZ8lPx4coG+LbifrP6Pt0aQvwn65LFzcAHY8ZUtgAnwExp2WoMpeQLvaA12p7bf/pLPFmS3a/ajr750cfE43wX4YYmU9wi7IddHBCsrc69vm8uuwQydYVhQVvmsUn7s+ebfD0GhXrI+yf2jqA4oPKdo+iHxMwHbYRmgjta4cUTqCWXkg0UHatIR4SxxWKK9PeXhgKiZfxWOthzXuGff4p6b54bH3Y3W3pNxJcK8ebgdI44iys0G0N/8qKGOAGg9Ni50n3yjy2GkxSKtMRtT/21I7Fg/H9lRIX6qK5YX6zSjvDL4BGiBfBnUNmFdzwfKX4Ct40OtJv1sDj0Hlzrk6xbM3tob7uCf4amyk96VHvQg7gltGzQG9wpcwX6BCesfJ3/kJiMmgs+Gm4errUeZqF+Up4IoOzoWLcmqETyLve/2BsKkFpGUvK7VYCz6j06RbQx+ogHhN3Qdb3QF+a/wVKF94OhSHR77sWcXytcKm82usHGW9QE2B3skq/QB7APaqnJ9NuvaufnF1GIhxYH3LSAeA+hM0hMfgNzATdHvjgDHDv+qkP8gW77XW2gwmYsJe2F3zZDgxI7NteTo+/1WD/B9Au3Zjh2RyrgAAAABJRU5ErkJggg==';
-	private mode = 'snow';
-
-	private INITIAL_BUFFERS = () => ({
-		position: { size: 3, value: [] },
-		color: { size: 4, value: [] },
-		size: { size: 1, value: [] },
-		rotation: { size: 3, value: [] },
-		speed: { size: 3, value: [] },
-	});
-
-	private INITIAL_UNIFORMS = () => ({
-		time: { type: 'float', value: 0 },
-		worldSize: { type: 'vec3', value: [0, 0, 0] },
-		gravity: { type: 'float', value: this.gravity },
-		wind: { type: 'float', value: 0 },
-		spin_factor: { type: 'float', value: this.mode === 'sakura' ? 8 : 1 },
-		turbulence: { type: 'float', value: this.mode === 'sakura' ? 2 : 1 },
-		projection: {
-			type: 'mat4',
-			value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
-		},
-	});
-
-	private UNIFORM_SETTERS = {
-		int: 'uniform1i',
-		float: 'uniform1f',
-		vec2: 'uniform2fv',
-		vec3: 'uniform3fv',
-		vec4: 'uniform4fv',
-		mat2: 'uniformMatrix2fv',
-		mat3: 'uniformMatrix3fv',
-		mat4: 'uniformMatrix4fv',
-	};
-
-	private CAMERA = {
-		fov: 60,
-		near: 5,
-		far: 10000,
-		aspect: 1,
-		z: 100,
-	};
-
-	private WIND = {
-		current: 0,
-		force: 0.01,
-		target: 0.01,
-		min: 0,
-		max: 0.125,
-		easing: 0.0005,
-	};
-	/**
-	 * @throws {Error} - Thrown when it fails to get WebGL context for the canvas 
-	 */
-	constructor(options: {
-		sakura?: boolean;
-	}) {
-		if (options.sakura) {
-			this.mode = 'sakura';
-			this.snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDItMDFUMTQ6Mzk6NTYrMDkwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyNC0wMi0wMVQxNDozOTo1NiswOTAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI2NCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjY0IgogICBleGlmOkNvbG9yU3BhY2U9IjEiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iNjQiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjY0IgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIgogICB0aWZmOlhSZXNvbHV0aW9uPSI3Mi8xIgogICB0aWZmOllSZXNvbHV0aW9uPSI3Mi8xIj4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0icHJvZHVjZWQiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IFBob3RvIDIgMi4zLjEiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/PhldI30AAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWRu0sDQRCHP6Mh4oOIWlhYBPHRJBIjiDYWEV+gFjGCr+ZyuUuEJB53JyK2gq2gINr4KvQv0FawFgRFEcTaWtFG5ZwzgQQxs+zst7+dGXZnwRPPqFmrKgzZnG3GRqOB2bn5gO8FDxV46aJRUS1jcnokTln7uJdYsduQW6t83L9Wm9QsFSqqhQdVw7SFx4QnVm3D5R3hZjWtJIXPhIOmXFD4ztUTeX5xOZXnL5fNeGwIPA3CgVQJJ0pYTZtZYXk57dnMilq4j/uSOi03My1rm8xWLGKMEiXAOMMM0UcPA+L7CBGhW3aUyQ//5k+xLLmqeIM1TJZIkcYmKOqKVNdk1UXXZGRYc/v/t6+W3hvJV6+LgvfZcd46wLcN31uO83nkON/HUPkEl7li/vIh9L+LvlXU2g/AvwHnV0UtsQsXm9DyaCim8itVyvToOryeQv0cNN1AzUK+Z4VzTh4gvi5fdQ17+9Ap8f7FHyc6Z8kcDq1+AAAACXBIWXMAAAsTAAALEwEAmpwYAAADwElEQVR4nO2bT4hWVRjGf75TkhoEkhSa/9ocRIIwCsrE1pVnLbkYdFdGgQRS6caVm3CVy2oRuqmQ2yJXKTJh4GqCGs/CJCcLccAJ/yDpnGnxHYeZ4TrNfOc55y78nuWdc3/ve57v+b65f86BgQaqotiE5bEJKxYx7onYhOU1egKwGkViE/YCN4Cx2ITNC4xbDVwAJmMT9tXobVnpArEJe4CvZx0aB7aZdxPzxhkwArw66/Ae8+5Eyf6KJiA2YRPw+bzD64EjLcP3MXfyAMdjEzYWaG1GxRIQmzAEnAVeb/nzFPCSeTeaxj4FBOCZlrEjwBvm3VSJPksm4BPaJw8wBHwXm/BibMIW4HvaJ09ifFygP6BQAtKkfgEeEyHvAy+YdxdFvBmVSsBBdJMnsQ4KeTOSJyA2YT1wCXhcjL4HPG/e/amElkjAAfSTJzEPqKHSBKQLmSvAKiV3lm4BG8y7GyqgOgHvU27yAE+mGjLJEhCbsBL4A3haxXyIJoCN5t0dBUyZgF2UnzypxtsqmNKAt4SsarUkX4F0I3ONOgkAuA48a97FXJAqAa9Qb/IAa4CXFSCVATXjL635yBuQ/RsQm7AWuCroZamaBtaZd3/nQBQJeFPA6EfLFLUVBrwmYPSr7bkAhQHPCRj9al0uQGHAWgGjs9oKA7I/hS5rZ/0XSC86JDclGVph3t3t9+TcBHT56T9QVg+5BnT5/X+grB4GCcgs/sgnYCjzfIWyesg14Hrm+Qpl9ZBrwMT/DymurB4GCeiyuEidGnCN3n15V5pOPfStLAPMu1vAWA4jU7+Zd7dzAIqboREBo7PaCgN+EjA6qz1IQDbAu9/prQeorUvm3eVciOqx+JcizlL0hQKiMuAreiu/amkq1cyWxADz7ipwWsFapH4w7/5SgJRvh+cviCyp4yqQeonMOWCHktmic+bdThVMvUSmyFK2kjWkBph354FTSuY8nTLvflYCSyyT+xD4pwB3EvhADZUbYN5dAfarucB+825cDS25WvwksFuEO2nevSNizVHJ1eLvAoplrePAewJOq4oZYN5NAsPkPTCZBoYTq4iK7hgx734EjmUgjpl3Z1T9tKnGpqlP6e+p0Vg6t6iKG5De3A6ztJul+/Si3/db38WqyrY58+4CcHQJpxxN5xRXFQOSjgCjixg3SvuusiKqZoB59y+964KbCwy7Cew27+7V6apuAkibnhbaEbq3xMaohVTVAADz7hvgMHN/FKeAQ+bdt7X7Kb519mGKTdgKfEbvYucj8+7XLvr4DxAA134c0w/5AAAAAElFTkSuQmCC';
-			this.size = 10;
-			this.density = 1 / 280;
-		}
-
-		const canvas = this.initCanvas();
-		const gl = canvas.getContext('webgl2', { antialias: true });
-		if (gl == null) throw new Error('Failed to get WebGL context');
-
-		document.body.append(canvas);
-
-		this.canvas = canvas;
-		this.gl = gl;
-		this.program = this.initProgram();
-		this.buffers = this.initBuffers();
-		this.uniforms = this.initUniforms();
-		this.texture = this.initTexture();
-		this.camera = this.initCamera();
-		this.wind = this.initWind();
-
-		this.resize = this.resize.bind(this);
-		this.update = this.update.bind(this);
-
-		window.addEventListener('resize', () => this.resize());
-	}
-
-	private initCanvas(): HTMLCanvasElement {
-		const canvas = document.createElement('canvas');
-
-		Object.assign(canvas.style, {
-			position: 'fixed',
-			top: 0,
-			left: 0,
-			width: '100vw',
-			height: '100vh',
-			background: 'transparent',
-			'pointer-events': 'none',
-			'z-index': 2147483647,
-		});
-
-		return canvas;
-	}
-
-	private initCamera() {
-		return { ...this.CAMERA };
-	}
-
-	private initWind() {
-		return { ...this.WIND };
-	}
-
-	private initShader(type, source): WebGLShader {
-		const { gl } = this;
-		const shader = gl.createShader(type);
-		if (shader == null) throw new Error('Failed to create shader');
-
-		gl.shaderSource(shader, source);
-		gl.compileShader(shader);
-
-		return shader;
-	}
-
-	private initProgram(): WebGLProgram {
-		const { gl } = this;
-		const vertex = this.initShader(gl.VERTEX_SHADER, this.VERTEX_SOURCE);
-		const fragment = this.initShader(gl.FRAGMENT_SHADER, this.FRAGMENT_SOURCE);
-		const program = gl.createProgram();
-		if (program == null) throw new Error('Failed to create program');
-
-		gl.attachShader(program, vertex);
-		gl.attachShader(program, fragment);
-		gl.linkProgram(program);
-		gl.useProgram(program);
-
-		return program;
-	}
-
-	private initBuffers(): SnowfallEffect['buffers'] {
-		const { gl, program } = this;
-		const buffers = this.INITIAL_BUFFERS() as unknown as SnowfallEffect['buffers'];
-
-		for (const [name, buffer] of Object.entries(buffers)) {
-			buffer.location = gl.getAttribLocation(program, `a_${name}`);
-			buffer.ref = gl.createBuffer()!;
-
-			gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref);
-			gl.enableVertexAttribArray(buffer.location);
-			gl.vertexAttribPointer(
-				buffer.location,
-				buffer.size,
-				gl.FLOAT,
-				false,
-				0,
-				0,
-			);
-		}
-
-		return buffers;
-	}
-
-	private updateBuffers() {
-		const { buffers } = this;
-
-		for (const name of Object.keys(buffers)) {
-			this.setBuffer(name);
-		}
-	}
-
-	private setBuffer(name: string, value?) {
-		const { gl, buffers } = this;
-		const buffer = buffers[name];
-
-		buffer.value = new Float32Array(value ?? buffer.value);
-
-		gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref);
-		gl.bufferData(gl.ARRAY_BUFFER, buffer.value, gl.STATIC_DRAW);
-	}
-
-	private initUniforms(): SnowfallEffect['uniforms'] {
-		const { gl, program } = this;
-		const uniforms = this.INITIAL_UNIFORMS() as unknown as SnowfallEffect['uniforms'];
-
-		for (const [name, uniform] of Object.entries(uniforms)) {
-			uniform.location = gl.getUniformLocation(program, `u_${name}`)!;
-		}
-
-		return uniforms;
-	}
-
-	private updateUniforms() {
-		const { uniforms } = this;
-
-		for (const name of Object.keys(uniforms)) {
-			this.setUniform(name);
-		}
-	}
-
-	private setUniform(name: string, value?) {
-		const { gl, uniforms } = this;
-		const uniform = uniforms[name];
-		const setter = this.UNIFORM_SETTERS[uniform.type];
-		const isMatrix = /^mat[2-4]$/i.test(uniform.type);
-
-		uniform.value = value ?? uniform.value;
-
-		if (isMatrix) {
-			gl[setter](uniform.location, false, uniform.value);
-		} else {
-			gl[setter](uniform.location, uniform.value);
-		}
-	}
-
-	private initTexture() {
-		const { gl } = this;
-		const texture = gl.createTexture();
-		if (texture == null) throw new Error('Failed to create texture');
-		const image = new Image();
-
-		gl.bindTexture(gl.TEXTURE_2D, texture);
-		gl.texImage2D(
-			gl.TEXTURE_2D,
-			0,
-			gl.RGBA,
-			1,
-			1,
-			0,
-			gl.RGBA,
-			gl.UNSIGNED_BYTE,
-			new Uint8Array([0, 0, 0, 0]),
-		);
-
-		image.onload = () => {
-			gl.bindTexture(gl.TEXTURE_2D, texture);
-			gl.texImage2D(
-				gl.TEXTURE_2D,
-				0,
-				gl.RGBA,
-				gl.RGBA,
-				gl.UNSIGNED_BYTE,
-				image,
-			);
-			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
-			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
-			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
-			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
-		};
-
-		image.src = this.snowflake;
-
-		return texture;
-	}
-
-	private initSnowflakes(vw: number, vh: number, dpi: number) {
-		const position: number[] = [];
-		const color: number[] = [];
-		const size: number[] = [];
-		const rotation: number[] = [];
-		const speed: number[] = [];
-
-		const height = 1 / this.density;
-		const width = (vw / vh) * height;
-		const depth = this.depth;
-		const count = this.count;
-		const length = (vw / vh) * count;
-
-		for (let i = 0; i < length; ++i) {
-			position.push(
-				-width + Math.random() * width * 2,
-				-height + Math.random() * height * 2,
-				Math.random() * depth * 2,
-			);
-
-			speed.push(1 + Math.random(), 1 + Math.random(), Math.random() * 10);
-
-			rotation.push(
-				Math.random() * 2 * Math.PI,
-				Math.random() * 20,
-				Math.random() * 10,
-			);
-
-			color.push(...this.color, 0.1 + Math.random() * this.opacity);
-			//size.push((this.size * Math.random() * this.size * vh * dpi) / 1000);
-			size.push((this.size * vh * dpi) / 1000);
-		}
-
-		this.setUniform('worldSize', [width, height, depth]);
-
-		this.setBuffer('position', position);
-		this.setBuffer('color', color);
-		this.setBuffer('rotation', rotation);
-		this.setBuffer('size', size);
-		this.setBuffer('speed', speed);
-	}
-
-	private setProjection(aspect: number) {
-		const { camera } = this;
-
-		camera.aspect = aspect;
-
-		const fovRad = (camera.fov * Math.PI) / 180;
-		const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad);
-		const rangeInv = 1.0 / (camera.near - camera.far);
-
-		const m0 = f / camera.aspect;
-		const m5 = f;
-		const m10 = (camera.near + camera.far) * rangeInv;
-		const m11 = -1;
-		const m14 = camera.near * camera.far * rangeInv * 2 + camera.z;
-		const m15 = camera.z;
-
-		return [m0, 0, 0, 0, 0, m5, 0, 0, 0, 0, m10, m11, 0, 0, m14, m15];
-	}
-
-	public render() {
-		const { gl } = this;
-
-		gl.enable(gl.BLEND);
-		gl.enable(gl.CULL_FACE);
-		gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
-		gl.disable(gl.DEPTH_TEST);
-
-		this.updateBuffers();
-		this.updateUniforms();
-		this.resize(true);
-
-		this.time = {
-			start: window.performance.now(),
-			previous: window.performance.now(),
-		};
-
-		if (this.raf) window.cancelAnimationFrame(this.raf);
-		this.raf = window.requestAnimationFrame(this.update);
-
-		return this;
-	}
-
-	private resize(updateSnowflakes = false) {
-		const { canvas, gl } = this;
-		const vw = canvas.offsetWidth;
-		const vh = canvas.offsetHeight;
-		const aspect = vw / vh;
-		const dpi = window.devicePixelRatio;
-
-		canvas.width = vw * dpi;
-		canvas.height = vh * dpi;
-
-		gl.viewport(0, 0, vw * dpi, vh * dpi);
-		gl.clearColor(0, 0, 0, 0);
-
-		if (updateSnowflakes === true) {
-			this.initSnowflakes(vw, vh, dpi);
-		}
-
-		this.setUniform('projection', this.setProjection(aspect));
-	}
-
-	private update(timestamp: number) {
-		const { gl, buffers, wind } = this;
-		const elapsed = (timestamp - this.time.start) * this.speed;
-		const delta = timestamp - this.time.previous;
-
-		gl.clear(gl.COLOR_BUFFER_BIT);
-		gl.drawArrays(
-			gl.POINTS,
-			0,
-			buffers.position.value.length / buffers.position.size,
-		);
-
-		if (Math.random() > 0.995) {
-			wind.target =
-				(wind.min + Math.random() * (wind.max - wind.min)) *
-				(Math.random() > 0.5 ? -1 : 1);
-		}
-
-		wind.force += (wind.target - wind.force) * wind.easing;
-		wind.current += wind.force * (delta * 0.2);
-
-		this.setUniform('wind', wind.current);
-		this.setUniform('time', elapsed);
-
-		this.time.previous = timestamp;
-
-		this.raf = window.requestAnimationFrame(this.update);
-	}
-}
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
deleted file mode 100644
index 436c2b75f0..0000000000
--- a/packages/frontend/src/scripts/sound.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { SoundStore } from '@/preferences/def.js';
-import { prefer } from '@/preferences.js';
-import { PREF_DEF } from '@/preferences/def.js';
-
-let ctx: AudioContext;
-const cache = new Map<string, AudioBuffer>();
-let canPlay = true;
-
-export const soundsTypes = [
-	// 音声なし
-	null,
-
-	// ドライブの音声
-	'_driveFile_',
-
-	// プリインストール
-	'syuilo/n-aec',
-	'syuilo/n-aec-4va',
-	'syuilo/n-aec-4vb',
-	'syuilo/n-aec-8va',
-	'syuilo/n-aec-8vb',
-	'syuilo/n-cea',
-	'syuilo/n-cea-4va',
-	'syuilo/n-cea-4vb',
-	'syuilo/n-cea-8va',
-	'syuilo/n-cea-8vb',
-	'syuilo/n-eca',
-	'syuilo/n-eca-4va',
-	'syuilo/n-eca-4vb',
-	'syuilo/n-eca-8va',
-	'syuilo/n-eca-8vb',
-	'syuilo/n-ea',
-	'syuilo/n-ea-4va',
-	'syuilo/n-ea-4vb',
-	'syuilo/n-ea-8va',
-	'syuilo/n-ea-8vb',
-	'syuilo/n-ea-harmony',
-	'syuilo/up',
-	'syuilo/down',
-	'syuilo/pope1',
-	'syuilo/pope2',
-	'syuilo/waon',
-	'syuilo/popo',
-	'syuilo/triple',
-	'syuilo/bubble1',
-	'syuilo/bubble2',
-	'syuilo/poi1',
-	'syuilo/poi2',
-	'syuilo/pirori',
-	'syuilo/pirori-wet',
-	'syuilo/pirori-square-wet',
-	'syuilo/square-pico',
-	'syuilo/reverved',
-	'syuilo/ryukyu',
-	'syuilo/kick',
-	'syuilo/snare',
-	'syuilo/queue-jammed',
-	'aisha/1',
-	'aisha/2',
-	'aisha/3',
-	'noizenecio/kick_gaba1',
-	'noizenecio/kick_gaba2',
-	'noizenecio/kick_gaba3',
-	'noizenecio/kick_gaba4',
-	'noizenecio/kick_gaba5',
-	'noizenecio/kick_gaba6',
-	'noizenecio/kick_gaba7',
-] as const;
-
-export const operationTypes = [
-	'noteMy',
-	'note',
-	'notification',
-	'reaction',
-] as const;
-
-/** サウンドの種類 */
-export type SoundType = typeof soundsTypes[number];
-
-/** スプライトの種類 */
-export type OperationType = typeof operationTypes[number];
-
-/**
- * 音声を読み込む
- * @param url url
- * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
- */
-export async function loadAudio(url: string, options?: { useCache?: boolean; }) {
-	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	if (ctx == null) {
-		ctx = new AudioContext();
-
-		window.addEventListener('beforeunload', () => {
-			ctx.close();
-		});
-	}
-	if (options?.useCache ?? true) {
-		if (cache.has(url)) {
-			return cache.get(url) as AudioBuffer;
-		}
-	}
-
-	let response: Response;
-
-	try {
-		response = await fetch(url);
-	} catch (err) {
-		return;
-	}
-
-	const arrayBuffer = await response.arrayBuffer();
-	const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
-
-	if (options?.useCache ?? true) {
-		cache.set(url, audioBuffer);
-	}
-
-	return audioBuffer;
-}
-
-/**
- * 既定のスプライトを再生する
- * @param type スプライトの種類を指定
- */
-export function playMisskeySfx(operationType: OperationType) {
-	const sound = prefer.s[`sound.on.${operationType}`];
-	playMisskeySfxFile(sound).then((succeed) => {
-		if (!succeed && sound.type === '_driveFile_') {
-			// ドライブファイルが存在しない場合はデフォルトのサウンドを再生する
-			const soundName = PREF_DEF[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>;
-			if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`);
-			playMisskeySfxFileInternal({
-				type: soundName,
-				volume: sound.volume,
-			});
-		}
-	});
-}
-
-/**
- * サウンド設定形式で指定された音声を再生する
- * @param soundStore サウンド設定
- */
-export async function playMisskeySfxFile(soundStore: SoundStore): Promise<boolean> {
-	// 連続して再生しない
-	if (!canPlay) return false;
-	// ユーザーアクティベーションが必要な場合はそれがない場合は再生しない
-	if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return false;
-	// サウンドがない場合は再生しない
-	if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return false;
-
-	canPlay = false;
-	return await playMisskeySfxFileInternal(soundStore).finally(() => {
-		// ごく短時間に音が重複しないように
-		setTimeout(() => {
-			canPlay = true;
-		}, 25);
-	});
-}
-
-async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise<boolean> {
-	if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
-		return false;
-	}
-	const masterVolume = prefer.s['sound.masterVolume'];
-	if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
-		return true; // ミュート時は成功として扱う
-	}
-	const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
-	const buffer = await loadAudio(url).catch(() => {
-		return undefined;
-	});
-	if (!buffer) return false;
-	const volume = soundStore.volume * masterVolume;
-	createSourceNode(buffer, { volume }).soundSource.start();
-	return true;
-}
-
-export async function playUrl(url: string, opts: {
-	volume?: number;
-	pan?: number;
-	playbackRate?: number;
-}) {
-	if (opts.volume === 0) {
-		return;
-	}
-	const buffer = await loadAudio(url);
-	if (!buffer) return;
-	createSourceNode(buffer, opts).soundSource.start();
-}
-
-export function createSourceNode(buffer: AudioBuffer, opts: {
-	volume?: number;
-	pan?: number;
-	playbackRate?: number;
-}): {
-		soundSource: AudioBufferSourceNode;
-		panNode: StereoPannerNode;
-		gainNode: GainNode;
-	} {
-	const panNode = ctx.createStereoPanner();
-	panNode.pan.value = opts.pan ?? 0;
-
-	const gainNode = ctx.createGain();
-
-	gainNode.gain.value = opts.volume ?? 1;
-
-	const soundSource = ctx.createBufferSource();
-	soundSource.buffer = buffer;
-	soundSource.playbackRate.value = opts.playbackRate ?? 1;
-	soundSource
-		.connect(panNode)
-		.connect(gainNode)
-		.connect(ctx.destination);
-
-	return { soundSource, panNode, gainNode };
-}
-
-/**
- * 音声の長さをミリ秒で取得する
- * @param file ファイルのURL（ドライブIDではない）
- */
-export async function getSoundDuration(file: string): Promise<number> {
-	const audioEl = document.createElement('audio');
-	audioEl.src = file;
-	return new Promise((resolve) => {
-		const si = setInterval(() => {
-			if (audioEl.readyState > 0) {
-				resolve(audioEl.duration * 1000);
-				clearInterval(si);
-				audioEl.remove();
-			}
-		}, 100);
-	});
-}
-
-/**
- * ミュートすべきかどうかを判断する
- */
-export function isMute(): boolean {
-	if (prefer.s['sound.notUseSound']) {
-		// サウンドを出力しない
-		return true;
-	}
-
-	// noinspection RedundantIfStatementJS
-	if (prefer.s['sound.useSoundOnlyWhenActive'] && document.visibilityState === 'hidden') {
-		// ブラウザがアクティブな時のみサウンドを出力する
-		return true;
-	}
-
-	return false;
-}
diff --git a/packages/frontend/src/scripts/sticky-sidebar.ts b/packages/frontend/src/scripts/sticky-sidebar.ts
deleted file mode 100644
index 50f1e6ecc8..0000000000
--- a/packages/frontend/src/scripts/sticky-sidebar.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export class StickySidebar {
-	private lastScrollTop = 0;
-	private container: HTMLElement;
-	private el: HTMLElement;
-	private spacer: HTMLElement;
-	private marginTop: number;
-	private isTop = false;
-	private isBottom = false;
-	private offsetTop: number;
-	private globalHeaderHeight = 59;
-
-	constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) {
-		this.container = container;
-		this.el = this.container.children[0] as HTMLElement;
-		this.el.style.position = 'sticky';
-		this.spacer = document.createElement('div');
-		this.container.prepend(this.spacer);
-		this.marginTop = marginTop;
-		this.offsetTop = this.container.getBoundingClientRect().top;
-		this.globalHeaderHeight = globalHeaderHeight;
-	}
-
-	public calc(scrollTop: number) {
-		if (scrollTop > this.lastScrollTop) { // downscroll
-			const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight);
-			this.el.style.bottom = null;
-			this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`;
-
-			this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight);
-
-			if (this.isTop) {
-				this.isTop = false;
-				this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`;
-			}
-		} else { // upscroll
-			const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight;
-			this.el.style.top = null;
-			this.el.style.bottom = `${-overflow}px`;
-
-			this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop;
-
-			if (this.isBottom) {
-				this.isBottom = false;
-				this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`;
-			}
-		}
-
-		this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
-	}
-}
diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts
deleted file mode 100644
index 9b1b368de4..0000000000
--- a/packages/frontend/src/scripts/stream-mock.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { EventEmitter } from 'eventemitter3';
-import * as Misskey from 'misskey-js';
-import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js';
-
-type AnyOf<T extends Record<any, any>> = T[keyof T];
-type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never;
-
-/**
- * Websocket無効化時に使うStreamのモック（なにもしない）
- */
-export class StreamMock extends EventEmitter<StreamEvents> implements IStream {
-	public readonly state = 'initializing';
-
-	constructor(...args: ConstructorParameters<typeof Misskey.Stream>) {
-		super();
-		// do nothing
-	}
-
-	public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock<Channels[C]> {
-		return new ChannelConnectionMock(this, channel, name);
-	}
-
-	public removeSharedConnection(connection: any): void {
-		// do nothing
-	}
-
-	public removeSharedConnectionPool(pool: any): void {
-		// do nothing
-	}
-
-	public disconnectToChannel(): void {
-		// do nothing
-	}
-
-	public send(typeOrPayload: string): void;
-	public send(typeOrPayload: string, payload: any): void;
-	public send(typeOrPayload: Record<string, any> | any[]): void;
-	public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
-		// do nothing
-	}
-
-	public ping(): void {
-		// do nothing
-	}
-
-	public heartbeat(): void {
-		// do nothing
-	}
-
-	public close(): void {
-		// do nothing
-	}
-}
-
-class ChannelConnectionMock<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
-	public id = '';
-	public name?: string; // for debug
-	public inCount = 0; // for debug
-	public outCount = 0; // for debug
-	public channel: string;
-
-	constructor(stream: IStream, ...args: OmitFirst<ConstructorParameters<typeof Misskey.ChannelConnection<Channel>>>) {
-		super();
-
-		this.channel = args[0];
-		this.name = args[1];
-	}
-
-	public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void {
-		// do nothing
-	}
-
-	public dispose(): void {
-		// do nothing
-	}
-}
diff --git a/packages/frontend/src/scripts/test-utils.ts b/packages/frontend/src/scripts/test-utils.ts
deleted file mode 100644
index 52bb2d94e0..0000000000
--- a/packages/frontend/src/scripts/test-utils.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export async function tick(): Promise<void> {
-	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never);
-}
diff --git a/packages/frontend/src/scripts/theme-editor.ts b/packages/frontend/src/scripts/theme-editor.ts
deleted file mode 100644
index 0206e378bf..0000000000
--- a/packages/frontend/src/scripts/theme-editor.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { v4 as uuid } from 'uuid';
-
-import { themeProps } from './theme.js';
-import type { Theme } from './theme.js';
-
-export type Default = null;
-export type Color = string;
-export type FuncName = 'alpha' | 'darken' | 'lighten';
-export type Func = { type: 'func'; name: FuncName; arg: number; value: string; };
-export type RefProp = { type: 'refProp'; key: string; };
-export type RefConst = { type: 'refConst'; key: string; };
-export type Css = { type: 'css'; value: string; };
-
-export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default;
-
-export type ThemeViewModel = [ string, ThemeValue ][];
-
-export const fromThemeString = (str?: string) : ThemeValue => {
-	if (!str) return null;
-	if (str.startsWith(':')) {
-		const parts = str.slice(1).split('<');
-		const name = parts[0] as FuncName;
-		const arg = parseFloat(parts[1]);
-		const value = parts[2].startsWith('@') ? parts[2].slice(1) : '';
-		return { type: 'func', name, arg, value };
-	} else if (str.startsWith('@')) {
-		return {
-			type: 'refProp',
-			key: str.slice(1),
-		};
-	} else if (str.startsWith('$')) {
-		return {
-			type: 'refConst',
-			key: str.slice(1),
-		};
-	} else if (str.startsWith('"')) {
-		return {
-			type: 'css',
-			value: str.substring(1).trim(),
-		};
-	} else {
-		return str;
-	}
-};
-
-export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => {
-	if (typeof value === 'string') return value;
-	switch (value.type) {
-		case 'func': return `:${value.name}<${value.arg}<@${value.value}`;
-		case 'refProp': return `@${value.key}`;
-		case 'refConst': return `$${value.key}`;
-		case 'css': return `" ${value.value}`;
-	}
-};
-
-export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => {
-	const props = { } as { [key: string]: string };
-	for (const [key, value] of vm) {
-		if (value === null) continue;
-		props[key] = toThemeString(value);
-	}
-
-	return {
-		id: uuid(),
-		name, desc, author, props, base,
-	};
-};
-
-export const convertToViewModel = (theme: Theme): ThemeViewModel => {
-	const vm: ThemeViewModel = [];
-	// プロパティの登録
-	vm.push(...themeProps.map(key => [key, fromThemeString(theme.props[key])] as [ string, ThemeValue ]));
-
-	// 定数の登録
-	const consts = Object
-		.keys(theme.props)
-		.filter(k => k.startsWith('$'))
-		.map(k => [k, fromThemeString(theme.props[k])] as [ string, ThemeValue ]);
-
-	vm.push(...consts);
-	return vm;
-};
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
deleted file mode 100644
index 851ba41e61..0000000000
--- a/packages/frontend/src/scripts/theme.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { ref } from 'vue';
-import tinycolor from 'tinycolor2';
-import lightTheme from '@@/themes/_light.json5';
-import darkTheme from '@@/themes/_dark.json5';
-import JSON5 from 'json5';
-import { deepClone } from './clone.js';
-import type { BundledTheme } from 'shiki/themes';
-import { globalEvents } from '@/events.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { addTheme, getThemes } from '@/theme-store.js';
-
-export type Theme = {
-	id: string;
-	name: string;
-	author: string;
-	desc?: string;
-	base?: 'dark' | 'light';
-	props: Record<string, string>;
-	codeHighlighter?: {
-		base: BundledTheme;
-		overrides?: Record<string, any>;
-	} | {
-		base: '_none_';
-		overrides: Record<string, any>;
-	};
-};
-
-export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
-
-export const getBuiltinThemes = () => Promise.all(
-	[
-		'l-light',
-		'l-coffee',
-		'l-apricot',
-		'l-rainy',
-		'l-botanical',
-		'l-vivid',
-		'l-cherry',
-		'l-sushi',
-		'l-u0',
-
-		'd-dark',
-		'd-persimmon',
-		'd-astro',
-		'd-future',
-		'd-botanical',
-		'd-green-lime',
-		'd-green-orange',
-		'd-cherry',
-		'd-ice',
-		'd-u0',
-	].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
-);
-
-export const getBuiltinThemesRef = () => {
-	const builtinThemes = ref<Theme[]>([]);
-	getBuiltinThemes().then(themes => builtinThemes.value = themes);
-	return builtinThemes;
-};
-
-let timeout: number | null = null;
-
-export function applyTheme(theme: Theme, persist = true) {
-	if (timeout) window.clearTimeout(timeout);
-
-	document.documentElement.classList.add('_themeChanging_');
-
-	timeout = window.setTimeout(() => {
-		document.documentElement.classList.remove('_themeChanging_');
-	}, 1000);
-
-	const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
-
-	document.documentElement.dataset.colorScheme = colorScheme;
-
-	// Deep copy
-	const _theme = deepClone(theme);
-
-	if (_theme.base) {
-		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
-		if (base) _theme.props = Object.assign({}, base.props, _theme.props);
-	}
-
-	const props = compile(_theme);
-
-	for (const tag of document.head.children) {
-		if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
-			tag.setAttribute('content', props['htmlThemeColor']);
-			break;
-		}
-	}
-
-	for (const [k, v] of Object.entries(props)) {
-		document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
-	}
-
-	document.documentElement.style.setProperty('color-scheme', colorScheme);
-
-	if (persist) {
-		miLocalStorage.setItem('theme', JSON.stringify(props));
-		miLocalStorage.setItem('themeId', theme.id);
-		miLocalStorage.setItem('colorScheme', colorScheme);
-	}
-
-	// 色計算など再度行えるようにクライアント全体に通知
-	globalEvents.emit('themeChanged');
-}
-
-function compile(theme: Theme): Record<string, string> {
-	function getColor(val: string): tinycolor.Instance {
-		if (val[0] === '@') { // ref (prop)
-			return getColor(theme.props[val.substring(1)]);
-		} else if (val[0] === '$') { // ref (const)
-			return getColor(theme.props[val]);
-		} else if (val[0] === ':') { // func
-			const parts = val.split('<');
-			const func = parts.shift().substring(1);
-			const arg = parseFloat(parts.shift());
-			const color = getColor(parts.join('<'));
-
-			switch (func) {
-				case 'darken': return color.darken(arg);
-				case 'lighten': return color.lighten(arg);
-				case 'alpha': return color.setAlpha(arg);
-				case 'hue': return color.spin(arg);
-				case 'saturate': return color.saturate(arg);
-			}
-		}
-
-		// other case
-		return tinycolor(val);
-	}
-
-	const props = {};
-
-	for (const [k, v] of Object.entries(theme.props)) {
-		if (k.startsWith('$')) continue; // ignore const
-
-		props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
-	}
-
-	return props;
-}
-
-function genValue(c: tinycolor.Instance): string {
-	return c.toRgbString();
-}
-
-export function validateTheme(theme: Record<string, any>): boolean {
-	if (theme.id == null || typeof theme.id !== 'string') return false;
-	if (theme.name == null || typeof theme.name !== 'string') return false;
-	if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
-	if (theme.props == null || typeof theme.props !== 'object') return false;
-	return true;
-}
-
-export function parseThemeCode(code: string): Theme {
-	let theme;
-
-	try {
-		theme = JSON5.parse(code);
-	} catch (err) {
-		throw new Error('Failed to parse theme json');
-	}
-	if (!validateTheme(theme)) {
-		throw new Error('This theme is invaild');
-	}
-	if (getThemes().some(t => t.id === theme.id)) {
-		throw new Error('This theme is already installed');
-	}
-
-	return theme;
-}
-
-export function previewTheme(code: string): void {
-	const theme = parseThemeCode(code);
-	if (theme) applyTheme(theme, false);
-}
-
-export async function installTheme(code: string): Promise<void> {
-	const theme = parseThemeCode(code);
-	if (!theme) return;
-	await addTheme(theme);
-}
diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/scripts/time.ts
deleted file mode 100644
index 275b67ed00..0000000000
--- a/packages/frontend/src/scripts/time.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-const dateTimeIntervals = {
-	'day': 86400000,
-	'hour': 3600000,
-	'ms': 1,
-};
-
-export function dateUTC(time: number[]): Date {
-	const d =
-		time.length === 2 ? Date.UTC(time[0], time[1])
-		: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
-		: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
-		: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
-		: time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
-		: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
-		: null;
-
-	if (!d) throw new Error('wrong number of arguments');
-
-	return new Date(d);
-}
-
-export function isTimeSame(a: Date, b: Date): boolean {
-	return a.getTime() === b.getTime();
-}
-
-export function isTimeBefore(a: Date, b: Date): boolean {
-	return (a.getTime() - b.getTime()) < 0;
-}
-
-export function isTimeAfter(a: Date, b: Date): boolean {
-	return (a.getTime() - b.getTime()) > 0;
-}
-
-export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
-	return new Date(x.getTime() + (value * dateTimeIntervals[span]));
-}
-
-export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
-	return new Date(x.getTime() - (value * dateTimeIntervals[span]));
-}
diff --git a/packages/frontend/src/scripts/timezones.ts b/packages/frontend/src/scripts/timezones.ts
deleted file mode 100644
index c7582e06da..0000000000
--- a/packages/frontend/src/scripts/timezones.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export const timezones = [{
-	name: 'UTC',
-	abbrev: 'UTC',
-	offset: 0,
-}, {
-	name: 'Europe/Berlin',
-	abbrev: 'CET',
-	offset: 60,
-}, {
-	name: 'Asia/Tokyo',
-	abbrev: 'JST',
-	offset: 540,
-}, {
-	name: 'Asia/Seoul',
-	abbrev: 'KST',
-	offset: 540,
-}, {
-	name: 'Asia/Shanghai',
-	abbrev: 'CST',
-	offset: 480,
-}, {
-	name: 'Australia/Sydney',
-	abbrev: 'AEST',
-	offset: 600,
-}, {
-	name: 'Australia/Darwin',
-	abbrev: 'ACST',
-	offset: 570,
-}, {
-	name: 'Australia/Perth',
-	abbrev: 'AWST',
-	offset: 480,
-}, {
-	name: 'America/New_York',
-	abbrev: 'EST',
-	offset: -300,
-}, {
-	name: 'America/Mexico_City',
-	abbrev: 'CST',
-	offset: -360,
-}, {
-	name: 'America/Phoenix',
-	abbrev: 'MST',
-	offset: -420,
-}, {
-	name: 'America/Los_Angeles',
-	abbrev: 'PST',
-	offset: -480,
-}];
diff --git a/packages/frontend/src/scripts/touch.ts b/packages/frontend/src/scripts/touch.ts
deleted file mode 100644
index 13c9d648dc..0000000000
--- a/packages/frontend/src/scripts/touch.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { ref } from 'vue';
-import { deviceKind } from '@/scripts/device-kind.js';
-
-const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
-
-export let isTouchUsing = deviceKind === 'tablet' || deviceKind === 'smartphone';
-
-if (isTouchSupported && !isTouchUsing) {
-	window.addEventListener('touchstart', () => {
-		// maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも
-		// タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする
-		isTouchUsing = true;
-	}, { passive: true });
-}
-
-/** (MkHorizontalSwipe) 横スワイプ中か？ */
-export const isHorizontalSwipeSwiping = ref(false);
diff --git a/packages/frontend/src/scripts/unison-reload.ts b/packages/frontend/src/scripts/unison-reload.ts
deleted file mode 100644
index a24941d02e..0000000000
--- a/packages/frontend/src/scripts/unison-reload.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-// SafariがBroadcastChannel未実装なのでライブラリを使う
-import { BroadcastChannel } from 'broadcast-channel';
-
-export const reloadChannel = new BroadcastChannel<string | null>('reload');
-
-// BroadcastChannelを用いて、クライアントが一斉にreloadするようにします。
-export function unisonReload(path?: string) {
-	if (path !== undefined) {
-		reloadChannel.postMessage(path);
-		location.href = path;
-	} else {
-		reloadChannel.postMessage(null);
-		location.reload();
-	}
-}
diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts
deleted file mode 100644
index d105a318a7..0000000000
--- a/packages/frontend/src/scripts/upload.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { reactive, ref } from 'vue';
-import * as Misskey from 'misskey-js';
-import { v4 as uuid } from 'uuid';
-import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
-import { apiUrl } from '@@/js/config.js';
-import { getCompressionConfig } from './upload/compress-config.js';
-import { $i } from '@/account.js';
-import { alert } from '@/os.js';
-import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
-import { prefer } from '@/preferences.js';
-
-type Uploading = {
-	id: string;
-	name: string;
-	progressMax: number | undefined;
-	progressValue: number | undefined;
-	img: string;
-};
-export const uploads = ref<Uploading[]>([]);
-
-const mimeTypeMap = {
-	'image/webp': 'webp',
-	'image/jpeg': 'jpg',
-	'image/png': 'png',
-} as const;
-
-export function uploadFile(
-	file: File,
-	folder?: string | Misskey.entities.DriveFolder,
-	name?: string,
-	keepOriginal: boolean = prefer.s.keepOriginalUploading,
-): Promise<Misskey.entities.DriveFile> {
-	if ($i == null) throw new Error('Not logged in');
-
-	const _folder = typeof folder === 'string' ? folder : folder?.id;
-
-	if (file.size > instance.maxFileSize) {
-		alert({
-			type: 'error',
-			title: i18n.ts.failedToUpload,
-			text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
-		});
-		return Promise.reject();
-	}
-
-	return new Promise((resolve, reject) => {
-		const id = uuid();
-
-		const reader = new FileReader();
-		reader.onload = async (): Promise<void> => {
-			const filename = name ?? file.name ?? 'untitled';
-			const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
-
-			const ctx = reactive<Uploading>({
-				id,
-				name: prefer.s.keepOriginalFilename ? filename : id + extension,
-				progressMax: undefined,
-				progressValue: undefined,
-				img: window.URL.createObjectURL(file),
-			});
-
-			uploads.value.push(ctx);
-
-			const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
-			let resizedImage: Blob | undefined;
-			if (config) {
-				try {
-					const resized = await readAndCompressImage(file, config);
-					if (resized.size < file.size || file.type === 'image/webp') {
-						// The compression may not always reduce the file size
-						// (and WebP is not browser safe yet)
-						resizedImage = resized;
-					}
-					if (_DEV_) {
-						const saved = ((1 - resized.size / file.size) * 100).toFixed(2);
-						console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
-					}
-
-					ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name;
-				} catch (err) {
-					console.error('Failed to resize image', err);
-				}
-			}
-
-			const formData = new FormData();
-			formData.append('i', $i!.token);
-			formData.append('force', 'true');
-			formData.append('file', resizedImage ?? file);
-			formData.append('name', ctx.name);
-			if (_folder) formData.append('folderId', _folder);
-
-			const xhr = new XMLHttpRequest();
-			xhr.open('POST', apiUrl + '/drive/files/create', true);
-			xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => {
-				if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
-					// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
-					uploads.value = uploads.value.filter(x => x.id !== id);
-
-					if (xhr.status === 413) {
-						alert({
-							type: 'error',
-							title: i18n.ts.failedToUpload,
-							text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
-						});
-					} else if (ev.target?.response) {
-						const res = JSON.parse(ev.target.response);
-						if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
-							alert({
-								type: 'error',
-								title: i18n.ts.failedToUpload,
-								text: i18n.ts.cannotUploadBecauseInappropriate,
-							});
-						} else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
-							alert({
-								type: 'error',
-								title: i18n.ts.failedToUpload,
-								text: i18n.ts.cannotUploadBecauseNoFreeSpace,
-							});
-						} else {
-							alert({
-								type: 'error',
-								title: i18n.ts.failedToUpload,
-								text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
-							});
-						}
-					} else {
-						alert({
-							type: 'error',
-							title: 'Failed to upload',
-							text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
-						});
-					}
-
-					reject();
-					return;
-				}
-
-				const driveFile = JSON.parse(ev.target.response);
-
-				resolve(driveFile);
-
-				uploads.value = uploads.value.filter(x => x.id !== id);
-			}) as (ev: ProgressEvent<EventTarget>) => any;
-
-			xhr.upload.onprogress = ev => {
-				if (ev.lengthComputable) {
-					ctx.progressMax = ev.total;
-					ctx.progressValue = ev.loaded;
-				}
-			};
-
-			xhr.send(formData);
-		};
-		reader.readAsArrayBuffer(file);
-	});
-}
diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts
deleted file mode 100644
index 3046b7f518..0000000000
--- a/packages/frontend/src/scripts/upload/compress-config.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import isAnimated from 'is-file-animated';
-import { isWebpSupported } from './isWebpSupported.js';
-import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer';
-
-const compressTypeMap = {
-	'image/jpeg': { quality: 0.90, mimeType: 'image/webp' },
-	'image/png': { quality: 1, mimeType: 'image/webp' },
-	'image/webp': { quality: 0.90, mimeType: 'image/webp' },
-	'image/svg+xml': { quality: 1, mimeType: 'image/webp' },
-} as const;
-
-const compressTypeMapFallback = {
-	'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' },
-	'image/png': { quality: 1, mimeType: 'image/png' },
-	'image/webp': { quality: 0.85, mimeType: 'image/jpeg' },
-	'image/svg+xml': { quality: 1, mimeType: 'image/png' },
-} as const;
-
-export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfigWithConvertedOutput | undefined> {
-	const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
-	if (!imgConfig || await isAnimated(file)) {
-		return;
-	}
-
-	return {
-		maxWidth: 2048,
-		maxHeight: 2048,
-		debug: true,
-		...imgConfig,
-	};
-}
diff --git a/packages/frontend/src/scripts/upload/isWebpSupported.ts b/packages/frontend/src/scripts/upload/isWebpSupported.ts
deleted file mode 100644
index 2511236ecc..0000000000
--- a/packages/frontend/src/scripts/upload/isWebpSupported.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-let isWebpSupportedCache: boolean | undefined;
-export function isWebpSupported() {
-	if (isWebpSupportedCache === undefined) {
-		const canvas = document.createElement('canvas');
-		canvas.width = 1;
-		canvas.height = 1;
-		isWebpSupportedCache = canvas.toDataURL('image/webp').startsWith('data:image/webp');
-	}
-	return isWebpSupportedCache;
-}
diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts
deleted file mode 100644
index bba64fc6ee..0000000000
--- a/packages/frontend/src/scripts/use-chart-tooltip.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { onUnmounted, onDeactivated, ref } from 'vue';
-import * as os from '@/os.js';
-import MkChartTooltip from '@/components/MkChartTooltip.vue';
-
-export function useChartTooltip(opts: { position: 'top' | 'middle' } = { position: 'top' }) {
-	const tooltipShowing = ref(false);
-	const tooltipX = ref(0);
-	const tooltipY = ref(0);
-	const tooltipTitle = ref<string | null>(null);
-	const tooltipSeries = ref<{
-		backgroundColor: string;
-		borderColor: string;
-		text: string;
-	}[] | null>(null);
-	const { dispose: disposeTooltipComponent } = os.popup(MkChartTooltip, {
-		showing: tooltipShowing,
-		x: tooltipX,
-		y: tooltipY,
-		title: tooltipTitle,
-		series: tooltipSeries,
-	}, {});
-
-	onUnmounted(() => {
-		disposeTooltipComponent();
-	});
-
-	onDeactivated(() => {
-		tooltipShowing.value = false;
-	});
-
-	function handler(context) {
-		if (context.tooltip.opacity === 0) {
-			tooltipShowing.value = false;
-			return;
-		}
-
-		tooltipTitle.value = context.tooltip.title[0];
-		tooltipSeries.value = context.tooltip.body.map((b, i) => ({
-			backgroundColor: context.tooltip.labelColors[i].backgroundColor,
-			borderColor: context.tooltip.labelColors[i].borderColor,
-			text: b.lines[0],
-		}));
-
-		const rect = context.chart.canvas.getBoundingClientRect();
-
-		tooltipShowing.value = true;
-		tooltipX.value = rect.left + window.scrollX + context.tooltip.caretX;
-		if (opts.position === 'top') {
-			tooltipY.value = rect.top + window.scrollY;
-		} else if (opts.position === 'middle') {
-			tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY;
-		}
-	}
-
-	return {
-		handler,
-	};
-}
diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/scripts/use-form.ts
deleted file mode 100644
index 26cca839c3..0000000000
--- a/packages/frontend/src/scripts/use-form.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { computed, reactive, watch } from 'vue';
-import type { Reactive } from 'vue';
-
-function copy<T>(v: T): T {
-	return JSON.parse(JSON.stringify(v));
-}
-
-function unwrapReactive<T>(v: Reactive<T>): T {
-	return JSON.parse(JSON.stringify(v));
-}
-
-export function useForm<T extends Record<string, any>>(initialState: T, save: (newState: T) => Promise<void>) {
-	const currentState = reactive<T>(copy(initialState));
-	const previousState = reactive<T>(copy(initialState));
-
-	const modifiedStates = reactive<Record<keyof T, boolean>>({} as any);
-	for (const key in currentState) {
-		modifiedStates[key] = false;
-	}
-	const modified = computed(() => Object.values(modifiedStates).some(v => v));
-	const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length);
-
-	watch([currentState, previousState], () => {
-		for (const key in modifiedStates) {
-			modifiedStates[key] = currentState[key] !== previousState[key];
-		}
-	}, { deep: true });
-
-	async function _save() {
-		await save(unwrapReactive(currentState));
-		for (const key in currentState) {
-			previousState[key] = copy(currentState[key]);
-		}
-	}
-
-	function discard() {
-		for (const key in currentState) {
-			currentState[key] = copy(previousState[key]);
-		}
-	}
-
-	return {
-		state: currentState,
-		savedState: previousState,
-		modifiedStates,
-		modified,
-		modifiedCount,
-		save: _save,
-		discard,
-	};
-}
diff --git a/packages/frontend/src/scripts/use-leave-guard.ts b/packages/frontend/src/scripts/use-leave-guard.ts
deleted file mode 100644
index 395c12a756..0000000000
--- a/packages/frontend/src/scripts/use-leave-guard.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Ref } from 'vue';
-
-export function useLeaveGuard(enabled: Ref<boolean>) {
-	/* TODO
-	const setLeaveGuard = inject('setLeaveGuard');
-
-	if (setLeaveGuard) {
-		setLeaveGuard(async () => {
-			if (!enabled.value) return false;
-
-			const { canceled } = await os.confirm({
-				type: 'warning',
-				text: i18n.ts.leaveConfirm,
-			});
-
-			return canceled;
-		});
-	} else {
-		onBeforeRouteLeave(async (to, from) => {
-			if (!enabled.value) return true;
-
-			const { canceled } = await os.confirm({
-				type: 'warning',
-				text: i18n.ts.leaveConfirm,
-			});
-
-			return !canceled;
-		});
-	}
-	*/
-
-	/*
-	function onBeforeLeave(ev: BeforeUnloadEvent) {
-		if (enabled.value) {
-			ev.preventDefault();
-			ev.returnValue = '';
-		}
-	}
-
-	window.addEventListener('beforeunload', onBeforeLeave);
-	onUnmounted(() => {
-		window.removeEventListener('beforeunload', onBeforeLeave);
-	});
-	*/
-}
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
deleted file mode 100644
index 0bc10e90e4..0000000000
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { onUnmounted } from 'vue';
-import type { Ref, ShallowRef } from 'vue';
-import * as Misskey from 'misskey-js';
-import { useStream } from '@/stream.js';
-import { $i } from '@/account.js';
-
-export function useNoteCapture(props: {
-	rootEl: ShallowRef<HTMLElement | undefined>;
-	note: Ref<Misskey.entities.Note>;
-	pureNote: Ref<Misskey.entities.Note>;
-	isDeletedRef: Ref<boolean>;
-}) {
-	const note = props.note;
-	const pureNote = props.pureNote;
-	const connection = $i ? useStream() : null;
-
-	function onStreamNoteUpdated(noteData): void {
-		const { type, id, body } = noteData;
-
-		if ((id !== note.value.id) && (id !== pureNote.value.id)) return;
-
-		switch (type) {
-			case 'reacted': {
-				const reaction = body.reaction;
-
-				if (body.emoji && !(body.emoji.name in note.value.reactionEmojis)) {
-					note.value.reactionEmojis[body.emoji.name] = body.emoji.url;
-				}
-
-				// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
-				const currentCount = (note.value.reactions || {})[reaction] || 0;
-
-				note.value.reactions[reaction] = currentCount + 1;
-				note.value.reactionCount += 1;
-
-				if ($i && (body.userId === $i.id)) {
-					note.value.myReaction = reaction;
-				}
-				break;
-			}
-
-			case 'unreacted': {
-				const reaction = body.reaction;
-
-				// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
-				const currentCount = (note.value.reactions || {})[reaction] || 0;
-
-				note.value.reactions[reaction] = Math.max(0, currentCount - 1);
-				note.value.reactionCount = Math.max(0, note.value.reactionCount - 1);
-				if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction];
-
-				if ($i && (body.userId === $i.id)) {
-					note.value.myReaction = null;
-				}
-				break;
-			}
-
-			case 'pollVoted': {
-				const choice = body.choice;
-
-				const choices = [...note.value.poll.choices];
-				choices[choice] = {
-					...choices[choice],
-					votes: choices[choice].votes + 1,
-					...($i && (body.userId === $i.id) ? {
-						isVoted: true,
-					} : {}),
-				};
-
-				note.value.poll.choices = choices;
-				break;
-			}
-
-			case 'deleted': {
-				props.isDeletedRef.value = true;
-				break;
-			}
-		}
-	}
-
-	function capture(withHandler = false): void {
-		if (connection) {
-			// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
-			connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id });
-			if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id });
-			if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
-		}
-	}
-
-	function decapture(withHandler = false): void {
-		if (connection) {
-			connection.send('un', {
-				id: note.value.id,
-			});
-			if (pureNote.value.id !== note.value.id) {
-				connection.send('un', {
-					id: pureNote.value.id,
-				});
-			}
-			if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
-		}
-	}
-
-	function onStreamConnected() {
-		capture(false);
-	}
-
-	capture(true);
-	if (connection) {
-		connection.on('_connected_', onStreamConnected);
-	}
-
-	onUnmounted(() => {
-		decapture(true);
-		if (connection) {
-			connection.off('_connected_', onStreamConnected);
-		}
-	});
-}
diff --git a/packages/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/scripts/use-tooltip.ts
deleted file mode 100644
index d9ddfc8b5d..0000000000
--- a/packages/frontend/src/scripts/use-tooltip.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { ref, watch, onUnmounted } from 'vue';
-import type { Ref } from 'vue';
-
-export function useTooltip(
-	elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>,
-	onShow: (showing: Ref<boolean>) => void,
-	delay = 300,
-): void {
-	let isHovering = false;
-
-	// iOS(Androidも？)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ
-	// 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる
-	// TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...？
-	let shouldIgnoreMouseover = false;
-
-	let timeoutId: number;
-
-	let changeShowingState: (() => void) | null;
-
-	let autoHidingTimer;
-
-	const open = () => {
-		close();
-		if (!isHovering) return;
-		if (elRef.value == null) return;
-		const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el;
-		if (!document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため
-
-		const showing = ref(true);
-		onShow(showing);
-		changeShowingState = () => {
-			showing.value = false;
-		};
-
-		autoHidingTimer = window.setInterval(() => {
-			if (elRef.value == null || !document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) {
-				if (!isHovering) return;
-				isHovering = false;
-				window.clearTimeout(timeoutId);
-				close();
-				window.clearInterval(autoHidingTimer);
-			}
-		}, 1000);
-	};
-
-	const close = () => {
-		if (changeShowingState != null) {
-			changeShowingState();
-			changeShowingState = null;
-		}
-	};
-
-	const onMouseover = () => {
-		if (isHovering) return;
-		if (shouldIgnoreMouseover) return;
-		isHovering = true;
-		timeoutId = window.setTimeout(open, delay);
-	};
-
-	const onMouseleave = () => {
-		if (!isHovering) return;
-		isHovering = false;
-		window.clearTimeout(timeoutId);
-		window.clearInterval(autoHidingTimer);
-		close();
-	};
-
-	const onTouchstart = () => {
-		shouldIgnoreMouseover = true;
-		if (isHovering) return;
-		isHovering = true;
-		timeoutId = window.setTimeout(open, delay);
-	};
-
-	const onTouchend = () => {
-		if (!isHovering) return;
-		isHovering = false;
-		window.clearTimeout(timeoutId);
-		window.clearInterval(autoHidingTimer);
-		close();
-	};
-
-	const stop = watch(elRef, () => {
-		if (elRef.value) {
-			stop();
-			const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el;
-			el.addEventListener('mouseover', onMouseover, { passive: true });
-			el.addEventListener('mouseleave', onMouseleave, { passive: true });
-			el.addEventListener('touchstart', onTouchstart, { passive: true });
-			el.addEventListener('touchend', onTouchend, { passive: true });
-			el.addEventListener('click', close, { passive: true });
-		}
-	}, {
-		immediate: true,
-		flush: 'post',
-	});
-
-	onUnmounted(() => {
-		close();
-	});
-}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 6e4b4cd0c1..b6bf0e5fba 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -8,12 +8,12 @@ import * as Misskey from 'misskey-js';
 import lightTheme from '@@/themes/l-light.json5';
 import darkTheme from '@@/themes/d-green-lime.json5';
 import { hemisphere } from '@@/js/intl-const.js';
-import type { DeviceKind } from '@/scripts/device-kind.js';
+import type { DeviceKind } from '@/utility/device-kind.js';
 import type { Plugin } from '@/plugin.js';
 import type { Column } from '@/deck.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { Storage } from '@/pizzax.js';
-import { DEFAULT_DEVICE_KIND } from '@/scripts/device-kind.js';
+import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
 
 interface PostFormAction {
 	title: string,
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index e63dac951c..e194e96a7f 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -8,7 +8,7 @@ import { markRaw } from 'vue';
 import { $i } from '@/account.js';
 import { wsOrigin } from '@@/js/config.js';
 // TODO: No WebsocketモードでStreamMockが使えそう
-//import { StreamMock } from '@/scripts/stream-mock.js';
+//import { StreamMock } from '@/utility/stream-mock.js';
 
 // heart beat interval in ms
 const HEART_BEAT_INTERVAL = 1000 * 60;
diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts
index 09c665a2ab..93fbe395f9 100644
--- a/packages/frontend/src/theme-store.ts
+++ b/packages/frontend/src/theme-store.ts
@@ -3,8 +3,8 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import type { Theme } from '@/scripts/theme.js';
-import { getBuiltinThemes } from '@/scripts/theme.js';
+import type { Theme } from '@/utility/theme.js';
+import { getBuiltinThemes } from '@/utility/theme.js';
 import { $i } from '@/account.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 51645f9676..e218cd8c62 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -50,9 +50,9 @@ import * as Misskey from 'misskey-js';
 import { swInject } from './sw-inject.js';
 import XNotification from './notification.vue';
 import { popups } from '@/os.js';
-import { pendingApiRequestsCount } from '@/scripts/misskey-api.js';
-import { uploads } from '@/scripts/upload.js';
-import * as sound from '@/scripts/sound.js';
+import { pendingApiRequestsCount } from '@/utility/misskey-api.js';
+import { uploads } from '@/utility/upload.js';
+import * as sound from '@/utility/sound.js';
 import { $i } from '@/account.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 1fb99f9f22..1797007f4a 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -97,7 +97,7 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
 import { useRouter } from '@/router/supplier.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index e234bb3a33..16e72fa227 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -34,9 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
 	display?: 'marquee' | 'oneByOne';
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index da8fa8bb21..4da89a181e 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -31,7 +31,7 @@ import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
 import { useInterval } from '@@/js/use-interval.js';
-import { shuffle } from '@/scripts/shuffle.js';
+import { shuffle } from '@/utility/shuffle.js';
 
 const props = defineProps<{
 	url?: string;
diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
index 078b595dca..c5bee51162 100644
--- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
@@ -34,9 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { getNoteSummary } from '@/scripts/get-note-summary.js';
+import { getNoteSummary } from '@/utility/get-note-summary.js';
 import { notePage } from '@/filters/note.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts
index ff851ad99f..df392c6532 100644
--- a/packages/frontend/src/ui/_common_/sw-inject.ts
+++ b/packages/frontend/src/ui/_common_/sw-inject.ts
@@ -4,10 +4,10 @@
  */
 
 import { post } from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { $i, login } from '@/account.js';
-import { getAccountFromId } from '@/scripts/get-account-from-id.js';
-import { deepClone } from '@/scripts/clone.js';
+import { getAccountFromId } from '@/utility/get-account-from-id.js';
+import { deepClone } from '@/utility/clone.js';
 import { mainRouter } from '@/router/main.js';
 
 export function swInject() {
diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue
index c7d1387eae..3e5653e46d 100644
--- a/packages/frontend/src/ui/_common_/upload.vue
+++ b/packages/frontend/src/ui/_common_/upload.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { } from 'vue';
 import * as os from '@/os.js';
-import { uploads } from '@/scripts/upload.js';
+import { uploads } from '@/utility/upload.js';
 import { i18n } from '@/i18n.js';
 
 const zIndex = os.claimZIndex('high');
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index c11771f028..747fa2ab85 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -56,7 +56,7 @@ import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { openAccountMenu as openAccountMenu_, $i } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
-// import { StickySidebar } from '@/scripts/sticky-sidebar.js';
+// import { StickySidebar } from '@/utility/sticky-sidebar.js';
 // import { mainRouter } from '@/router.js';
 //import MisskeyLogo from '@assets/client/misskey.svg';
 import { store } from '@/store.js';
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 63c60a3d6f..abe612ab3e 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -51,10 +51,10 @@ import { instanceName } from '@@/js/config.js';
 import { isLink } from '@@/js/is-link.js';
 import XSidebar from './classic.sidebar.vue';
 import XCommon from './_common_/common.vue';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
-import { StickySidebar } from '@/scripts/sticky-sidebar.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
+import { StickySidebar } from '@/utility/sticky-sidebar.js';
 import * as os from '@/os.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 2107f6f53a..b723f92ad6 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -103,8 +103,8 @@ import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { unisonReload } from '@/utility/unison-reload.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import { prefer } from '@/preferences.js';
 import XMainColumn from '@/ui/deck/main-column.vue';
 import XTlColumn from '@/ui/deck/tl-column.vue';
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index 5f0c607edb..ae282ba324 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -23,11 +23,11 @@ import type { SoundStore } from '@/preferences/def.js';
 import { updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { antennasCache } from '@/cache.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index f002f655f7..964b42874e 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -30,10 +30,10 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { favoritedChannelsCache } from '@/cache.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index f627ab5262..2b4e86b8a2 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -23,11 +23,11 @@ import type { SoundStore } from '@/preferences/def.js';
 import { updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { userListsCache } from '@/cache.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 906e94f0b3..bab938af20 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -24,10 +24,10 @@ import { getScrollContainer } from '@@/js/scroll.js';
 import { isLink } from '@@/js/is-link.js';
 import XColumn from './column.vue';
 import type { Column } from '@/deck.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 4d1be08706..5d07440017 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -22,10 +22,10 @@ import type { SoundStore } from '@/preferences/def.js';
 import { updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 96da944398..2dec7bf5aa 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -43,7 +43,7 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/tl-note-notification.ts b/packages/frontend/src/ui/deck/tl-note-notification.ts
index 277ebb0511..728c0d0d29 100644
--- a/packages/frontend/src/ui/deck/tl-note-notification.ts
+++ b/packages/frontend/src/ui/deck/tl-note-notification.ts
@@ -5,9 +5,9 @@
 
 import * as Misskey from 'misskey-js';
 import type { Ref } from 'vue';
-import type { SoundType } from '@/scripts/sound.js';
+import type { SoundType } from '@/utility/sound.js';
 import type { SoundStore } from '@/preferences/def.js';
-import { getSoundDuration, playMisskeySfxFile, soundsTypes } from '@/scripts/sound.js';
+import { getSoundDuration, playMisskeySfxFile, soundsTypes } from '@/utility/sound.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
index 5cbcd69b2c..919a6f2246 100644
--- a/packages/frontend/src/ui/minimum.vue
+++ b/packages/frontend/src/ui/minimum.vue
@@ -16,8 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, provide, ref } from 'vue';
 import XCommon from './_common_/common.vue';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 import { instanceName } from '@@/js/config.js';
 import { mainRouter } from '@/router/main.js';
 
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index bddb62dc60..e012d615a8 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -103,15 +103,15 @@ import { isLink } from '@@/js/is-link.js';
 import XCommon from './_common_/common.vue';
 import type { Ref } from 'vue';
 import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
 import { store } from '@/store.js';
 import { navbarItemDef } from '@/navbar.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 9ba9786ca8..3125f74922 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -72,12 +72,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, provide, ref, computed } from 'vue';
 import { instanceName } from '@@/js/config.js';
 import XCommon from './_common_/common.vue';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 import * as os from '@/os.js';
 import { instance } from '@/instance.js';
 import XSigninDialog from '@/components/MkSigninDialog.vue';
 import XSignupDialog from '@/components/MkSignupDialog.vue';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
 import { mainRouter } from '@/router/main.js';
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index 2e31d056c1..f4db5d30fe 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, provide, ref } from 'vue';
 import XCommon from './_common_/common.vue';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
+import type { PageMetadata } from '@/utility/page-metadata.js';
 import { instanceName, ui } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { mainRouter } from '@/router/main.js';
diff --git a/packages/frontend/src/utility/achievements.ts b/packages/frontend/src/utility/achievements.ts
new file mode 100644
index 0000000000..3025a985ba
--- /dev/null
+++ b/packages/frontend/src/utility/achievements.ts
@@ -0,0 +1,513 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { $i } from '@/account.js';
+
+export const ACHIEVEMENT_TYPES = [
+	'notes1',
+	'notes10',
+	'notes100',
+	'notes500',
+	'notes1000',
+	'notes5000',
+	'notes10000',
+	'notes20000',
+	'notes30000',
+	'notes40000',
+	'notes50000',
+	'notes60000',
+	'notes70000',
+	'notes80000',
+	'notes90000',
+	'notes100000',
+	'login3',
+	'login7',
+	'login15',
+	'login30',
+	'login60',
+	'login100',
+	'login200',
+	'login300',
+	'login400',
+	'login500',
+	'login600',
+	'login700',
+	'login800',
+	'login900',
+	'login1000',
+	'passedSinceAccountCreated1',
+	'passedSinceAccountCreated2',
+	'passedSinceAccountCreated3',
+	'loggedInOnBirthday',
+	'loggedInOnNewYearsDay',
+	'noteClipped1',
+	'noteFavorited1',
+	'myNoteFavorited1',
+	'profileFilled',
+	'markedAsCat',
+	'following1',
+	'following10',
+	'following50',
+	'following100',
+	'following300',
+	'followers1',
+	'followers10',
+	'followers50',
+	'followers100',
+	'followers300',
+	'followers500',
+	'followers1000',
+	'collectAchievements30',
+	'viewAchievements3min',
+	'iLoveMisskey',
+	'foundTreasure',
+	'client30min',
+	'client60min',
+	'noteDeletedWithin1min',
+	'postedAtLateNight',
+	'postedAt0min0sec',
+	'selfQuote',
+	'htl20npm',
+	'viewInstanceChart',
+	'outputHelloWorldOnScratchpad',
+	'open3windows',
+	'driveFolderCircularReference',
+	'reactWithoutRead',
+	'clickedClickHere',
+	'justPlainLucky',
+	'setNameToSyuilo',
+	'cookieClicked',
+	'brainDiver',
+	'smashTestNotificationButton',
+	'tutorialCompleted',
+	'bubbleGameExplodingHead',
+	'bubbleGameDoubleExplodingHead',
+] as const;
+
+export const ACHIEVEMENT_BADGES = {
+	'notes1': {
+		img: '/fluent-emoji/1f4dd.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'notes10': {
+		img: '/fluent-emoji/1f4d1.png',
+		bg: null,
+		frame: 'bronze',
+	},
+	'notes100': {
+		img: '/fluent-emoji/1f4d2.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'notes500': {
+		img: '/fluent-emoji/1f4da.png',
+		bg: null,
+		frame: 'bronze',
+	},
+	'notes1000': {
+		img: '/fluent-emoji/1f5c3.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'notes5000': {
+		img: '/fluent-emoji/1f304.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'notes10000': {
+		img: '/fluent-emoji/1f3d9.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'silver',
+	},
+	'notes20000': {
+		img: '/fluent-emoji/1f307.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'silver',
+	},
+	'notes30000': {
+		img: '/fluent-emoji/1f306.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'silver',
+	},
+	'notes40000': {
+		img: '/fluent-emoji/1f303.png',
+		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
+		frame: 'silver',
+	},
+	'notes50000': {
+		img: '/fluent-emoji/1fa90.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'gold',
+	},
+	'notes60000': {
+		img: '/fluent-emoji/2604.png',
+		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
+		frame: 'gold',
+	},
+	'notes70000': {
+		img: '/fluent-emoji/1f30c.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'gold',
+	},
+	'notes80000': {
+		img: '/fluent-emoji/1f30c.png',
+		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
+		frame: 'gold',
+	},
+	'notes90000': {
+		img: '/fluent-emoji/1f30c.png',
+		bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
+		frame: 'gold',
+	},
+	'notes100000': {
+		img: '/fluent-emoji/267e.png',
+		bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
+		frame: 'platinum',
+	},
+	'login3': {
+		img: '/fluent-emoji/1f331.png',
+		bg: null,
+		frame: 'bronze',
+	},
+	'login7': {
+		img: '/fluent-emoji/1f331.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'login15': {
+		img: '/fluent-emoji/1f331.png',
+		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
+		frame: 'bronze',
+	},
+	'login30': {
+		img: '/fluent-emoji/1fab4.png',
+		bg: null,
+		frame: 'bronze',
+	},
+	'login60': {
+		img: '/fluent-emoji/1fab4.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'login100': {
+		img: '/fluent-emoji/1fab4.png',
+		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
+		frame: 'silver',
+	},
+	'login200': {
+		img: '/fluent-emoji/1f333.png',
+		bg: null,
+		frame: 'silver',
+	},
+	'login300': {
+		img: '/fluent-emoji/1f333.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'silver',
+	},
+	'login400': {
+		img: '/fluent-emoji/1f333.png',
+		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
+		frame: 'silver',
+	},
+	'login500': {
+		img: '/fluent-emoji/1f304.png',
+		bg: null,
+		frame: 'silver',
+	},
+	'login600': {
+		img: '/fluent-emoji/1f304.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'gold',
+	},
+	'login700': {
+		img: '/fluent-emoji/1f304.png',
+		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
+		frame: 'gold',
+	},
+	'login800': {
+		img: '/fluent-emoji/1f307.png',
+		bg: null,
+		frame: 'gold',
+	},
+	'login900': {
+		img: '/fluent-emoji/1f307.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'gold',
+	},
+	'login1000': {
+		img: '/fluent-emoji/1f307.png',
+		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
+		frame: 'platinum',
+	},
+	'noteClipped1': {
+		img: '/fluent-emoji/1f587.png',
+		bg: null,
+		frame: 'bronze',
+	},
+	'noteFavorited1': {
+		img: '/fluent-emoji/1f31f.png',
+		bg: null,
+		frame: 'bronze',
+	},
+	'myNoteFavorited1': {
+		img: '/fluent-emoji/1f320.png',
+		bg: null,
+		frame: 'silver',
+	},
+	'profileFilled': {
+		img: '/fluent-emoji/1f44c.png',
+		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
+		frame: 'bronze',
+	},
+	'markedAsCat': {
+		img: '/fluent-emoji/1f408.png',
+		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
+		frame: 'bronze',
+	},
+	'following1': {
+		img: '/fluent-emoji/2618.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'following10': {
+		img: '/fluent-emoji/1f6b8.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'following50': {
+		img: '/fluent-emoji/1f91d.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'following100': {
+		img: '/fluent-emoji/1f4af.png',
+		bg: 'linear-gradient(0deg, rgb(255 53 184), rgb(255 206 69))',
+		frame: 'silver',
+	},
+	'following300': {
+		img: '/fluent-emoji/1f970.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'silver',
+	},
+	'followers1': {
+		img: '/fluent-emoji/2618.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'followers10': {
+		img: '/fluent-emoji/1f44b.png',
+		bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
+		frame: 'bronze',
+	},
+	'followers50': {
+		img: '/fluent-emoji/1f411.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'bronze',
+	},
+	'followers100': {
+		img: '/fluent-emoji/1f60e.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'silver',
+	},
+	'followers300': {
+		img: '/fluent-emoji/1f3c6.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'silver',
+	},
+	'followers500': {
+		img: '/fluent-emoji/1f4e1.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'gold',
+	},
+	'followers1000': {
+		img: '/fluent-emoji/1f451.png',
+		bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
+		frame: 'platinum',
+	},
+	'collectAchievements30': {
+		img: '/fluent-emoji/1f3c5.png',
+		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
+		frame: 'silver',
+	},
+	'viewAchievements3min': {
+		img: '/fluent-emoji/1f3c5.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'bronze',
+	},
+	'iLoveMisskey': {
+		img: '/fluent-emoji/2764.png',
+		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
+		frame: 'silver',
+	},
+	'foundTreasure': {
+		img: '/fluent-emoji/1f3c6.png',
+		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
+		frame: 'gold',
+	},
+	'client30min': {
+		img: '/fluent-emoji/1f552.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'bronze',
+	},
+	'client60min': {
+		img: '/fluent-emoji/1f552.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'silver',
+	},
+	'noteDeletedWithin1min': {
+		img: '/fluent-emoji/1f5d1.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'bronze',
+	},
+	'postedAtLateNight': {
+		img: '/fluent-emoji/1f319.png',
+		bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
+		frame: 'bronze',
+	},
+	'postedAt0min0sec': {
+		img: '/fluent-emoji/1f55b.png',
+		bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))',
+		frame: 'bronze',
+	},
+	'selfQuote': {
+		img: '/fluent-emoji/1f4dd.png',
+		bg: null,
+		frame: 'bronze',
+	},
+	'htl20npm': {
+		img: '/fluent-emoji/1f30a.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'bronze',
+	},
+	'viewInstanceChart': {
+		img: '/fluent-emoji/1f4ca.png',
+		bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))',
+		frame: 'bronze',
+	},
+	'outputHelloWorldOnScratchpad': {
+		img: '/fluent-emoji/1f530.png',
+		bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))',
+		frame: 'bronze',
+	},
+	'open3windows': {
+		img: '/fluent-emoji/1f5a5.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'bronze',
+	},
+	'driveFolderCircularReference': {
+		img: '/fluent-emoji/1f4c2.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'bronze',
+	},
+	'reactWithoutRead': {
+		img: '/fluent-emoji/2753.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'bronze',
+	},
+	'clickedClickHere': {
+		img: '/fluent-emoji/2757.png',
+		bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
+		frame: 'bronze',
+	},
+	'justPlainLucky': {
+		img: '/fluent-emoji/1f340.png',
+		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
+		frame: 'silver',
+	},
+	'setNameToSyuilo': {
+		img: '/fluent-emoji/1f36e.png',
+		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
+		frame: 'bronze',
+	},
+	'passedSinceAccountCreated1': {
+		img: '/fluent-emoji/0031-20e3.png',
+		bg: null,
+		frame: 'bronze',
+	},
+	'passedSinceAccountCreated2': {
+		img: '/fluent-emoji/0032-20e3.png',
+		bg: null,
+		frame: 'silver',
+	},
+	'passedSinceAccountCreated3': {
+		img: '/fluent-emoji/0033-20e3.png',
+		bg: null,
+		frame: 'gold',
+	},
+	'loggedInOnBirthday': {
+		img: '/fluent-emoji/1f382.png',
+		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
+		frame: 'silver',
+	},
+	'loggedInOnNewYearsDay': {
+		img: '/fluent-emoji/1f38d.png',
+		bg: 'linear-gradient(0deg, rgb(255 144 144), rgb(255 232 168))',
+		frame: 'silver',
+	},
+	'cookieClicked': {
+		img: '/fluent-emoji/1f36a.png',
+		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
+		frame: 'bronze',
+	},
+	'brainDiver': {
+		img: '/fluent-emoji/1f9e0.png',
+		bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
+		frame: 'bronze',
+	},
+	'smashTestNotificationButton': {
+		img: '/fluent-emoji/1f514.png',
+		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
+		frame: 'bronze',
+	},
+	'tutorialCompleted': {
+		img: '/fluent-emoji/1f393.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'bronze',
+	},
+	'bubbleGameExplodingHead': {
+		img: '/fluent-emoji/1f92f.png',
+		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
+		frame: 'bronze',
+	},
+	'bubbleGameDoubleExplodingHead': {
+		img: '/fluent-emoji/1f92f.png',
+		bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
+		frame: 'silver',
+	},
+/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
+} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
+	img: string;
+	bg: string | null;
+	frame: 'bronze' | 'silver' | 'gold' | 'platinum';
+}>;
+ */
+} as const;
+
+export const claimedAchievements: typeof ACHIEVEMENT_TYPES[number][] = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : [];
+
+const claimingQueue = new Set<string>();
+
+export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
+	if ($i == null) return;
+	if ($i.movedTo) return;
+	if (claimedAchievements.includes(type)) return;
+	claimingQueue.add(type);
+	claimedAchievements.push(type);
+	await new Promise(resolve => setTimeout(resolve, (claimingQueue.size - 1) * 500));
+	window.setTimeout(() => {
+		claimingQueue.delete(type);
+	}, 500);
+	misskeyApi('i/claim-achievement', { name: type });
+}
+
+if (_DEV_) {
+	(window as any).unlockAllAchievements = () => {
+		for (const t of ACHIEVEMENT_TYPES) {
+			claimAchievement(t);
+		}
+	};
+}
diff --git a/packages/frontend/src/utility/admin-lookup.ts b/packages/frontend/src/utility/admin-lookup.ts
new file mode 100644
index 0000000000..7405e229fe
--- /dev/null
+++ b/packages/frontend/src/utility/admin-lookup.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+
+export async function lookupUser() {
+	const { canceled, result } = await os.inputText({
+		title: i18n.ts.usernameOrUserId,
+	});
+	if (canceled) return;
+
+	const show = (user) => {
+		os.pageWindow(`/admin/user/${user.id}`);
+	};
+
+	const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result));
+	const idPromise = misskeyApi('users/show', { userId: result });
+	let _notFound = false;
+	const notFound = () => {
+		if (_notFound) {
+			os.alert({
+				type: 'error',
+				text: i18n.ts.noSuchUser,
+			});
+		} else {
+			_notFound = true;
+		}
+	};
+	usernamePromise.then(show).catch(err => {
+		if (err.code === 'NO_SUCH_USER') {
+			notFound();
+		}
+	});
+	idPromise.then(show).catch(err => {
+		notFound();
+	});
+}
+
+export async function lookupUserByEmail() {
+	const { canceled, result } = await os.inputText({
+		title: i18n.ts.emailAddress,
+		type: 'email',
+	});
+	if (canceled) return;
+
+	try {
+		const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result });
+
+		os.pageWindow(`/admin/user/${user.id}`);
+	} catch (err) {
+		if (err.code === 'USER_NOT_FOUND') {
+			os.alert({
+				type: 'error',
+				text: i18n.ts.noSuchUser,
+			});
+		} else {
+			throw err;
+		}
+	}
+}
+
+export async function lookupFile() {
+	const { canceled, result: q } = await os.inputText({
+		title: i18n.ts.fileIdOrUrl,
+		minLength: 1,
+	});
+	if (canceled) return;
+
+	const show = (file) => {
+		os.pageWindow(`/admin/file/${file.id}`);
+	};
+
+	misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
+		show(file);
+	}).catch(err => {
+		if (err.code === 'NO_SUCH_FILE') {
+			os.alert({
+				type: 'error',
+				text: i18n.ts.notFound,
+			});
+		}
+	});
+}
diff --git a/packages/frontend/src/utility/array.ts b/packages/frontend/src/utility/array.ts
new file mode 100644
index 0000000000..f2feb29dfc
--- /dev/null
+++ b/packages/frontend/src/utility/array.ts
@@ -0,0 +1,118 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+type EndoRelation<T> = (a: T, b: T) => boolean;
+type Predicate<T> = (x: T) => boolean;
+
+/**
+ * Count the number of elements that satisfy the predicate
+ */
+
+export function countIf<T>(f: Predicate<T>, xs: T[]): number {
+	return xs.filter(f).length;
+}
+
+/**
+ * Count the number of elements that is equal to the element
+ */
+export function count<T>(a: T, xs: T[]): number {
+	return countIf(x => x === a, xs);
+}
+
+/**
+ * Concatenate an array of arrays
+ */
+export function concat<T>(xss: T[][]): T[] {
+	return ([] as T[]).concat(...xss);
+}
+
+/**
+ * Intersperse the element between the elements of the array
+ * @param sep The element to be interspersed
+ */
+export function intersperse<T>(sep: T, xs: T[]): T[] {
+	return concat(xs.map(x => [sep, x])).slice(1);
+}
+
+/**
+ * Returns the array of elements that is not equal to the element
+ */
+export function erase<T>(a: T, xs: T[]): T[] {
+	return xs.filter(x => x !== a);
+}
+
+/**
+ * Finds the array of all elements in the first array not contained in the second array.
+ * The order of result values are determined by the first array.
+ */
+export function difference<T>(xs: T[], ys: T[]): T[] {
+	return xs.filter(x => !ys.includes(x));
+}
+
+/**
+ * Remove all but the first element from every group of equivalent elements
+ */
+export function unique<T>(xs: T[]): T[] {
+	return [...new Set(xs)];
+}
+
+export function uniqueBy<TValue, TKey>(values: TValue[], keySelector: (value: TValue) => TKey): TValue[] {
+	const map = new Map<TKey, TValue>();
+
+	for (const value of values) {
+		const key = keySelector(value);
+		if (!map.has(key)) map.set(key, value);
+	}
+
+	return [...map.values()];
+}
+
+export function sum(xs: number[]): number {
+	return xs.reduce((a, b) => a + b, 0);
+}
+
+export function maximum(xs: number[]): number {
+	return Math.max(...xs);
+}
+
+/**
+ * Compare two arrays by lexicographical order
+ */
+export function lessThan(xs: number[], ys: number[]): boolean {
+	for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
+		if (xs[i] < ys[i]) return true;
+		if (xs[i] > ys[i]) return false;
+	}
+	return xs.length < ys.length;
+}
+
+/**
+ * Returns the longest prefix of elements that satisfy the predicate
+ */
+export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
+	const ys: T[] = [];
+	for (const x of xs) {
+		if (f(x)) {
+			ys.push(x);
+		} else {
+			break;
+		}
+	}
+	return ys;
+}
+
+export function cumulativeSum(xs: number[]): number[] {
+	const ys = Array.from(xs); // deep copy
+	for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
+	return ys;
+}
+
+export function toArray<T>(x: T | T[] | undefined): T[] {
+	return Array.isArray(x) ? x : x != null ? [x] : [];
+}
+
+export function toSingle<T>(x: T | T[] | undefined): T | undefined {
+	return Array.isArray(x) ? x[0] : x;
+}
diff --git a/packages/frontend/src/utility/autocomplete.ts b/packages/frontend/src/utility/autocomplete.ts
new file mode 100644
index 0000000000..9a603b848c
--- /dev/null
+++ b/packages/frontend/src/utility/autocomplete.ts
@@ -0,0 +1,323 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { nextTick, ref, defineAsyncComponent } from 'vue';
+import getCaretCoordinates from 'textarea-caret';
+import { toASCII } from 'punycode.js';
+import type { Ref } from 'vue';
+import { popup } from '@/os.js';
+
+export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
+
+export class Autocomplete {
+	private suggestion: {
+		x: Ref<number>;
+		y: Ref<number>;
+		q: Ref<any>;
+		close: () => void;
+	} | null;
+	private textarea: HTMLInputElement | HTMLTextAreaElement;
+	private currentType: string;
+	private textRef: Ref<string | number | null>;
+	private opening: boolean;
+	private onlyType: SuggestionType[];
+
+	private get text(): string {
+		// Use raw .value to get the latest value
+		// (Because v-model does not update while composition)
+		return this.textarea.value;
+	}
+
+	private set text(text: string) {
+		// Use ref value to notify other watchers
+		// (Because .value setter never fires input/change events)
+		this.textRef.value = text;
+	}
+
+	/**
+	 * 対象のテキストエリアを与えてインスタンスを初期化します。
+	 */
+	constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string | number | null>, onlyType?: SuggestionType[]) {
+		//#region BIND
+		this.onInput = this.onInput.bind(this);
+		this.complete = this.complete.bind(this);
+		this.close = this.close.bind(this);
+		//#endregion
+
+		this.suggestion = null;
+		this.textarea = textarea;
+		this.textRef = textRef;
+		this.opening = false;
+		this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam'];
+
+		this.attach();
+	}
+
+	/**
+	 * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
+	 */
+	public attach() {
+		this.textarea.addEventListener('input', this.onInput);
+	}
+
+	/**
+	 * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
+	 */
+	public detach() {
+		this.textarea.removeEventListener('input', this.onInput);
+		this.close();
+	}
+
+	/**
+	 * テキスト入力時
+	 */
+	private onInput() {
+		const caretPos = this.textarea.selectionStart;
+		const text = this.text.substring(0, caretPos).split('\n').pop()!;
+
+		const mentionIndex = text.lastIndexOf('@');
+		const hashtagIndex = text.lastIndexOf('#');
+		const emojiIndex = text.lastIndexOf(':');
+		const mfmTagIndex = text.lastIndexOf('$');
+		const mfmParamIndex = text.lastIndexOf('.');
+
+		const max = Math.max(
+			mentionIndex,
+			hashtagIndex,
+			emojiIndex,
+			mfmTagIndex);
+
+		if (max === -1) {
+			this.close();
+			return;
+		}
+
+		const afterLastMfmParam = text.split(/\$\[[a-zA-Z]+/).pop();
+
+		const isMention = mentionIndex !== -1;
+		const isHashtag = hashtagIndex !== -1;
+		const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam.includes(' ');
+		const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
+		const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
+
+		let opened = false;
+
+		if (isMention && this.onlyType.includes('user')) {
+			// ユーザのサジェスト中に@を入力すると、その位置から新たにユーザ名を取りなおそうとしてしまう
+			// この動きはリモートユーザのサジェストを阻害するので、@を検知したらその位置よりも前の@を探し、
+			// ホスト名を含むリモートのユーザ名を全て拾えるようにする
+			const mentionIndexAlt = text.lastIndexOf('@', mentionIndex - 1);
+			const username = mentionIndexAlt === -1
+				? text.substring(mentionIndex + 1)
+				: text.substring(mentionIndexAlt + 1);
+			if (username !== '' && username.match(/^[a-zA-Z0-9_@.]+$/)) {
+				this.open('user', username);
+				opened = true;
+			} else if (username === '') {
+				this.open('user', null);
+				opened = true;
+			}
+		}
+
+		if (isHashtag && !opened && this.onlyType.includes('hashtag')) {
+			const hashtag = text.substring(hashtagIndex + 1);
+			if (!hashtag.includes(' ')) {
+				this.open('hashtag', hashtag);
+				opened = true;
+			}
+		}
+
+		if (isEmoji && !opened && this.onlyType.includes('emoji')) {
+			const emoji = text.substring(emojiIndex + 1);
+			if (!emoji.includes(' ')) {
+				this.open('emoji', emoji);
+				opened = true;
+			}
+		}
+
+		if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) {
+			const mfmTag = text.substring(mfmTagIndex + 1);
+			if (!mfmTag.includes(' ')) {
+				this.open('mfmTag', mfmTag.replace('[', ''));
+				opened = true;
+			}
+		}
+
+		if (isMfmParam && !opened && this.onlyType.includes('mfmParam')) {
+			const mfmParam = text.substring(mfmParamIndex + 1);
+			if (!mfmParam.includes(' ')) {
+				this.open('mfmParam', {
+					tag: text.substring(mfmTagIndex + 2, mfmParamIndex),
+					params: mfmParam.split(','),
+				});
+				opened = true;
+			}
+		}
+
+		if (!opened) {
+			this.close();
+		}
+	}
+
+	/**
+	 * サジェストを提示します。
+	 */
+	private async open(type: string, q: any) {
+		if (type !== this.currentType) {
+			this.close();
+		}
+		if (this.opening) return;
+		this.opening = true;
+		this.currentType = type;
+
+		//#region サジェストを表示すべき位置を計算
+		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
+
+		const rect = this.textarea.getBoundingClientRect();
+
+		const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
+		const y = rect.top + caretPosition.top - this.textarea.scrollTop;
+		//#endregion
+
+		if (this.suggestion) {
+			this.suggestion.x.value = x;
+			this.suggestion.y.value = y;
+			this.suggestion.q.value = q;
+
+			this.opening = false;
+		} else {
+			const _x = ref(x);
+			const _y = ref(y);
+			const _q = ref(q);
+
+			const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
+				textarea: this.textarea,
+				close: this.close,
+				type: type,
+				q: _q,
+				x: _x,
+				y: _y,
+			}, {
+				done: (res) => {
+					this.complete(res);
+				},
+			});
+
+			this.suggestion = {
+				q: _q,
+				x: _x,
+				y: _y,
+				close: () => dispose(),
+			};
+
+			this.opening = false;
+		}
+	}
+
+	/**
+	 * サジェストを閉じます。
+	 */
+	private close() {
+		if (this.suggestion == null) return;
+
+		this.suggestion.close();
+		this.suggestion = null;
+
+		this.textarea.focus();
+	}
+
+	/**
+	 * オートコンプリートする
+	 */
+	private complete({ type, value }) {
+		this.close();
+
+		const caret = this.textarea.selectionStart;
+
+		if (type === 'user') {
+			const source = this.text;
+
+			const before = source.substring(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
+			const after = source.substring(caret);
+
+			const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
+
+			// 挿入
+			this.text = `${trimmedBefore}@${acct} ${after}`;
+
+			// キャレットを戻す
+			nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (acct.length + 2);
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		} else if (type === 'hashtag') {
+			const source = this.text;
+
+			const before = source.substring(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
+			const after = source.substring(caret);
+
+			// 挿入
+			this.text = `${trimmedBefore}#${value} ${after}`;
+
+			// キャレットを戻す
+			nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (value.length + 2);
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		} else if (type === 'emoji') {
+			const source = this.text;
+
+			const before = source.substring(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
+			const after = source.substring(caret);
+
+			// 挿入
+			this.text = trimmedBefore + value + after;
+
+			// キャレットを戻す
+			nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + value.length;
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		} else if (type === 'mfmTag') {
+			const source = this.text;
+
+			const before = source.substring(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
+			const after = source.substring(caret);
+
+			// 挿入
+			this.text = `${trimmedBefore}$[${value} ]${after}`;
+
+			// キャレットを戻す
+			nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (value.length + 3);
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		} else if (type === 'mfmParam') {
+			const source = this.text;
+
+			const before = source.substring(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('.'));
+			const after = source.substring(caret);
+
+			// 挿入
+			this.text = `${trimmedBefore}.${value}${after}`;
+
+			// キャレットを戻す
+			nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (value.length + 1);
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		}
+	}
+}
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
new file mode 100644
index 0000000000..8e2a896b17
--- /dev/null
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -0,0 +1,822 @@
+
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// This file was automatically generated by create-search-index.
+// Do not edit this file.
+
+import { i18n } from '@/i18n.js';
+
+export type SearchIndexItem = {
+	id: string;
+	path?: string;
+	label: string;
+	keywords: string[];
+	icon?: string;
+	children?: SearchIndexItem[];
+};
+
+export const searchIndexes: SearchIndexItem[] = [
+	{
+		id: 'flXd1LC7r',
+		children: [
+			{
+				id: 'hB11H5oul',
+				label: i18n.ts.syncDeviceDarkMode,
+				keywords: ['sync', 'device', 'dark', 'light', 'mode'],
+			},
+			{
+				id: 'fDbLtIKeo',
+				label: i18n.ts.themeForLightMode,
+				keywords: ['light', 'theme'],
+			},
+			{
+				id: 'eLOwK5Ia2',
+				label: i18n.ts.themeForDarkMode,
+				keywords: ['dark', 'theme'],
+			},
+			{
+				id: 'ujvMfyzUr',
+				label: i18n.ts.setWallpaper,
+				keywords: ['wallpaper'],
+			},
+		],
+		label: i18n.ts.theme,
+		keywords: ['theme'],
+		path: '/settings/theme',
+		icon: 'ti ti-palette',
+	},
+	{
+		id: '6fFIRXUww',
+		children: [
+			{
+				id: 'nO7NnzqiC',
+				label: i18n.ts.notUseSound,
+				keywords: ['mute'],
+			},
+			{
+				id: 'oALW4ja7U',
+				label: i18n.ts.useSoundOnlyWhenActive,
+				keywords: ['active', 'mute'],
+			},
+			{
+				id: 'BbJK2SKT2',
+				label: i18n.ts.masterVolume,
+				keywords: ['volume', 'master'],
+			},
+		],
+		label: i18n.ts.sounds,
+		keywords: ['sounds'],
+		path: '/settings/sounds',
+		icon: 'ti ti-music',
+	},
+	{
+		id: '5BjnxMfYV',
+		children: [
+			{
+				id: '3UqdSCaFw',
+				children: [
+					{
+						id: '75QPEg57v',
+						label: i18n.ts.changePassword,
+						keywords: [],
+					},
+				],
+				label: i18n.ts.password,
+				keywords: ['password'],
+			},
+			{
+				id: '2fa',
+				children: [
+					{
+						id: 'qCXM0HtJ7',
+						label: i18n.ts.totp,
+						keywords: ['totp', 'app', i18n.ts.totpDescription],
+					},
+					{
+						id: '3g1RePuD9',
+						label: i18n.ts.securityKeyAndPasskey,
+						keywords: ['security', 'key', 'passkey'],
+					},
+					{
+						id: 'pFRud5u8k',
+						label: i18n.ts.passwordLessLogin,
+						keywords: ['password', 'less', 'key', 'passkey', 'login', 'signin', i18n.ts.passwordLessLoginDescription],
+					},
+				],
+				label: i18n.ts['2fa'],
+				keywords: ['2fa'],
+			},
+		],
+		label: i18n.ts.security,
+		keywords: ['security'],
+		path: '/settings/security',
+		icon: 'ti ti-lock',
+	},
+	{
+		id: 'w4L6myH61',
+		children: [
+			{
+				id: 'ru8DrOn3J',
+				label: i18n.ts._profile.changeBanner,
+				keywords: ['banner', 'change'],
+			},
+			{
+				id: 'CCnD8Apnu',
+				label: i18n.ts._profile.changeAvatar,
+				keywords: ['avatar', 'icon', 'change'],
+			},
+			{
+				id: 'yFEVCJxFX',
+				label: i18n.ts._profile.name,
+				keywords: ['name'],
+			},
+			{
+				id: '2O1S5reaB',
+				label: i18n.ts._profile.description,
+				keywords: ['description', 'bio'],
+			},
+			{
+				id: 'pWi4OLS8g',
+				label: i18n.ts.location,
+				keywords: ['location', 'locale'],
+			},
+			{
+				id: 'oLO5X6Wtw',
+				label: i18n.ts.birthday,
+				keywords: ['birthday', 'birthdate', 'age'],
+			},
+			{
+				id: 'm2trKwPgq',
+				label: i18n.ts.language,
+				keywords: ['language', 'locale'],
+			},
+			{
+				id: 'kfDZxCDp9',
+				label: i18n.ts._profile.metadataEdit,
+				keywords: ['metadata'],
+			},
+			{
+				id: 'uPt3MFymp',
+				label: i18n.ts._profile.followedMessage,
+				keywords: ['follow', 'message', i18n.ts._profile.followedMessageDescription],
+			},
+			{
+				id: 'wuGg0tBjw',
+				label: i18n.ts.reactionAcceptance,
+				keywords: ['reaction'],
+			},
+			{
+				id: 'EezPpmMnf',
+				children: [
+					{
+						id: 'f2cRLh8ad',
+						label: i18n.ts.flagAsCat,
+						keywords: ['cat'],
+					},
+					{
+						id: 'eVoViiF3h',
+						label: i18n.ts.flagAsBot,
+						keywords: ['bot'],
+					},
+				],
+				label: i18n.ts.advancedSettings,
+				keywords: [],
+			},
+		],
+		label: i18n.ts.profile,
+		keywords: ['profile'],
+		path: '/settings/profile',
+		icon: 'ti ti-user',
+	},
+	{
+		id: '2rp9ka5Ht',
+		children: [
+			{
+				id: 'qBUSKPxLW',
+				label: i18n.ts.makeFollowManuallyApprove,
+				keywords: ['follow', 'lock', i18n.ts.lockedAccountInfo],
+			},
+			{
+				id: '3LZBlZCej',
+				label: i18n.ts.autoAcceptFollowed,
+				keywords: ['follow', 'auto', 'accept'],
+			},
+			{
+				id: '9gOp28wKG',
+				label: i18n.ts.makeReactionsPublic,
+				keywords: ['reaction', 'public', i18n.ts.makeReactionsPublicDescription],
+			},
+			{
+				id: 'CjAkqMhct',
+				label: i18n.ts.followingVisibility,
+				keywords: ['following', 'visibility'],
+			},
+			{
+				id: '4nEwI6LYt',
+				label: i18n.ts.followersVisibility,
+				keywords: ['follower', 'visibility'],
+			},
+			{
+				id: 'naMp37wTL',
+				label: i18n.ts.hideOnlineStatus,
+				keywords: ['online', 'status', i18n.ts.hideOnlineStatusDescription],
+			},
+			{
+				id: 'p0dCVR0UP',
+				label: i18n.ts.noCrawle,
+				keywords: ['crawle', 'index', 'search', i18n.ts.noCrawleDescription],
+			},
+			{
+				id: 'aceURmNPq',
+				label: i18n.ts.preventAiLearning,
+				keywords: ['crawle', 'ai', i18n.ts.preventAiLearningDescription],
+			},
+			{
+				id: 'ahABA0j7u',
+				label: i18n.ts.makeExplorable,
+				keywords: ['explore', i18n.ts.makeExplorableDescription],
+			},
+			{
+				id: 'cyeDbLN8N',
+				children: [
+					{
+						id: 'xEYlOghao',
+						label: i18n.ts._accountSettings.requireSigninToViewContents,
+						keywords: ['login', 'signin'],
+					},
+					{
+						id: 'sMmYFCS60',
+						label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore,
+						keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription],
+					},
+					{
+						id: '2prkeWRSd',
+						label: i18n.ts._accountSettings.makeNotesHiddenBefore,
+						keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription],
+					},
+				],
+				label: i18n.ts.lockdown,
+				keywords: ['lockdown'],
+			},
+		],
+		label: i18n.ts.privacy,
+		keywords: ['privacy'],
+		path: '/settings/privacy',
+		icon: 'ti ti-lock-open',
+	},
+	{
+		id: '3yCAv0IsZ',
+		children: [
+			{
+				id: 'x1GWSQnPw',
+				label: i18n.ts.uiLanguage,
+				keywords: ['language'],
+			},
+			{
+				id: 'EOSa4rtt3',
+				label: i18n.ts.overridedDeviceKind,
+				keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
+			},
+			{
+				id: 'm9LhX8BG8',
+				label: i18n.ts.showFixedPostForm,
+				keywords: ['post', 'form', 'timeline'],
+			},
+			{
+				id: 'snyCQ5oKE',
+				label: i18n.ts.showFixedPostFormInChannel,
+				keywords: ['post', 'form', 'timeline', 'channel'],
+			},
+			{
+				id: '8j36S4Ev6',
+				label: i18n.ts.pinnedList,
+				keywords: ['pinned', 'list'],
+			},
+			{
+				id: 'CWpyT9vLK',
+				label: i18n.ts.enableQuickAddMfmFunction,
+				keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
+			},
+			{
+				id: '1yhown1Xc',
+				label: i18n.ts.rememberNoteVisibility,
+				keywords: ['remember', 'keep', 'note', 'visibility'],
+			},
+			{
+				id: 'wUeAI5QBV',
+				label: i18n.ts.defaultNoteVisibility,
+				keywords: ['default', 'note', 'visibility'],
+			},
+			{
+				id: '6kMj4HVOg',
+				children: [
+					{
+						id: 'DQIcvf64G',
+						label: i18n.ts.collapseRenotes,
+						keywords: ['renote', i18n.ts.collapseRenotesDescription],
+					},
+					{
+						id: 'igFN7RIUa',
+						label: i18n.ts.showNoteActionsOnlyHover,
+						keywords: ['hover', 'show', 'footer', 'action'],
+					},
+					{
+						id: '9uxocbLO0',
+						label: i18n.ts.showClipButtonInNoteFooter,
+						keywords: ['footer', 'action', 'clip', 'show'],
+					},
+					{
+						id: 'eaT1O1Fao',
+						label: i18n.ts.enableAdvancedMfm,
+						keywords: ['mfm', 'enable', 'show', 'advanced'],
+					},
+					{
+						id: 'omxZk3eET',
+						label: i18n.ts.showReactionsCount,
+						keywords: ['reaction', 'count', 'show'],
+					},
+					{
+						id: 'epvi2Nv2G',
+						label: i18n.ts.loadRawImages,
+						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
+					},
+				],
+				label: i18n.ts.note,
+				keywords: ['note'],
+			},
+			{
+				id: 'jb3HUeyrx',
+				children: [
+					{
+						id: 'ykifk3NHS',
+						label: i18n.ts.useGroupedNotifications,
+						keywords: ['group'],
+					},
+				],
+				label: i18n.ts.notifications,
+				keywords: ['notification'],
+			},
+			{
+				id: 'abEAdSpYY',
+				children: [
+					{
+						id: 'lBbtAg0Hm',
+						label: i18n.ts.openImageInNewTab,
+						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
+					},
+					{
+						id: 'E9whefUtX',
+						label: i18n.ts.useReactionPickerForContextMenu,
+						keywords: ['reaction', 'picker', 'contextmenu', 'open'],
+					},
+					{
+						id: 'iQaBbJBva',
+						label: i18n.ts.enableInfiniteScroll,
+						keywords: ['load', 'auto', 'more'],
+					},
+					{
+						id: 'hgEVGgJa1',
+						label: i18n.ts.disableStreamingTimeline,
+						keywords: ['disable', 'streaming', 'timeline'],
+					},
+					{
+						id: 'yxehrHZ6x',
+						label: i18n.ts.alwaysConfirmFollow,
+						keywords: ['follow', 'confirm', 'always'],
+					},
+					{
+						id: 'DdoFLaSG8',
+						label: i18n.ts.confirmWhenRevealingSensitiveMedia,
+						keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
+					},
+					{
+						id: 'uIMCIK7kG',
+						label: i18n.ts.confirmOnReact,
+						keywords: ['reaction', 'confirm'],
+					},
+					{
+						id: 'zvM13vl26',
+						label: i18n.ts.keepCw,
+						keywords: ['remember', 'keep', 'note', 'cw'],
+					},
+					{
+						id: 'm75VEWI3S',
+						label: i18n.ts.whenServerDisconnected,
+						keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
+					},
+					{
+						id: 'bLO9vCyKW',
+						label: i18n.ts.numberOfPageCache,
+						keywords: ['cache', 'page'],
+					},
+					{
+						id: 'iQ7Er89l5',
+						label: i18n.ts.dataSaver,
+						keywords: ['datasaver'],
+					},
+				],
+				label: i18n.ts.behavior,
+				keywords: ['behavior'],
+			},
+			{
+				id: 'C2WYcVM1d',
+				children: [
+					{
+						id: 'Cu7ErCM7C',
+						label: i18n.ts.forceShowAds,
+						keywords: ['ad', 'show'],
+					},
+					{
+						id: 'BBxwy4F6E',
+						label: i18n.ts.hemisphere,
+						keywords: [],
+					},
+					{
+						id: '9YdUwDC8d',
+						label: i18n.ts.additionalEmojiDictionary,
+						keywords: ['emoji', 'dictionary', 'additional', 'extra'],
+					},
+				],
+				label: i18n.ts.other,
+				keywords: [],
+			},
+		],
+		label: i18n.ts.preferences,
+		keywords: ['general', 'preferences'],
+		path: '/settings/preferences',
+		icon: 'ti ti-adjustments',
+	},
+	{
+		id: 'mwkwtw83Y',
+		label: i18n.ts.plugins,
+		keywords: ['plugin'],
+		path: '/settings/plugin',
+		icon: 'ti ti-plug',
+	},
+	{
+		id: 'F1uK9ssiY',
+		children: [
+			{
+				id: 'msAcN6u3S',
+				label: i18n.ts.accountInfo,
+				keywords: ['account', 'info'],
+			},
+			{
+				id: 'ts8DgdnZV',
+				label: i18n.ts.accountMigration,
+				keywords: ['account', 'move', 'migration'],
+			},
+			{
+				id: '4BG7nBECm',
+				label: i18n.ts.closeAccount,
+				keywords: ['account', 'close', 'delete', i18n.ts._accountDelete.requestAccountDelete],
+			},
+			{
+				id: '2qI6ruPgi',
+				label: i18n.ts.experimentalFeatures,
+				keywords: ['experimental', 'feature', 'flags'],
+			},
+			{
+				id: 'cIeaax47o',
+				label: i18n.ts.developer,
+				keywords: ['developer', 'mode', 'debug'],
+			},
+		],
+		label: i18n.ts.other,
+		keywords: ['other'],
+		path: '/settings/other',
+		icon: 'ti ti-dots',
+	},
+	{
+		id: '3icEvyv2D',
+		children: [
+			{
+				id: 'Tyt3gZTy',
+				children: [
+					{
+						id: '9b7ZURyAt',
+						label: i18n.ts.showMutedWord,
+						keywords: ['show'],
+					},
+				],
+				label: i18n.ts.wordMute,
+				keywords: ['note', 'word', 'soft', 'mute', 'hide'],
+			},
+			{
+				id: 'kdMk41II0',
+				label: i18n.ts.hardWordMute,
+				keywords: ['note', 'word', 'hard', 'mute', 'hide'],
+			},
+			{
+				id: 'mjORQamAK',
+				label: i18n.ts.instanceMute,
+				keywords: ['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide'],
+			},
+			{
+				id: '1ZT7S9FZd',
+				label: `${i18n.ts.mutedUsers} (${ i18n.ts.renote })`,
+				keywords: ['renote', 'mute', 'hide', 'user'],
+			},
+			{
+				id: 'ANrPit3kQ',
+				label: i18n.ts.mutedUsers,
+				keywords: ['note', 'mute', 'hide', 'user'],
+			},
+			{
+				id: 'bPAE4lfno',
+				label: i18n.ts.blockedUsers,
+				keywords: ['block', 'user'],
+			},
+		],
+		label: i18n.ts.muteAndBlock,
+		keywords: ['mute', 'block'],
+		path: '/settings/mute-block',
+		icon: 'ti ti-ban',
+	},
+	{
+		id: 'qE2vLlMkF',
+		children: [
+			{
+				id: 'hPPEzjvZC',
+				label: i18n.ts._exportOrImport.allNotes,
+				keywords: ['notes'],
+			},
+			{
+				id: 'AFaeHsCUB',
+				label: i18n.ts._exportOrImport.favoritedNotes,
+				keywords: ['favorite', 'notes'],
+			},
+			{
+				id: 'xyCPmQiRo',
+				label: i18n.ts._exportOrImport.clips,
+				keywords: ['clip', 'notes'],
+			},
+			{
+				id: 'Ch7hWAGUy',
+				label: i18n.ts._exportOrImport.followingList,
+				keywords: ['following', 'users'],
+			},
+			{
+				id: 'AwPgFboEx',
+				label: i18n.ts._exportOrImport.userLists,
+				keywords: ['user', 'lists'],
+			},
+			{
+				id: 'nporiHshC',
+				label: i18n.ts._exportOrImport.muteList,
+				keywords: ['mute', 'users'],
+			},
+			{
+				id: 'BsCzR7vNw',
+				label: i18n.ts._exportOrImport.blockingList,
+				keywords: ['block', 'users'],
+			},
+			{
+				id: 'dvf4IgYrQ',
+				label: i18n.ts.antennas,
+				keywords: ['antennas'],
+			},
+		],
+		label: i18n.ts.importAndExport,
+		keywords: ['import', 'export', 'data'],
+		path: '/settings/import-export',
+		icon: 'ti ti-package',
+	},
+	{
+		id: '3Tcxw4Fwl',
+		children: [
+			{
+				id: 'iIai9O65I',
+				label: i18n.ts.emailAddress,
+				keywords: ['email', 'address'],
+			},
+			{
+				id: 'i6cC6oi0m',
+				label: i18n.ts.receiveAnnouncementFromInstance,
+				keywords: ['announcement', 'email'],
+			},
+			{
+				id: 'C1YTinP11',
+				label: i18n.ts.emailNotification,
+				keywords: ['notification', 'email'],
+			},
+		],
+		label: i18n.ts.email,
+		keywords: ['email'],
+		path: '/settings/email',
+		icon: 'ti ti-mail',
+	},
+	{
+		id: 'tnYoppRiv',
+		children: [
+			{
+				id: 'ncIq6TAR2',
+				label: i18n.ts.usageAmount,
+				keywords: ['capacity', 'usage'],
+			},
+			{
+				id: '2c4CQSvSr',
+				label: i18n.ts.statistics,
+				keywords: ['statistics', 'usage'],
+			},
+			{
+				id: 'pepHELHMt',
+				label: i18n.ts.uploadFolder,
+				keywords: ['default', 'upload', 'folder'],
+			},
+			{
+				id: 'xqOWrABxV',
+				label: i18n.ts.keepOriginalUploading,
+				keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription],
+			},
+			{
+				id: 'D8HUTGWE1',
+				label: i18n.ts.keepOriginalFilename,
+				keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription],
+			},
+			{
+				id: '6xAvsWSZi',
+				label: i18n.ts.alwaysMarkSensitive,
+				keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'],
+			},
+			{
+				id: 'csNNPF1KX',
+				label: i18n.ts.enableAutoSensitive,
+				keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription],
+			},
+		],
+		label: i18n.ts.drive,
+		keywords: ['drive'],
+		path: '/settings/drive',
+		icon: 'ti ti-cloud',
+	},
+	{
+		id: 'gtaOSdIJB',
+		label: i18n.ts.avatarDecorations,
+		keywords: ['avatar', 'icon', 'decoration'],
+		path: '/settings/avatar-decoration',
+		icon: 'ti ti-sparkles',
+	},
+	{
+		id: 'AqPvMgn3A',
+		children: [
+			{
+				id: 'j5gTtuMWP',
+				label: i18n.ts.useBlurEffect,
+				keywords: ['blur'],
+			},
+			{
+				id: 'C05WQNSIJ',
+				label: i18n.ts.useBlurEffectForModal,
+				keywords: ['blur', 'modal'],
+			},
+			{
+				id: 'snVKNr7Bw',
+				label: i18n.ts.highlightSensitiveMedia,
+				keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
+			},
+			{
+				id: 'DsS2CwjYE',
+				label: i18n.ts.squareAvatars,
+				keywords: ['avatar', 'icon', 'square'],
+			},
+			{
+				id: 'xCcTDl651',
+				label: i18n.ts.showAvatarDecorations,
+				keywords: ['avatar', 'icon', 'decoration', 'show'],
+			},
+			{
+				id: '3dHw723VD',
+				label: i18n.ts.showGapBetweenNotesInTimeline,
+				keywords: ['note', 'timeline', 'gap'],
+			},
+			{
+				id: 'AWi72xbrl',
+				label: i18n.ts.seasonalScreenEffect,
+				keywords: ['effect', 'show'],
+			},
+			{
+				id: 'Ces8FsJws',
+				label: i18n.ts.menuStyle,
+				keywords: ['menu', 'style', 'popup', 'drawer'],
+			},
+			{
+				id: 'wDr9xSXCv',
+				label: i18n.ts.emojiStyle,
+				keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
+			},
+			{
+				id: 'vFB0pLzck',
+				label: i18n.ts.fontSize,
+				keywords: ['font', 'size'],
+			},
+			{
+				id: '23BhvYXPC',
+				label: i18n.ts.useSystemFont,
+				keywords: ['font', 'system', 'native'],
+			},
+			{
+				id: 'EeNLndAOa',
+				children: [
+					{
+						id: 'rAAPoaodS',
+						label: i18n.ts.reactionsDisplaySize,
+						keywords: ['reaction', 'size', 'scale', 'display'],
+					},
+					{
+						id: 'qTLAvNWsc',
+						label: i18n.ts.limitWidthOfReaction,
+						keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
+					},
+					{
+						id: '2lWgzAm13',
+						label: i18n.ts.mediaListWithOneImageAppearance,
+						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
+					},
+					{
+						id: 'EU7HbxOR5',
+						label: i18n.ts.instanceTicker,
+						keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
+					},
+					{
+						id: 'AEtM0FAp1',
+						label: i18n.ts.displayOfSensitiveMedia,
+						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
+					},
+				],
+				label: i18n.ts.displayOfNote,
+				keywords: ['note', 'display'],
+			},
+			{
+				id: 'A1FMC2Zon',
+				children: [
+					{
+						id: 'CB37G5ZDo',
+						label: i18n.ts.position,
+						keywords: ['position'],
+					},
+					{
+						id: 'gGS2i19hS',
+						label: i18n.ts.stackAxis,
+						keywords: ['stack', 'axis', 'direction'],
+					},
+				],
+				label: i18n.ts.notificationDisplay,
+				keywords: ['notification', 'display'],
+			},
+		],
+		label: i18n.ts.appearance,
+		keywords: ['appearance'],
+		path: '/settings/appearance',
+		icon: 'ti ti-device-desktop',
+	},
+	{
+		id: 'f08Mi1Uwn',
+		children: [
+			{
+				id: '7ov7ceoij',
+				label: i18n.ts.reduceUiAnimation,
+				keywords: ['animation', 'motion', 'reduce'],
+			},
+			{
+				id: 'cXr3tFdpa',
+				label: i18n.ts.disableShowingAnimatedImages,
+				keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'],
+			},
+			{
+				id: 'Ok1UBwtP',
+				label: i18n.ts.enableAnimatedMfm,
+				keywords: ['mfm', 'enable', 'show', 'animated'],
+			},
+			{
+				id: 'yPEpJigqY',
+				label: i18n.ts.enableHorizontalSwipe,
+				keywords: ['swipe', 'horizontal', 'tab'],
+			},
+			{
+				id: 'h7iZtdTU3',
+				label: i18n.ts.keepScreenOn,
+				keywords: ['keep', 'screen', 'display', 'on'],
+			},
+			{
+				id: 'gP1BY3PDy',
+				label: i18n.ts.useNativeUIForVideoAudioPlayer,
+				keywords: ['native', 'system', 'video', 'audio', 'player', 'media'],
+			},
+			{
+				id: 'jnMK3M6rs',
+				label: i18n.ts._contextMenu.title,
+				keywords: ['contextmenu', 'system', 'native'],
+			},
+		],
+		label: i18n.ts.accessibility,
+		keywords: ['accessibility'],
+		path: '/settings/accessibility',
+		icon: 'ti ti-accessible',
+	},
+] as const;
+
+export type SearchIndex = typeof searchIndexes;
diff --git a/packages/frontend/src/utility/cache.ts b/packages/frontend/src/utility/cache.ts
new file mode 100644
index 0000000000..0fbdf34d5d
--- /dev/null
+++ b/packages/frontend/src/utility/cache.ts
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref } from 'vue';
+
+export class Cache<T> {
+	private cachedAt: number | null = null;
+	public value = ref<T | undefined>();
+	private lifetime: number;
+	private fetcher: () => Promise<T>;
+
+	constructor(lifetime: Cache<never>['lifetime'], fetcher: () => Promise<T>) {
+		this.lifetime = lifetime;
+		this.fetcher = fetcher;
+	}
+
+	public set(value: T): void {
+		this.cachedAt = Date.now();
+		this.value.value = value;
+	}
+
+	private get(): T | undefined {
+		if (this.cachedAt == null) return undefined;
+		if ((Date.now() - this.cachedAt) > this.lifetime) {
+			this.value.value = undefined;
+			this.cachedAt = null;
+			return undefined;
+		}
+		return this.value.value;
+	}
+
+	public delete() {
+		this.cachedAt = null;
+	}
+
+	/**
+	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
+	 */
+	public async fetch(): Promise<T> {
+		const cachedValue = this.get();
+		if (cachedValue !== undefined) {
+			// Cache HIT
+			return cachedValue;
+		}
+
+		// Cache MISS
+		const value = await this.fetcher();
+		this.set(value);
+		return value;
+	}
+}
diff --git a/packages/frontend/src/utility/chart-legend.ts b/packages/frontend/src/utility/chart-legend.ts
new file mode 100644
index 0000000000..e701d18dd2
--- /dev/null
+++ b/packages/frontend/src/utility/chart-legend.ts
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Plugin } from 'chart.js';
+import MkChartLegend from '@/components/MkChartLegend.vue';
+
+export const chartLegend = (legend: InstanceType<typeof MkChartLegend>) => ({
+	id: 'htmlLegend',
+	afterUpdate(chart, args, options) {
+		// Reuse the built-in legendItems generator
+		const items = chart.options.plugins.legend.labels.generateLabels(chart);
+
+		legend.update(chart, items);
+	},
+}) as Plugin;
diff --git a/packages/frontend/src/utility/chart-vline.ts b/packages/frontend/src/utility/chart-vline.ts
new file mode 100644
index 0000000000..465ca591c6
--- /dev/null
+++ b/packages/frontend/src/utility/chart-vline.ts
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Plugin } from 'chart.js';
+
+export const chartVLine = (vLineColor: string) => ({
+	id: 'vLine',
+	beforeDraw(chart, args, options) {
+		if (chart.tooltip?._active?.length) {
+			const ctx = chart.ctx;
+			const xs = chart.tooltip._active.map(a => a.element.x);
+			const x = xs.reduce((a, b) => a + b, 0) / xs.length;
+			const topY = chart.scales.y.top;
+			const bottomY = chart.scales.y.bottom;
+
+			ctx.save();
+			ctx.beginPath();
+			ctx.moveTo(x, bottomY);
+			ctx.lineTo(x, topY);
+			ctx.lineWidth = 1;
+			ctx.strokeStyle = vLineColor;
+			ctx.stroke();
+			ctx.restore();
+		}
+	},
+}) as Plugin;
diff --git a/packages/frontend/src/utility/check-permissions.ts b/packages/frontend/src/utility/check-permissions.ts
new file mode 100644
index 0000000000..ed86529d5b
--- /dev/null
+++ b/packages/frontend/src/utility/check-permissions.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { instance } from '@/instance.js';
+import { $i } from '@/account.js';
+
+export const notesSearchAvailable = (
+	// FIXME: instance.policies would be null in Vitest
+	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+	($i == null && instance.policies != null && instance.policies.canSearchNotes) ||
+	($i != null && $i.policies.canSearchNotes) ||
+	false
+) as boolean;
+
+export const canSearchNonLocalNotes = (
+	instance.noteSearchableScope === 'global'
+);
diff --git a/packages/frontend/src/utility/check-reaction-permissions.ts b/packages/frontend/src/utility/check-reaction-permissions.ts
new file mode 100644
index 0000000000..281ea2520e
--- /dev/null
+++ b/packages/frontend/src/utility/check-reaction-permissions.ts
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import type { UnicodeEmojiDef } from '@@/js/emojilist.js';
+
+export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean {
+	if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする;
+	if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能
+
+	const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
+	return !(emoji.localOnly && note.user.host !== me.host)
+      && !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
+      && (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
+}
diff --git a/packages/frontend/src/utility/check-word-mute.ts b/packages/frontend/src/utility/check-word-mute.ts
new file mode 100644
index 0000000000..98fea1bced
--- /dev/null
+++ b/packages/frontend/src/utility/check-word-mute.ts
@@ -0,0 +1,43 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import * as Misskey from 'misskey-js';
+
+export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
+	// 自分自身
+	if (me && (note.userId === me.id)) return false;
+
+	if (mutedWords.length > 0) {
+		const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
+
+		if (text === '') return false;
+
+		const matched = mutedWords.filter(filter => {
+			if (Array.isArray(filter)) {
+				// Clean up
+				const filteredFilter = filter.filter(keyword => keyword !== '');
+				if (filteredFilter.length === 0) return false;
+
+				return filteredFilter.every(keyword => text.includes(keyword));
+			} else {
+				// represents RegExp
+				const regexp = filter.match(/^\/(.+)\/(.*)$/);
+
+				// This should never happen due to input sanitisation.
+				if (!regexp) return false;
+
+				try {
+					return new RegExp(regexp[1], regexp[2]).test(text);
+				} catch (err) {
+					// This should never happen due to input sanitisation.
+					return false;
+				}
+			}
+		});
+
+		if (matched.length > 0) return matched;
+	}
+
+	return false;
+}
diff --git a/packages/frontend/src/utility/clear-cache.ts b/packages/frontend/src/utility/clear-cache.ts
new file mode 100644
index 0000000000..b6ae254727
--- /dev/null
+++ b/packages/frontend/src/utility/clear-cache.ts
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { unisonReload } from '@/utility/unison-reload.js';
+import * as os from '@/os.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { fetchCustomEmojis } from '@/custom-emojis.js';
+import { fetchInstance } from '@/instance.js';
+
+export async function clearCache() {
+	os.waiting();
+	miLocalStorage.removeItem('instance');
+	miLocalStorage.removeItem('instanceCachedAt');
+	miLocalStorage.removeItem('locale');
+	miLocalStorage.removeItem('localeVersion');
+	miLocalStorage.removeItem('theme');
+	miLocalStorage.removeItem('emojis');
+	miLocalStorage.removeItem('lastEmojisFetchedAt');
+	await fetchInstance(true);
+	await fetchCustomEmojis(true);
+	unisonReload();
+}
diff --git a/packages/frontend/src/utility/clicker-game.ts b/packages/frontend/src/utility/clicker-game.ts
new file mode 100644
index 0000000000..0544be7757
--- /dev/null
+++ b/packages/frontend/src/utility/clicker-game.ts
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref, computed } from 'vue';
+import { misskeyApi } from '@/utility/misskey-api.js';
+
+type SaveData = {
+	gameVersion: number;
+	cookies: number;
+	totalCookies: number;
+	totalHandmadeCookies: number;
+	clicked: number;
+	achievements: any[];
+	facilities: any[];
+};
+
+export const saveData = ref<SaveData>();
+export const ready = computed(() => saveData.value != null);
+
+let prev = '';
+
+export async function load() {
+	try {
+		saveData.value = await misskeyApi('i/registry/get', {
+			scope: ['clickerGame'],
+			key: 'saveData',
+		});
+	} catch (err) {
+		if (err.code === 'NO_SUCH_KEY') {
+			saveData.value = {
+				gameVersion: 2,
+				cookies: 0,
+				totalCookies: 0,
+				totalHandmadeCookies: 0,
+				clicked: 0,
+				achievements: [],
+				facilities: [],
+			};
+			save();
+			return;
+		}
+		throw err;
+	}
+
+	// migration
+	if (saveData.value.gameVersion === 1) {
+		saveData.value = {
+			gameVersion: 2,
+			cookies: saveData.value.cookies,
+			totalCookies: saveData.value.cookies,
+			totalHandmadeCookies: saveData.value.cookies,
+			clicked: saveData.value.clicked,
+			achievements: [],
+			facilities: [],
+		};
+		save();
+	}
+}
+
+export async function save() {
+	const current = JSON.stringify(saveData.value);
+	if (current === prev) return;
+
+	await misskeyApi('i/registry/set', {
+		scope: ['clickerGame'],
+		key: 'saveData',
+		value: saveData.value,
+	});
+
+	prev = current;
+}
diff --git a/packages/frontend/src/utility/clone.ts b/packages/frontend/src/utility/clone.ts
new file mode 100644
index 0000000000..ea8eea14b5
--- /dev/null
+++ b/packages/frontend/src/utility/clone.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// structredCloneが遅いため
+// SEE: http://var.blog.jp/archives/86038606.html
+// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
+// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
+
+export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
+
+export function deepClone<T extends Cloneable>(x: T): T {
+	if (typeof x === 'object') {
+		if (x === null) return x;
+		if (Array.isArray(x)) return x.map(deepClone) as T;
+		const obj = {} as Record<string | number | symbol, Cloneable>;
+		for (const [k, v] of Object.entries(x)) {
+			obj[k] = v === undefined ? undefined : deepClone(v);
+		}
+		return obj as T;
+	} else {
+		return x;
+	}
+}
diff --git a/packages/frontend/src/utility/code-highlighter.ts b/packages/frontend/src/utility/code-highlighter.ts
new file mode 100644
index 0000000000..4f2aff9d4c
--- /dev/null
+++ b/packages/frontend/src/utility/code-highlighter.ts
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { createHighlighterCore } from 'shiki/core';
+import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
+import darkPlus from 'shiki/themes/dark-plus.mjs';
+import { bundledThemesInfo } from 'shiki/themes';
+import { bundledLanguagesInfo } from 'shiki/langs';
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
+import defaultLightTheme from '@@/themes/l-light.json5';
+import defaultDarkTheme from '@@/themes/d-green-lime.json5';
+import { unique } from './array.js';
+import { deepClone } from './clone.js';
+import { deepMerge } from './merge.js';
+import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core';
+import { prefer } from '@/preferences.js';
+
+let _highlighter: HighlighterCore | null = null;
+
+export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
+export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
+export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
+	const theme = deepClone(mode === 'light' ? prefer.s.lightTheme ?? defaultLightTheme : prefer.s.darkTheme ?? defaultDarkTheme);
+
+	if (theme.base) {
+		const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
+		if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
+	}
+
+	if (theme.codeHighlighter) {
+		let _res: ThemeRegistration = {};
+		if (theme.codeHighlighter.base === '_none_') {
+			_res = deepClone(theme.codeHighlighter.overrides);
+		} else {
+			const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
+			_res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
+		}
+		if (_res.name == null) {
+			_res.name = theme.id;
+		}
+		_res.type = mode;
+
+		if (getName) {
+			return _res.name;
+		}
+		return _res;
+	}
+
+	if (getName) {
+		return 'dark-plus';
+	}
+	return darkPlus;
+}
+
+export async function getHighlighter(): Promise<HighlighterCore> {
+	if (!_highlighter) {
+		return await initHighlighter();
+	}
+	return _highlighter;
+}
+
+async function initHighlighter() {
+	// テーマの重複を消す
+	const themes = unique([
+		darkPlus,
+		...(await Promise.all([getTheme('light'), getTheme('dark')])),
+	]);
+
+	const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript');
+	const highlighter = await createHighlighterCore({
+		engine: createOnigurumaEngine(() => import('shiki/onig.wasm?init')),
+		themes,
+		langs: [
+			...(jsLangInfo ? [async () => await jsLangInfo.import()] : []),
+			async () => (await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json')).default as unknown as LanguageRegistration,
+		],
+	});
+
+	// TODO
+	//watch('lightTheme', async () => {
+	//	const newTheme = await getTheme('light');
+	//	if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+	//		highlighter.loadTheme(newTheme);
+	//	}
+	//});
+	//watch('darkTheme', async () => {
+	//	const newTheme = await getTheme('dark');
+	//	if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+	//		highlighter.loadTheme(newTheme);
+	//	}
+	//});
+
+	_highlighter = highlighter;
+
+	return highlighter;
+}
diff --git a/packages/frontend/src/utility/collect-page-vars.ts b/packages/frontend/src/utility/collect-page-vars.ts
new file mode 100644
index 0000000000..5096c0669e
--- /dev/null
+++ b/packages/frontend/src/utility/collect-page-vars.ts
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+interface StringPageVar {
+	name: string,
+	type: 'string',
+	value: string
+}
+
+interface NumberPageVar {
+	name: string,
+	type: 'number',
+	value: number
+}
+
+interface BooleanPageVar {
+	name: string,
+	type: 'boolean',
+	value: boolean
+}
+
+type PageVar = StringPageVar | NumberPageVar | BooleanPageVar;
+
+export function collectPageVars(content): PageVar[] {
+	const pageVars: PageVar[] = [];
+	const collect = (xs: any[]): void => {
+		for (const x of xs) {
+			if (x.type === 'textInput') {
+				pageVars.push({
+					name: x.name,
+					type: 'string',
+					value: x.default || '',
+				});
+			} else if (x.type === 'textareaInput') {
+				pageVars.push({
+					name: x.name,
+					type: 'string',
+					value: x.default || '',
+				});
+			} else if (x.type === 'numberInput') {
+				pageVars.push({
+					name: x.name,
+					type: 'number',
+					value: x.default || 0,
+				});
+			} else if (x.type === 'switch') {
+				pageVars.push({
+					name: x.name,
+					type: 'boolean',
+					value: x.default || false,
+				});
+			} else if (x.type === 'counter') {
+				pageVars.push({
+					name: x.name,
+					type: 'number',
+					value: 0,
+				});
+			} else if (x.type === 'radioButton') {
+				pageVars.push({
+					name: x.name,
+					type: 'string',
+					value: x.default || '',
+				});
+			} else if (x.children) {
+				collect(x.children);
+			}
+		}
+	};
+	collect(content);
+	return pageVars;
+}
diff --git a/packages/frontend/src/utility/color.ts b/packages/frontend/src/utility/color.ts
new file mode 100644
index 0000000000..a11255ffd1
--- /dev/null
+++ b/packages/frontend/src/utility/color.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const alpha = (hex: string, a: number): string => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
diff --git a/packages/frontend/src/utility/confetti.ts b/packages/frontend/src/utility/confetti.ts
new file mode 100644
index 0000000000..8e53a6ceeb
--- /dev/null
+++ b/packages/frontend/src/utility/confetti.ts
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import _confetti from 'canvas-confetti';
+import * as os from '@/os.js';
+
+export function confetti(options: { duration?: number; } = {}) {
+	const duration = options.duration ?? 1000 * 4;
+	const animationEnd = Date.now() + duration;
+	const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: os.claimZIndex('high') };
+
+	function randomInRange(min, max) {
+		return Math.random() * (max - min) + min;
+	}
+
+	const interval = setInterval(() => {
+		const timeLeft = animationEnd - Date.now();
+
+		if (timeLeft <= 0) {
+			return clearInterval(interval);
+		}
+
+		const particleCount = 50 * (timeLeft / duration);
+		// since particles fall down, start a bit higher than random
+		_confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } }));
+		_confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } }));
+	}, 250);
+}
diff --git a/packages/frontend/src/utility/contains.ts b/packages/frontend/src/utility/contains.ts
new file mode 100644
index 0000000000..6137c06e85
--- /dev/null
+++ b/packages/frontend/src/utility/contains.ts
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export default (parent, child, checkSame = true) => {
+	if (checkSame && parent === child) return true;
+	let node = child.parentNode;
+	while (node) {
+		if (node === parent) return true;
+		node = node.parentNode;
+	}
+	return false;
+};
diff --git a/packages/frontend/src/utility/copy-to-clipboard.ts b/packages/frontend/src/utility/copy-to-clipboard.ts
new file mode 100644
index 0000000000..7e0bb25606
--- /dev/null
+++ b/packages/frontend/src/utility/copy-to-clipboard.ts
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * Clipboardに値をコピー(TODO: 文字列以外も対応)
+ */
+export function copyToClipboard(input: string | null) {
+	if (input) navigator.clipboard.writeText(input);
+};
diff --git a/packages/frontend/src/utility/device-kind.ts b/packages/frontend/src/utility/device-kind.ts
new file mode 100644
index 0000000000..7aadb617ca
--- /dev/null
+++ b/packages/frontend/src/utility/device-kind.ts
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
+
+const ua = navigator.userAgent.toLowerCase();
+const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
+const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
+
+export const DEFAULT_DEVICE_KIND: DeviceKind = (
+	isSmartphone
+		? 'smartphone'
+		: isTablet
+			? 'tablet'
+			: 'desktop'
+);
+
+export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
+
+export function updateDeviceKind(kind: DeviceKind | null) {
+	deviceKind = kind ?? DEFAULT_DEVICE_KIND;
+}
diff --git a/packages/frontend/src/utility/emoji-picker.ts b/packages/frontend/src/utility/emoji-picker.ts
new file mode 100644
index 0000000000..7ff5863243
--- /dev/null
+++ b/packages/frontend/src/utility/emoji-picker.ts
@@ -0,0 +1,61 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent, ref } from 'vue';
+import type { Ref } from 'vue';
+import { popup } from '@/os.js';
+import { store } from '@/store.js';
+
+/**
+ * 絵文字ピッカーを表示する。
+ * 類似の機能として{@link ReactionPicker}が存在しているが、この機能とは動きが異なる。
+ * 投稿フォームなどで絵文字を選択する時など、絵文字ピックアップ後でもダイアログが消えずに残り、
+ * 一度表示したダイアログを連続で使用できることが望ましいシーンでの利用が想定される。
+ */
+class EmojiPicker {
+	private src: Ref<HTMLElement | null> = ref(null);
+	private manualShowing = ref(false);
+	private onChosen?: (emoji: string) => void;
+	private onClosed?: () => void;
+
+	constructor() {
+		// nop
+	}
+
+	public async init() {
+		const emojisRef = store.reactiveState.pinnedEmojis;
+		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
+			src: this.src,
+			pinnedEmojis: emojisRef,
+			asReactionPicker: false,
+			manualShowing: this.manualShowing,
+			choseAndClose: false,
+		}, {
+			done: emoji => {
+				if (this.onChosen) this.onChosen(emoji);
+			},
+			close: () => {
+				this.manualShowing.value = false;
+			},
+			closed: () => {
+				this.src.value = null;
+				if (this.onClosed) this.onClosed();
+			},
+		});
+	}
+
+	public show(
+		src: HTMLElement,
+		onChosen?: EmojiPicker['onChosen'],
+		onClosed?: EmojiPicker['onClosed'],
+	) {
+		this.src.value = src;
+		this.manualShowing.value = true;
+		this.onChosen = onChosen;
+		this.onClosed = onClosed;
+	}
+}
+
+export const emojiPicker = new EmojiPicker();
diff --git a/packages/frontend/src/utility/extract-mentions.ts b/packages/frontend/src/utility/extract-mentions.ts
new file mode 100644
index 0000000000..d518562053
--- /dev/null
+++ b/packages/frontend/src/utility/extract-mentions.ts
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// test is located in test/extract-mentions
+
+import * as mfm from 'mfm-js';
+
+export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] {
+	// TODO: 重複を削除
+	const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention');
+	const mentions = mentionNodes.map(x => x.props);
+
+	return mentions;
+}
diff --git a/packages/frontend/src/utility/extract-url-from-mfm.ts b/packages/frontend/src/utility/extract-url-from-mfm.ts
new file mode 100644
index 0000000000..570823d5b5
--- /dev/null
+++ b/packages/frontend/src/utility/extract-url-from-mfm.ts
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as mfm from 'mfm-js';
+import { unique } from '@/utility/array.js';
+
+// unique without hash
+// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
+const removeHash = (x: string) => x.replace(/#[^#]*$/, '');
+
+export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] {
+	const urlNodes = mfm.extract(nodes, (node) => {
+		return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent));
+	});
+	const urls: string[] = unique(urlNodes.map(x => x.props.url));
+
+	return urls.reduce((array, url) => {
+		const urlWithoutHash = removeHash(url);
+		if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url);
+		return array;
+	}, [] as string[]);
+}
diff --git a/packages/frontend/src/utility/file-drop.ts b/packages/frontend/src/utility/file-drop.ts
new file mode 100644
index 0000000000..4259fe25e9
--- /dev/null
+++ b/packages/frontend/src/utility/file-drop.ts
@@ -0,0 +1,121 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type DroppedItem = DroppedFile | DroppedDirectory;
+
+export type DroppedFile = {
+	isFile: true;
+	path: string;
+	file: File;
+};
+
+export type DroppedDirectory = {
+	isFile: false;
+	path: string;
+	children: DroppedItem[];
+};
+
+export async function extractDroppedItems(ev: DragEvent): Promise<DroppedItem[]> {
+	const dropItems = ev.dataTransfer?.items;
+	if (!dropItems || dropItems.length === 0) {
+		return [];
+	}
+
+	const apiTestItem = dropItems[0];
+	if ('webkitGetAsEntry' in apiTestItem) {
+		return readDataTransferItems(dropItems);
+	} else {
+		// webkitGetAsEntryに対応していない場合はfilesから取得する（ディレクトリのサポートは出来ない）
+		const dropFiles = ev.dataTransfer.files;
+		if (dropFiles.length === 0) {
+			return [];
+		}
+
+		const droppedFiles = Array.of<DroppedFile>();
+		for (let i = 0; i < dropFiles.length; i++) {
+			const file = dropFiles.item(i);
+			if (file) {
+				droppedFiles.push({
+					isFile: true,
+					path: file.name,
+					file,
+				});
+			}
+		}
+
+		return droppedFiles;
+	}
+}
+
+/**
+ * ドラッグ＆ドロップされたファイルのリストからディレクトリ構造とファイルへの参照（{@link File}）を取得する。
+ */
+export async function readDataTransferItems(itemList: DataTransferItemList): Promise<DroppedItem[]> {
+	async function readEntry(entry: FileSystemEntry): Promise<DroppedItem> {
+		if (entry.isFile) {
+			return {
+				isFile: true,
+				path: entry.fullPath,
+				file: await readFile(entry as FileSystemFileEntry),
+			};
+		} else {
+			return {
+				isFile: false,
+				path: entry.fullPath,
+				children: await readDirectory(entry as FileSystemDirectoryEntry),
+			};
+		}
+	}
+
+	function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise<File> {
+		return new Promise((resolve, reject) => {
+			fileSystemFileEntry.file(resolve, reject);
+		});
+	}
+
+	function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
+		return new Promise(async (resolve) => {
+			const allEntries = Array.of<FileSystemEntry>();
+			const reader = fileSystemDirectoryEntry.createReader();
+			while (true) {
+				const entries = await new Promise<FileSystemEntry[]>((res, rej) => reader.readEntries(res, rej));
+				if (entries.length === 0) {
+					break;
+				}
+				allEntries.push(...entries);
+			}
+
+			resolve(await Promise.all(allEntries.map(readEntry)));
+		});
+	}
+
+	// 扱いにくいので配列に変換
+	const items = Array.of<DataTransferItem>();
+	for (let i = 0; i < itemList.length; i++) {
+		items.push(itemList[i]);
+	}
+
+	return Promise.all(
+		items
+			.map(it => it.webkitGetAsEntry())
+			.filter(it => it)
+			.map(it => readEntry(it!)),
+	);
+}
+
+/**
+ * {@link DroppedItem}のリストからディレクトリを再帰的に検索し、ファイルのリストを取得する。
+ */
+export function flattenDroppedFiles(items: DroppedItem[]): DroppedFile[] {
+	const result = Array.of<DroppedFile>();
+	for (const item of items) {
+		if (item.isFile) {
+			result.push(item);
+		} else {
+			result.push(...flattenDroppedFiles(item.children));
+		}
+	}
+	return result;
+}
diff --git a/packages/frontend/src/utility/focus-trap.ts b/packages/frontend/src/utility/focus-trap.ts
new file mode 100644
index 0000000000..fd17fa38a0
--- /dev/null
+++ b/packages/frontend/src/utility/focus-trap.ts
@@ -0,0 +1,134 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
+
+const focusTrapElements = new Set<HTMLElement>();
+const ignoreElements = [
+	'script',
+	'style',
+];
+
+function containsFocusTrappedElements(el: HTMLElement): boolean {
+	return Array.from(focusTrapElements).some((focusTrapElement) => {
+		return el.contains(focusTrapElement);
+	});
+}
+
+function getZIndex(el: HTMLElement): number {
+	const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10);
+	if (isNaN(zIndex)) {
+		return 0;
+	}
+	return zIndex;
+}
+
+function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null {
+	let highestZIndexElement: HTMLElement | null = null;
+	let highestZIndex = -Infinity;
+
+	focusTrapElements.forEach((el) => {
+		const zIndex = getZIndex(el);
+		if (zIndex > highestZIndex) {
+			highestZIndex = zIndex;
+			highestZIndexElement = el;
+		}
+	});
+
+	return highestZIndexElement == null ? null : {
+		el: highestZIndexElement,
+		zIndex: highestZIndex,
+	};
+}
+
+function releaseFocusTrap(el: HTMLElement): void {
+	focusTrapElements.delete(el);
+	if (el.inert === true) {
+		el.inert = false;
+	}
+
+	const highestZIndexElement = getHighestZIndexElement();
+
+	if (el.parentElement != null && el !== document.body) {
+		el.parentElement.childNodes.forEach((siblingNode) => {
+			const siblingEl = getHTMLElementOrNull(siblingNode);
+			if (!siblingEl) return;
+			if (
+				siblingEl !== el &&
+				(
+					highestZIndexElement == null ||
+					siblingEl === highestZIndexElement.el ||
+					siblingEl.contains(highestZIndexElement.el)
+				)
+			) {
+				siblingEl.inert = false;
+			} else if (
+				highestZIndexElement != null &&
+				siblingEl !== highestZIndexElement.el &&
+				!siblingEl.contains(highestZIndexElement.el) &&
+				!ignoreElements.includes(siblingEl.tagName.toLowerCase())
+			) {
+				siblingEl.inert = true;
+			} else {
+				siblingEl.inert = false;
+			}
+		});
+		releaseFocusTrap(el.parentElement);
+	}
+}
+
+export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void;
+export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; };
+export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void {
+	const highestZIndexElement = getHighestZIndexElement();
+
+	const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex;
+	const zIndex = getZIndex(el);
+
+	// If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead
+	// Focus trapping for this element will be done in the release function
+	if (!parent && zIndex < highestZIndex) {
+		focusTrapElements.add(el);
+		if (highestZIndexElement) {
+			focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls);
+		}
+		return {
+			release: () => {
+				releaseFocusTrap(el);
+			},
+		};
+	}
+
+	if (el.inert === true) {
+		el.inert = false;
+	}
+
+	if (el.parentElement != null && el !== document.body) {
+		el.parentElement.childNodes.forEach((siblingNode) => {
+			const siblingEl = getHTMLElementOrNull(siblingNode);
+			if (!siblingEl) return;
+			if (
+				siblingEl !== el &&
+				(
+					hasInteractionWithOtherFocusTrappedEls === false ||
+					(!focusTrapElements.has(siblingEl) && !containsFocusTrappedElements(siblingEl))
+				) &&
+				!ignoreElements.includes(siblingEl.tagName.toLowerCase())
+			) {
+				siblingEl.inert = true;
+			}
+		});
+		focusTrap(el.parentElement, hasInteractionWithOtherFocusTrappedEls, true);
+	}
+
+	if (!parent) {
+		focusTrapElements.add(el);
+
+		return {
+			release: () => {
+				releaseFocusTrap(el);
+			},
+		};
+	}
+}
diff --git a/packages/frontend/src/utility/focus.ts b/packages/frontend/src/utility/focus.ts
new file mode 100644
index 0000000000..e3fd928d1d
--- /dev/null
+++ b/packages/frontend/src/utility/focus.ts
@@ -0,0 +1,80 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js';
+import { getElementOrNull, getNodeOrNull } from '@/utility/get-dom-node-or-null.js';
+
+type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement;
+
+export const isFocusable = (input: MaybeHTMLElement | null | undefined): input is HTMLElement => {
+	if (input == null || !(input instanceof HTMLElement)) return false;
+
+	if (input.tabIndex < 0) return false;
+	if ('disabled' in input && input.disabled === true) return false;
+	if ('readonly' in input && input.readonly === true) return false;
+
+	if (!input.ownerDocument.contains(input)) return false;
+
+	const style = window.getComputedStyle(input);
+	if (style.display === 'none') return false;
+	if (style.visibility === 'hidden') return false;
+	if (style.opacity === '0') return false;
+	if (style.pointerEvents === 'none') return false;
+
+	return true;
+};
+
+export const focusPrev = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+	const element = self ? input : getElementOrNull(input)?.previousElementSibling;
+	if (element == null) return;
+	if (isFocusable(element)) {
+		focusOrScroll(element, scroll);
+	} else {
+		focusPrev(element, false, scroll);
+	}
+};
+
+export const focusNext = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+	const element = self ? input : getElementOrNull(input)?.nextElementSibling;
+	if (element == null) return;
+	if (isFocusable(element)) {
+		focusOrScroll(element, scroll);
+	} else {
+		focusNext(element, false, scroll);
+	}
+};
+
+export const focusParent = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
+	const element = self ? input : getNodeOrNull(input)?.parentElement;
+	if (element == null) return;
+	if (isFocusable(element)) {
+		focusOrScroll(element, scroll);
+	} else {
+		focusParent(element, false, scroll);
+	}
+};
+
+const focusOrScroll = (element: HTMLElement, scroll: boolean) => {
+	if (scroll) {
+		const scrollContainer = getScrollContainer(element) ?? document.documentElement;
+		const scrollContainerTop = getScrollPosition(scrollContainer);
+		const stickyTop = getStickyTop(element, scrollContainer);
+		const stickyBottom = getStickyBottom(element, scrollContainer);
+		const top = element.getBoundingClientRect().top;
+		const bottom = element.getBoundingClientRect().bottom;
+
+		let scrollTo = scrollContainerTop;
+		if (top < stickyTop) {
+			scrollTo += top - stickyTop;
+		} else if (bottom > window.innerHeight - stickyBottom) {
+			scrollTo += bottom - window.innerHeight + stickyBottom;
+		}
+		scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' });
+	}
+
+	if (document.activeElement !== element) {
+		element.focus({ preventScroll: true });
+	}
+};
diff --git a/packages/frontend/src/utility/form.ts b/packages/frontend/src/utility/form.ts
new file mode 100644
index 0000000000..1032e97ac9
--- /dev/null
+++ b/packages/frontend/src/utility/form.ts
@@ -0,0 +1,104 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+
+type EnumItem = string | {
+	label: string;
+	value: string;
+};
+
+type Hidden = boolean | ((v: any) => boolean);
+
+export type FormItem = {
+	label?: string;
+	type: 'string';
+	default?: string | null;
+	description?: string;
+	required?: boolean;
+	hidden?: Hidden;
+	multiline?: boolean;
+	treatAsMfm?: boolean;
+} | {
+	label?: string;
+	type: 'number';
+	default?: number | null;
+	description?: string;
+	required?: boolean;
+	hidden?: Hidden;
+	step?: number;
+} | {
+	label?: string;
+	type: 'boolean';
+	default?: boolean | null;
+	description?: string;
+	hidden?: Hidden;
+} | {
+	label?: string;
+	type: 'enum';
+	default?: string | null;
+	required?: boolean;
+	hidden?: Hidden;
+	enum: EnumItem[];
+} | {
+	label?: string;
+	type: 'radio';
+	default?: unknown | null;
+	required?: boolean;
+	hidden?: Hidden;
+	options: {
+		label: string;
+		value: unknown;
+	}[];
+} | {
+	label?: string;
+	type: 'range';
+	default?: number | null;
+	description?: string;
+	required?: boolean;
+	step?: number;
+	min: number;
+	max: number;
+	textConverter?: (value: number) => string;
+	hidden?: Hidden;
+} | {
+	label?: string;
+	type: 'object';
+	default?: Record<string, unknown> | null;
+	hidden: Hidden;
+} | {
+	label?: string;
+	type: 'array';
+	default?: unknown[] | null;
+	hidden: Hidden;
+} | {
+	type: 'button';
+	content?: string;
+	hidden?: Hidden;
+	action: (ev: MouseEvent, v: any) => void;
+} | {
+	type: 'drive-file';
+	defaultFileId?: string | null;
+	hidden?: Hidden;
+	validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>;
+};
+
+export type Form = Record<string, FormItem>;
+
+type GetItemType<Item extends FormItem> =
+	Item['type'] extends 'string' ? string :
+	Item['type'] extends 'number' ? number :
+	Item['type'] extends 'boolean' ? boolean :
+	Item['type'] extends 'radio' ? unknown :
+	Item['type'] extends 'range' ? number :
+	Item['type'] extends 'enum' ? string :
+	Item['type'] extends 'array' ? unknown[] :
+	Item['type'] extends 'object' ? Record<string, unknown> :
+	Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined :
+	never;
+
+export type GetFormResultType<F extends Form> = {
+	[P in keyof F]: GetItemType<F[P]>;
+};
diff --git a/packages/frontend/src/utility/format-time-string.ts b/packages/frontend/src/utility/format-time-string.ts
new file mode 100644
index 0000000000..d383f143e1
--- /dev/null
+++ b/packages/frontend/src/utility/format-time-string.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const defaultLocaleStringFormats: { [index: string]: string } = {
+	'weekday': 'narrow',
+	'era': 'narrow',
+	'year': 'numeric',
+	'month': 'numeric',
+	'day': 'numeric',
+	'hour': 'numeric',
+	'minute': 'numeric',
+	'second': 'numeric',
+	'timeZoneName': 'short',
+};
+
+function formatLocaleString(date: Date, format: string): string {
+	return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => {
+		if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) {
+			return date.toLocaleString(window.navigator.language, { [kind]: option ? option : defaultLocaleStringFormats[kind] });
+		} else {
+			return match;
+		}
+	});
+}
+
+export function formatDateTimeString(date: Date, format: string): string {
+	return format
+		.replace(/yyyy/g, date.getFullYear().toString())
+		.replace(/yy/g, date.getFullYear().toString().slice(-2))
+		.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long' }))
+		.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short' }))
+		.replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2))
+		.replace(/M/g, (date.getMonth() + 1).toString())
+		.replace(/dd/g, (`0${date.getDate()}`).slice(-2))
+		.replace(/d/g, date.getDate().toString())
+		.replace(/HH/g, (`0${date.getHours()}`).slice(-2))
+		.replace(/H/g, date.getHours().toString())
+		.replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2))
+		.replace(/h/g, ((date.getHours() % 12) || 12).toString())
+		.replace(/mm/g, (`0${date.getMinutes()}`).slice(-2))
+		.replace(/m/g, date.getMinutes().toString())
+		.replace(/ss/g, (`0${date.getSeconds()}`).slice(-2))
+		.replace(/s/g, date.getSeconds().toString())
+		.replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM');
+}
+
+export function formatTimeString(date: Date, format: string): string {
+	return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
+		if (localeformat) return formatLocaleString(date, localeformat);
+		if (datetimeformat) return formatDateTimeString(date, datetimeformat);
+		return match;
+	});
+}
diff --git a/packages/frontend/src/utility/fullscreen.ts b/packages/frontend/src/utility/fullscreen.ts
new file mode 100644
index 0000000000..7a0a018ef3
--- /dev/null
+++ b/packages/frontend/src/utility/fullscreen.ts
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
+
+type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
+	webkitEnterFullscreen?(): void;
+	webkitExitFullscreen?(): void;
+};
+
+type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
+
+type RequestFullscreenProps = {
+	readonly videoEl: VideoEl;
+	readonly playerEl: PlayerEl;
+	readonly options?: FullscreenOptions | null;
+};
+
+type ExitFullscreenProps = {
+	readonly videoEl: VideoEl;
+};
+
+export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
+	if (playerEl.requestFullscreen != null) {
+		playerEl.requestFullscreen(options ?? undefined);
+		return;
+	}
+	if (videoEl.webkitEnterFullscreen != null) {
+		videoEl.webkitEnterFullscreen();
+		return;
+	}
+};
+
+export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
+	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+	if (document.exitFullscreen != null) {
+		document.exitFullscreen();
+		return;
+	}
+	if (videoEl.webkitExitFullscreen != null) {
+		videoEl.webkitExitFullscreen();
+		return;
+	}
+};
diff --git a/packages/frontend/src/utility/get-account-from-id.ts b/packages/frontend/src/utility/get-account-from-id.ts
new file mode 100644
index 0000000000..5d9662a747
--- /dev/null
+++ b/packages/frontend/src/utility/get-account-from-id.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { get } from '@/utility/idb-proxy.js';
+
+export async function getAccountFromId(id: string) {
+	const accounts = await get('accounts') as { token: string; id: string; }[];
+	if (!accounts) console.log('Accounts are not recorded');
+	return accounts.find(account => account.id === id);
+}
diff --git a/packages/frontend/src/utility/get-appear-note.ts b/packages/frontend/src/utility/get-appear-note.ts
new file mode 100644
index 0000000000..40ce80eac9
--- /dev/null
+++ b/packages/frontend/src/utility/get-appear-note.ts
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+
+export function getAppearNote(note: Misskey.entities.Note) {
+	return Misskey.note.isPureRenote(note) ? note.renote : note;
+}
diff --git a/packages/frontend/src/utility/get-bg-color.ts b/packages/frontend/src/utility/get-bg-color.ts
new file mode 100644
index 0000000000..ccf60b454f
--- /dev/null
+++ b/packages/frontend/src/utility/get-bg-color.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import tinycolor from 'tinycolor2';
+
+export const getBgColor = (elem?: Element | null | undefined): string | null => {
+	if (elem == null) return null;
+
+	const { backgroundColor: bg } = window.getComputedStyle(elem);
+
+	if (bg && tinycolor(bg).getAlpha() !== 0) {
+		return bg;
+	}
+
+	return getBgColor(elem.parentElement);
+};
diff --git a/packages/frontend/src/utility/get-dom-node-or-null.ts b/packages/frontend/src/utility/get-dom-node-or-null.ts
new file mode 100644
index 0000000000..fbf54675fd
--- /dev/null
+++ b/packages/frontend/src/utility/get-dom-node-or-null.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const getNodeOrNull = (input: unknown): Node | null => {
+	if (input instanceof Node) return input;
+	return null;
+};
+
+export const getElementOrNull = (input: unknown): Element | null => {
+	if (input instanceof Element) return input;
+	return null;
+};
+
+export const getHTMLElementOrNull = (input: unknown): HTMLElement | null => {
+	if (input instanceof HTMLElement) return input;
+	return null;
+};
diff --git a/packages/frontend/src/utility/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts
new file mode 100644
index 0000000000..477043a342
--- /dev/null
+++ b/packages/frontend/src/utility/get-drive-file-menu.ts
@@ -0,0 +1,162 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { defineAsyncComponent } from 'vue';
+import type { MenuItem } from '@/types/menu.js';
+import { i18n } from '@/i18n.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { prefer } from '@/preferences.js';
+
+function rename(file: Misskey.entities.DriveFile) {
+	os.inputText({
+		title: i18n.ts.renameFile,
+		placeholder: i18n.ts.inputNewFileName,
+		default: file.name,
+	}).then(({ canceled, result: name }) => {
+		if (canceled) return;
+		misskeyApi('drive/files/update', {
+			fileId: file.id,
+			name: name,
+		});
+	});
+}
+
+function describe(file: Misskey.entities.DriveFile) {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
+		default: file.comment ?? '',
+		file: file,
+	}, {
+		done: caption => {
+			misskeyApi('drive/files/update', {
+				fileId: file.id,
+				comment: caption.length === 0 ? null : caption,
+			});
+		},
+		closed: () => dispose(),
+	});
+}
+
+function move(file: Misskey.entities.DriveFile) {
+	os.selectDriveFolder(false).then(folder => {
+		misskeyApi('drive/files/update', {
+			fileId: file.id,
+			folderId: folder[0] ? folder[0].id : null,
+		});
+	});
+}
+
+function toggleSensitive(file: Misskey.entities.DriveFile) {
+	misskeyApi('drive/files/update', {
+		fileId: file.id,
+		isSensitive: !file.isSensitive,
+	}).catch(err => {
+		os.alert({
+			type: 'error',
+			title: i18n.ts.error,
+			text: err.message,
+		});
+	});
+}
+
+function copyUrl(file: Misskey.entities.DriveFile) {
+	copyToClipboard(file.url);
+	os.success();
+}
+
+/*
+function addApp() {
+	alert('not implemented yet');
+}
+*/
+async function deleteFile(file: Misskey.entities.DriveFile) {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
+	});
+
+	if (canceled) return;
+	misskeyApi('drive/files/delete', {
+		fileId: file.id,
+	});
+}
+
+export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] {
+	const isImage = file.type.startsWith('image/');
+
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
+		type: 'link',
+		to: `/my/drive/file/${file.id}`,
+		text: i18n.ts._fileViewer.title,
+		icon: 'ti ti-info-circle',
+	}, { type: 'divider' }, {
+		text: i18n.ts.rename,
+		icon: 'ti ti-forms',
+		action: () => rename(file),
+	}, {
+		text: i18n.ts.move,
+		icon: 'ti ti-folder-symlink',
+		action: () => move(file),
+	}, {
+		text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
+		icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
+		action: () => toggleSensitive(file),
+	}, {
+		text: i18n.ts.describeFile,
+		icon: 'ti ti-text-caption',
+		action: () => describe(file),
+	});
+
+	if (isImage) {
+		menuItems.push({
+			text: i18n.ts.cropImage,
+			icon: 'ti ti-crop',
+			action: () => os.cropImage(file, {
+				aspectRatio: NaN,
+				uploadFolder: folder ? folder.id : folder,
+			}),
+		});
+	}
+
+	menuItems.push({ type: 'divider' }, {
+		text: i18n.ts.createNoteFromTheFile,
+		icon: 'ti ti-pencil',
+		action: () => os.post({
+			initialFiles: [file],
+		}),
+	}, {
+		text: i18n.ts.copyUrl,
+		icon: 'ti ti-link',
+		action: () => copyUrl(file),
+	}, {
+		type: 'a',
+		href: file.url,
+		target: '_blank',
+		text: i18n.ts.download,
+		icon: 'ti ti-download',
+		download: file.name,
+	}, { type: 'divider' }, {
+		text: i18n.ts.delete,
+		icon: 'ti ti-trash',
+		danger: true,
+		action: () => deleteFile(file),
+	});
+
+	if (prefer.s.devMode) {
+		menuItems.push({ type: 'divider' }, {
+			icon: 'ti ti-id',
+			text: i18n.ts.copyFileId,
+			action: () => {
+				copyToClipboard(file.id);
+			},
+		});
+	}
+
+	return menuItems;
+}
diff --git a/packages/frontend/src/utility/get-embed-code.ts b/packages/frontend/src/utility/get-embed-code.ts
new file mode 100644
index 0000000000..9021520da8
--- /dev/null
+++ b/packages/frontend/src/utility/get-embed-code.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { defineAsyncComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js';
+import { url } from '@@/js/config.js';
+import * as os from '@/os.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js';
+
+const MOBILE_THRESHOLD = 500;
+
+/**
+ * パラメータを正規化する（埋め込みコード作成用）
+ * @param params パラメータ
+ * @returns 正規化されたパラメータ
+ */
+export function normalizeEmbedParams(params: EmbedParams): Record<string, string> {
+	// paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す
+	const normalizedParams: Record<string, string> = {};
+	for (const key in params) {
+		// デフォルトの値と同じならparamsに含めない
+		if (params[key] == null || params[key] === defaultEmbedParams[key]) {
+			continue;
+		}
+		switch (typeof params[key]) {
+			case 'number':
+				normalizedParams[key] = params[key].toString();
+				break;
+			case 'boolean':
+				normalizedParams[key] = params[key] ? 'true' : 'false';
+				break;
+			default:
+				normalizedParams[key] = params[key];
+				break;
+		}
+	}
+	return normalizedParams;
+}
+
+/**
+ * 埋め込みコードを生成（iframe IDの発番もやる）
+ */
+export function getEmbedCode(path: string, params?: EmbedParams): string {
+	const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく
+
+	let paramString = '';
+	if (params) {
+		const searchParams = new URLSearchParams(normalizeEmbedParams(params));
+		paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString();
+	}
+
+	const iframeCode = [
+		`<iframe src="${url + path + paramString}" data-misskey-embed-id="${iframeId}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" style="border: none; width: 100%; max-width: 500px; height: 300px; color-scheme: light dark;"></iframe>`,
+		`<script defer src="${url}/embed.js"></script>`,
+	];
+	return iframeCode.join('\n');
+}
+
+/**
+ * 埋め込みコードを生成してコピーする（カスタマイズ機能つき）
+ *
+ * カスタマイズ機能がいらない場合（事前にパラメータを指定する場合）は getEmbedCode を直接使ってください
+ */
+export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) {
+	const _params = { ...params };
+
+	if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) {
+		_params.maxHeight = 700;
+	}
+
+	// PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー
+	if (window.innerWidth < MOBILE_THRESHOLD) {
+		copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params));
+		os.success();
+	} else {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), {
+			entity,
+			id,
+			params: _params,
+		}, {
+			closed: () => dispose(),
+		});
+	}
+}
diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts
new file mode 100644
index 0000000000..c95eaa20dd
--- /dev/null
+++ b/packages/frontend/src/utility/get-note-menu.ts
@@ -0,0 +1,685 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent } from 'vue';
+import type { Ref, ShallowRef } from 'vue';
+import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
+import { claimAchievement } from './achievements.js';
+import type { MenuItem } from '@/types/menu.js';
+import { $i } from '@/account.js';
+import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { store, noteActions } from '@/store.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { getUserMenu } from '@/utility/get-user-menu.js';
+import { clipsCache, favoritedChannelsCache } from '@/cache.js';
+import MkRippleEffect from '@/components/MkRippleEffect.vue';
+import { isSupportShare } from '@/utility/navigator.js';
+import { getAppearNote } from '@/utility/get-appear-note.js';
+import { genEmbedCode } from '@/utility/get-embed-code.js';
+import { prefer } from '@/preferences.js';
+
+export async function getNoteClipMenu(props: {
+	note: Misskey.entities.Note;
+	isDeleted: Ref<boolean>;
+	currentClip?: Misskey.entities.Clip;
+}) {
+	function getClipName(clip: Misskey.entities.Clip) {
+		if ($i && clip.userId === $i.id && clip.notesCount != null) {
+			return `${clip.name} (${clip.notesCount}/${$i.policies.noteEachClipsLimit})`;
+		} else {
+			return clip.name;
+		}
+	}
+
+	const appearNote = getAppearNote(props.note);
+
+	const clips = await clipsCache.fetch();
+	const menu: MenuItem[] = [...clips.map(clip => ({
+		text: getClipName(clip),
+		action: () => {
+			claimAchievement('noteClipped1');
+			os.promiseDialog(
+				misskeyApi('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
+				null,
+				async (err) => {
+					if (err.id === '734806c4-542c-463a-9311-15c512803965') {
+						const confirm = await os.confirm({
+							type: 'warning',
+							text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }),
+						});
+						if (!confirm.canceled) {
+							os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }).then(() => {
+								clipsCache.set(clips.map(c => {
+									if (c.id === clip.id) {
+										return {
+											...c,
+											notesCount: Math.max(0, ((c.notesCount ?? 0) - 1)),
+										};
+									} else {
+										return c;
+									}
+								}));
+							});
+							if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
+						}
+					} else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') {
+						os.alert({
+							type: 'error',
+							text: i18n.ts.clipNoteLimitExceeded,
+						});
+					} else {
+						os.alert({
+							type: 'error',
+							text: err.message + '\n' + err.id,
+						});
+					}
+				},
+			).then(() => {
+				clipsCache.set(clips.map(c => {
+					if (c.id === clip.id) {
+						return {
+							...c,
+							notesCount: (c.notesCount ?? 0) + 1,
+						};
+					} else {
+						return c;
+					}
+				}));
+			});
+		},
+	})), { type: 'divider' }, {
+		icon: 'ti ti-plus',
+		text: i18n.ts.createNew,
+		action: async () => {
+			const { canceled, result } = await os.form(i18n.ts.createNewClip, {
+				name: {
+					type: 'string',
+					default: null,
+					label: i18n.ts.name,
+				},
+				description: {
+					type: 'string',
+					required: false,
+					default: null,
+					multiline: true,
+					label: i18n.ts.description,
+				},
+				isPublic: {
+					type: 'boolean',
+					label: i18n.ts.public,
+					default: false,
+				},
+			});
+			if (canceled) return;
+
+			const clip = await os.apiWithDialog('clips/create', result);
+
+			clipsCache.delete();
+
+			claimAchievement('noteClipped1');
+			os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
+		},
+	}];
+
+	return menu;
+}
+
+export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem {
+	return {
+		icon: 'ti ti-exclamation-circle',
+		text,
+		action: (): void => {
+			const localUrl = `${url}/notes/${note.id}`;
+			let noteInfo = '';
+			if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`;
+			noteInfo += `Local Note: ${localUrl}\n`;
+			const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
+				user: note.user,
+				initialComment: `${noteInfo}-----\n`,
+			}, {
+				closed: () => dispose(),
+			});
+		},
+	};
+}
+
+export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem {
+	return {
+		icon: 'ti ti-link',
+		text,
+		action: (): void => {
+			copyToClipboard(`${url}/notes/${note.id}`);
+			os.success();
+		},
+	};
+}
+
+function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined {
+	if (note.url != null || note.uri != null) return undefined;
+	if (['specified', 'followers'].includes(note.visibility)) return undefined;
+
+	return {
+		icon: 'ti ti-code',
+		text,
+		action: (): void => {
+			genEmbedCode('notes', note.id);
+		},
+	};
+}
+
+export function getNoteMenu(props: {
+	note: Misskey.entities.Note;
+	translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
+	translating: Ref<boolean>;
+	isDeleted: Ref<boolean>;
+	currentClip?: Misskey.entities.Clip;
+}) {
+	const appearNote = getAppearNote(props.note);
+
+	const cleanups = [] as (() => void)[];
+
+	function del(): void {
+		os.confirm({
+			type: 'warning',
+			text: i18n.ts.noteDeleteConfirm,
+		}).then(({ canceled }) => {
+			if (canceled) return;
+
+			misskeyApi('notes/delete', {
+				noteId: appearNote.id,
+			});
+
+			if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
+				claimAchievement('noteDeletedWithin1min');
+			}
+		});
+	}
+
+	function delEdit(): void {
+		os.confirm({
+			type: 'warning',
+			text: i18n.ts.deleteAndEditConfirm,
+		}).then(({ canceled }) => {
+			if (canceled) return;
+
+			misskeyApi('notes/delete', {
+				noteId: appearNote.id,
+			});
+
+			os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
+
+			if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
+				claimAchievement('noteDeletedWithin1min');
+			}
+		});
+	}
+
+	function toggleFavorite(favorite: boolean): void {
+		claimAchievement('noteFavorited1');
+		os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
+			noteId: appearNote.id,
+		});
+	}
+
+	function toggleThreadMute(mute: boolean): void {
+		os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+			noteId: appearNote.id,
+		});
+	}
+
+	function copyContent(): void {
+		copyToClipboard(appearNote.text);
+		os.success();
+	}
+
+	function togglePin(pin: boolean): void {
+		os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
+			noteId: appearNote.id,
+		}, undefined, {
+			'72dab508-c64d-498f-8740-a8eec1ba385a': {
+				text: i18n.ts.pinLimitExceeded,
+			},
+		});
+	}
+
+	async function unclip(): Promise<void> {
+		if (!props.currentClip) return;
+		os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
+		props.isDeleted.value = true;
+	}
+
+	async function promote(): Promise<void> {
+		const { canceled, result: days } = await os.inputNumber({
+			title: i18n.ts.numberOfDays,
+		});
+
+		if (canceled || days == null) return;
+
+		os.apiWithDialog('admin/promo/create', {
+			noteId: appearNote.id,
+			expiresAt: Date.now() + (86400000 * days),
+		});
+	}
+
+	function share(): void {
+		navigator.share({
+			title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }),
+			text: appearNote.text ?? '',
+			url: `${url}/notes/${appearNote.id}`,
+		});
+	}
+
+	function openDetail(): void {
+		os.pageWindow(`/notes/${appearNote.id}`);
+	}
+
+	async function translate(): Promise<void> {
+		if (props.translation.value != null) return;
+		props.translating.value = true;
+		const res = await misskeyApi('notes/translate', {
+			noteId: appearNote.id,
+			targetLang: miLocalStorage.getItem('lang') ?? navigator.language,
+		});
+		props.translating.value = false;
+		props.translation.value = res;
+	}
+
+	const menuItems: MenuItem[] = [];
+
+	if ($i) {
+		const statePromise = misskeyApi('notes/state', {
+			noteId: appearNote.id,
+		});
+
+		if (props.currentClip?.userId === $i.id) {
+			menuItems.push({
+				icon: 'ti ti-backspace',
+				text: i18n.ts.unclip,
+				danger: true,
+				action: unclip,
+			}, { type: 'divider' });
+		}
+
+		menuItems.push({
+			icon: 'ti ti-info-circle',
+			text: i18n.ts.details,
+			action: openDetail,
+		}, {
+			icon: 'ti ti-copy',
+			text: i18n.ts.copyContent,
+			action: copyContent,
+		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
+
+		if (appearNote.url || appearNote.uri) {
+			menuItems.push({
+				icon: 'ti ti-link',
+				text: i18n.ts.copyRemoteLink,
+				action: () => {
+					copyToClipboard(appearNote.url ?? appearNote.uri);
+					os.success();
+				},
+			}, {
+				icon: 'ti ti-external-link',
+				text: i18n.ts.showOnRemote,
+				action: () => {
+					window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
+				},
+			});
+		} else {
+			menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
+		}
+
+		if (isSupportShare()) {
+			menuItems.push({
+				icon: 'ti ti-share',
+				text: i18n.ts.share,
+				action: share,
+			});
+		}
+
+		if ($i.policies.canUseTranslator && instance.translatorAvailable) {
+			menuItems.push({
+				icon: 'ti ti-language-hiragana',
+				text: i18n.ts.translate,
+				action: translate,
+			});
+		}
+
+		menuItems.push({ type: 'divider' });
+
+		menuItems.push(statePromise.then(state => state.isFavorited ? {
+			icon: 'ti ti-star-off',
+			text: i18n.ts.unfavorite,
+			action: () => toggleFavorite(false),
+		} : {
+			icon: 'ti ti-star',
+			text: i18n.ts.favorite,
+			action: () => toggleFavorite(true),
+		}));
+
+		menuItems.push({
+			type: 'parent',
+			icon: 'ti ti-paperclip',
+			text: i18n.ts.clip,
+			children: () => getNoteClipMenu(props),
+		});
+
+		menuItems.push(statePromise.then(state => state.isMutedThread ? {
+			icon: 'ti ti-message-off',
+			text: i18n.ts.unmuteThread,
+			action: () => toggleThreadMute(false),
+		} : {
+			icon: 'ti ti-message-off',
+			text: i18n.ts.muteThread,
+			action: () => toggleThreadMute(true),
+		}));
+
+		if (appearNote.userId === $i.id) {
+			if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) {
+				menuItems.push({
+					icon: 'ti ti-pinned-off',
+					text: i18n.ts.unpin,
+					action: () => togglePin(false),
+				});
+			} else {
+				menuItems.push({
+					icon: 'ti ti-pin',
+					text: i18n.ts.pin,
+					action: () => togglePin(true),
+				});
+			}
+		}
+
+		menuItems.push({
+			type: 'parent',
+			icon: 'ti ti-user',
+			text: i18n.ts.user,
+			children: async () => {
+				const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
+				const { menu, cleanup } = getUserMenu(user);
+				cleanups.push(cleanup);
+				return menu;
+			},
+		});
+
+		if (appearNote.userId !== $i.id) {
+			menuItems.push({ type: 'divider' });
+			menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse));
+		}
+
+		if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) {
+			menuItems.push({ type: 'divider' });
+			menuItems.push({
+				type: 'parent',
+				icon: 'ti ti-device-tv',
+				text: i18n.ts.channel,
+				children: async () => {
+					const channelChildMenu = [] as MenuItem[];
+
+					const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id });
+
+					if (channel.pinnedNoteIds.includes(appearNote.id)) {
+						channelChildMenu.push({
+							icon: 'ti ti-pinned-off',
+							text: i18n.ts.unpin,
+							action: () => os.apiWithDialog('channels/update', {
+								channelId: appearNote.channel!.id,
+								pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id),
+							}),
+						});
+					} else {
+						channelChildMenu.push({
+							icon: 'ti ti-pin',
+							text: i18n.ts.pin,
+							action: () => os.apiWithDialog('channels/update', {
+								channelId: appearNote.channel!.id,
+								pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id],
+							}),
+						});
+					}
+					return channelChildMenu;
+				},
+			});
+		}
+
+		if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) {
+			menuItems.push({ type: 'divider' });
+			if (appearNote.userId === $i.id) {
+				menuItems.push({
+					icon: 'ti ti-edit',
+					text: i18n.ts.deleteAndEdit,
+					action: delEdit,
+				});
+			}
+			menuItems.push({
+				icon: 'ti ti-trash',
+				text: i18n.ts.delete,
+				danger: true,
+				action: del,
+			});
+		}
+	} else {
+		menuItems.push({
+			icon: 'ti ti-info-circle',
+			text: i18n.ts.details,
+			action: openDetail,
+		}, {
+			icon: 'ti ti-copy',
+			text: i18n.ts.copyContent,
+			action: copyContent,
+		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
+
+		if (appearNote.url || appearNote.uri) {
+			menuItems.push({
+				icon: 'ti ti-link',
+				text: i18n.ts.copyRemoteLink,
+				action: () => {
+					copyToClipboard(appearNote.url ?? appearNote.uri);
+					os.success();
+				},
+			}, {
+				icon: 'ti ti-external-link',
+				text: i18n.ts.showOnRemote,
+				action: () => {
+					window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
+				},
+			});
+		} else {
+			menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
+		}
+	}
+
+	if (noteActions.length > 0) {
+		menuItems.push({ type: 'divider' });
+
+		menuItems.push(...noteActions.map(action => ({
+			icon: 'ti ti-plug',
+			text: action.title,
+			action: () => {
+				action.handler(appearNote);
+			},
+		})));
+	}
+
+	if (prefer.s.devMode) {
+		menuItems.push({ type: 'divider' }, {
+			icon: 'ti ti-id',
+			text: i18n.ts.copyNoteId,
+			action: () => {
+				copyToClipboard(appearNote.id);
+				os.success();
+			},
+		});
+	}
+
+	const cleanup = () => {
+		if (_DEV_) console.log('note menu cleanup', cleanups);
+		for (const cl of cleanups) {
+			cl();
+		}
+	};
+
+	return {
+		menu: menuItems,
+		cleanup,
+	};
+}
+
+type Visibility = (typeof Misskey.noteVisibilities)[number];
+
+function smallerVisibility(a: Visibility, b: Visibility): Visibility {
+	if (a === 'specified' || b === 'specified') return 'specified';
+	if (a === 'followers' || b === 'followers') return 'followers';
+	if (a === 'home' || b === 'home') return 'home';
+	// if (a === 'public' || b === 'public')
+	return 'public';
+}
+
+export function getRenoteMenu(props: {
+	note: Misskey.entities.Note;
+	renoteButton: ShallowRef<HTMLElement | undefined>;
+	mock?: boolean;
+}) {
+	const appearNote = getAppearNote(props.note);
+
+	const channelRenoteItems: MenuItem[] = [];
+	const normalRenoteItems: MenuItem[] = [];
+	const normalExternalChannelRenoteItems: MenuItem[] = [];
+
+	if (appearNote.channel) {
+		channelRenoteItems.push(...[{
+			text: i18n.ts.inChannelRenote,
+			icon: 'ti ti-repeat',
+			action: () => {
+				const el = props.renoteButton.value;
+				if (el && prefer.s.animation) {
+					const rect = el.getBoundingClientRect();
+					const x = rect.left + (el.offsetWidth / 2);
+					const y = rect.top + (el.offsetHeight / 2);
+					const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+						end: () => dispose(),
+					});
+				}
+
+				if (!props.mock) {
+					misskeyApi('notes/create', {
+						renoteId: appearNote.id,
+						channelId: appearNote.channelId,
+					}).then(() => {
+						os.toast(i18n.ts.renoted);
+					});
+				}
+			},
+		}, {
+			text: i18n.ts.inChannelQuote,
+			icon: 'ti ti-quote',
+			action: () => {
+				if (!props.mock) {
+					os.post({
+						renote: appearNote,
+						channel: appearNote.channel,
+					});
+				}
+			},
+		}]);
+	}
+
+	if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) {
+		normalRenoteItems.push(...[{
+			text: i18n.ts.renote,
+			icon: 'ti ti-repeat',
+			action: () => {
+				const el = props.renoteButton.value;
+				if (el && prefer.s.animation) {
+					const rect = el.getBoundingClientRect();
+					const x = rect.left + (el.offsetWidth / 2);
+					const y = rect.top + (el.offsetHeight / 2);
+					const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+						end: () => dispose(),
+					});
+				}
+
+				const configuredVisibility = prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility;
+				const localOnly = prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly;
+
+				let visibility = appearNote.visibility;
+				visibility = smallerVisibility(visibility, configuredVisibility);
+				if (appearNote.channel?.isSensitive) {
+					visibility = smallerVisibility(visibility, 'home');
+				}
+
+				if (!props.mock) {
+					misskeyApi('notes/create', {
+						localOnly,
+						visibility,
+						renoteId: appearNote.id,
+					}).then(() => {
+						os.toast(i18n.ts.renoted);
+					});
+				}
+			},
+		}, (props.mock) ? undefined : {
+			text: i18n.ts.quote,
+			icon: 'ti ti-quote',
+			action: () => {
+				os.post({
+					renote: appearNote,
+				});
+			},
+		}]);
+
+		normalExternalChannelRenoteItems.push({
+			type: 'parent',
+			icon: 'ti ti-repeat',
+			text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel,
+			children: async () => {
+				const channels = await favoritedChannelsCache.fetch();
+				return channels.filter((channel) => {
+					if (!appearNote.channelId) return true;
+					return channel.id !== appearNote.channelId;
+				}).map((channel) => ({
+					text: channel.name,
+					action: () => {
+						const el = props.renoteButton.value;
+						if (el && prefer.s.animation) {
+							const rect = el.getBoundingClientRect();
+							const x = rect.left + (el.offsetWidth / 2);
+							const y = rect.top + (el.offsetHeight / 2);
+							const { dispose } = os.popup(MkRippleEffect, { x, y }, {
+								end: () => dispose(),
+							});
+						}
+
+						if (!props.mock) {
+							misskeyApi('notes/create', {
+								renoteId: appearNote.id,
+								channelId: channel.id,
+							}).then(() => {
+								os.toast(i18n.tsx.renotedToX({ name: channel.name }));
+							});
+						}
+					},
+				}));
+			},
+		});
+	}
+
+	const renoteItems = [
+		...normalRenoteItems,
+		...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [],
+		...channelRenoteItems,
+		...(normalExternalChannelRenoteItems.length > 0 && (normalRenoteItems.length > 0 || channelRenoteItems.length > 0)) ? [{ type: 'divider' }] as MenuItem[] : [],
+		...normalExternalChannelRenoteItems,
+	];
+
+	return {
+		menu: renoteItems,
+	};
+}
diff --git a/packages/frontend/src/utility/get-note-summary.ts b/packages/frontend/src/utility/get-note-summary.ts
new file mode 100644
index 0000000000..6fd9947ac1
--- /dev/null
+++ b/packages/frontend/src/utility/get-note-summary.ts
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { i18n } from '@/i18n.js';
+
+/**
+ * 投稿を表す文字列を取得します。
+ * @param {*} note (packされた)投稿
+ */
+export const getNoteSummary = (note?: Misskey.entities.Note | null): string => {
+	if (note == null) {
+		return '';
+	}
+
+	if (note.deletedAt) {
+		return `(${i18n.ts.deletedNote})`;
+	}
+
+	if (note.isHidden) {
+		return `(${i18n.ts.invisibleNote})`;
+	}
+
+	let summary = '';
+
+	// 本文
+	if (note.cw != null) {
+		summary += note.cw;
+	} else {
+		summary += note.text ? note.text : '';
+	}
+
+	// ファイルが添付されているとき
+	if ((note.files || []).length !== 0) {
+		summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`;
+	}
+
+	// 投票が添付されているとき
+	if (note.poll) {
+		summary += ` (${i18n.ts.poll})`;
+	}
+
+	// 返信のとき
+	if (note.replyId) {
+		if (note.reply) {
+			summary += `\n\nRE: ${getNoteSummary(note.reply)}`;
+		} else {
+			summary += '\n\nRE: ...';
+		}
+	}
+
+	// Renoteのとき
+	if (note.renoteId) {
+		if (note.renote) {
+			summary += `\n\nRN: ${getNoteSummary(note.renote)}`;
+		} else {
+			summary += '\n\nRN: ...';
+		}
+	}
+
+	return summary.trim();
+};
diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts
new file mode 100644
index 0000000000..d739976cb1
--- /dev/null
+++ b/packages/frontend/src/utility/get-user-menu.ts
@@ -0,0 +1,441 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { toUnicode } from 'punycode.js';
+import { defineAsyncComponent, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
+import { host, url } from '@@/js/config.js';
+import type { IRouter } from '@/nirax.js';
+import type { MenuItem } from '@/types/menu.js';
+import { i18n } from '@/i18n.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { userActions } from '@/store.js';
+import { $i, iAmModerator } from '@/account.js';
+import { notesSearchAvailable, canSearchNonLocalNotes } from '@/utility/check-permissions.js';
+import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
+import { mainRouter } from '@/router/main.js';
+import { genEmbedCode } from '@/utility/get-embed-code.js';
+import { prefer } from '@/preferences.js';
+
+export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
+	const meId = $i ? $i.id : null;
+
+	const cleanups = [] as (() => void)[];
+
+	async function toggleMute() {
+		if (user.isMuted) {
+			os.apiWithDialog('mute/delete', {
+				userId: user.id,
+			}).then(() => {
+				user.isMuted = false;
+			});
+		} else {
+			const { canceled, result: period } = await os.select({
+				title: i18n.ts.mutePeriod,
+				items: [{
+					value: 'indefinitely', text: i18n.ts.indefinitely,
+				}, {
+					value: 'tenMinutes', text: i18n.ts.tenMinutes,
+				}, {
+					value: 'oneHour', text: i18n.ts.oneHour,
+				}, {
+					value: 'oneDay', text: i18n.ts.oneDay,
+				}, {
+					value: 'oneWeek', text: i18n.ts.oneWeek,
+				}],
+				default: 'indefinitely',
+			});
+			if (canceled) return;
+
+			const expiresAt = period === 'indefinitely' ? null
+				: period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
+				: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
+				: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
+				: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
+				: null;
+
+			os.apiWithDialog('mute/create', {
+				userId: user.id,
+				expiresAt,
+			}).then(() => {
+				user.isMuted = true;
+			});
+		}
+	}
+
+	async function toggleRenoteMute() {
+		os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', {
+			userId: user.id,
+		}).then(() => {
+			user.isRenoteMuted = !user.isRenoteMuted;
+		});
+	}
+
+	async function toggleBlock() {
+		if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
+
+		os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', {
+			userId: user.id,
+		}).then(() => {
+			user.isBlocking = !user.isBlocking;
+		});
+	}
+
+	async function toggleNotify() {
+		os.apiWithDialog('following/update', {
+			userId: user.id,
+			notify: user.notify === 'normal' ? 'none' : 'normal',
+		}).then(() => {
+			user.notify = user.notify === 'normal' ? 'none' : 'normal';
+		});
+	}
+
+	function reportAbuse() {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
+			user: user,
+		}, {
+			closed: () => dispose(),
+		});
+	}
+
+	async function getConfirmed(text: string): Promise<boolean> {
+		const confirm = await os.confirm({
+			type: 'warning',
+			title: 'confirm',
+			text,
+		});
+
+		return !confirm.canceled;
+	}
+
+	async function userInfoUpdate() {
+		os.apiWithDialog('federation/update-remote-user', {
+			userId: user.id,
+		});
+	}
+
+	async function invalidateFollow() {
+		if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return;
+
+		os.apiWithDialog('following/invalidate', {
+			userId: user.id,
+		}).then(() => {
+			user.isFollowed = !user.isFollowed;
+		});
+	}
+
+	async function editMemo(): Promise<void> {
+		const userDetailed = await misskeyApi('users/show', {
+			userId: user.id,
+		});
+		const { canceled, result } = await os.form(i18n.ts.editMemo, {
+			memo: {
+				type: 'string',
+				required: true,
+				multiline: true,
+				label: i18n.ts.memo,
+				default: userDetailed.memo,
+			},
+		});
+		if (canceled) return;
+
+		os.apiWithDialog('users/update-memo', {
+			memo: result.memo,
+			userId: user.id,
+		});
+	}
+
+	const menuItems: MenuItem[] = [];
+
+	menuItems.push({
+		icon: 'ti ti-at',
+		text: i18n.ts.copyUsername,
+		action: () => {
+			copyToClipboard(`@${user.username}@${user.host ?? host}`);
+		},
+	});
+
+	if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) {
+		menuItems.push({
+			icon: 'ti ti-search',
+			text: i18n.ts.searchThisUsersNotes,
+			action: () => {
+				router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
+			},
+		});
+	}
+
+	if (iAmModerator) {
+		menuItems.push({
+			icon: 'ti ti-user-exclamation',
+			text: i18n.ts.moderation,
+			action: () => {
+				router.push(`/admin/user/${user.id}`);
+			},
+		});
+	}
+
+	menuItems.push({
+		icon: 'ti ti-rss',
+		text: i18n.ts.copyRSS,
+		action: () => {
+			copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
+		},
+	});
+
+	if (user.host != null && user.url != null) {
+		menuItems.push({
+			icon: 'ti ti-external-link',
+			text: i18n.ts.showOnRemote,
+			action: () => {
+				if (user.url == null) return;
+				window.open(user.url, '_blank', 'noopener');
+			},
+		});
+	} else {
+		menuItems.push({
+			icon: 'ti ti-code',
+			text: i18n.ts.genEmbedCode,
+			type: 'parent',
+			children: [{
+				text: i18n.ts.noteOfThisUser,
+				action: () => {
+					genEmbedCode('user-timeline', user.id);
+				},
+			}], // TODO: ユーザーカードの埋め込みなど
+		});
+	}
+
+	menuItems.push({
+		icon: 'ti ti-share',
+		text: i18n.ts.copyProfileUrl,
+		action: () => {
+			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
+			copyToClipboard(`${url}/${canonical}`);
+		},
+	});
+
+	if ($i) {
+		menuItems.push({
+			icon: 'ti ti-mail',
+			text: i18n.ts.sendMessage,
+			action: () => {
+				const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
+				os.post({ specified: user, initialText: `${canonical} ` });
+			},
+		}, { type: 'divider' }, {
+			icon: 'ti ti-pencil',
+			text: i18n.ts.editMemo,
+			action: editMemo,
+		}, {
+			type: 'parent',
+			icon: 'ti ti-list',
+			text: i18n.ts.addToList,
+			children: async () => {
+				const lists = await userListsCache.fetch();
+				return lists.map(list => {
+					const isListed = ref(list.userIds?.includes(user.id) ?? false);
+					cleanups.push(watch(isListed, () => {
+						if (isListed.value) {
+							os.apiWithDialog('users/lists/push', {
+								listId: list.id,
+								userId: user.id,
+							}).then(() => {
+								list.userIds?.push(user.id);
+							});
+						} else {
+							os.apiWithDialog('users/lists/pull', {
+								listId: list.id,
+								userId: user.id,
+							}).then(() => {
+								list.userIds?.splice(list.userIds.indexOf(user.id), 1);
+							});
+						}
+					}));
+
+					return {
+						type: 'switch',
+						text: list.name,
+						ref: isListed,
+					};
+				});
+			},
+		}, {
+			type: 'parent',
+			icon: 'ti ti-antenna',
+			text: i18n.ts.addToAntenna,
+			children: async () => {
+				const antennas = await antennasCache.fetch();
+				const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
+				return antennas.filter((a) => a.src === 'users').map(antenna => ({
+					text: antenna.name,
+					action: async () => {
+						await os.apiWithDialog('antennas/update', {
+							antennaId: antenna.id,
+							name: antenna.name,
+							keywords: antenna.keywords,
+							excludeKeywords: antenna.excludeKeywords,
+							src: antenna.src,
+							userListId: antenna.userListId,
+							users: [...antenna.users, canonical],
+							caseSensitive: antenna.caseSensitive,
+							withReplies: antenna.withReplies,
+							withFile: antenna.withFile,
+							notify: antenna.notify,
+						});
+						antennasCache.delete();
+					},
+				}));
+			},
+		});
+	}
+
+	if ($i && meId !== user.id) {
+		if (iAmModerator) {
+			menuItems.push({
+				type: 'parent',
+				icon: 'ti ti-badges',
+				text: i18n.ts.roles,
+				children: async () => {
+					const roles = await rolesCache.fetch();
+
+					return roles.filter(r => r.target === 'manual').map(r => ({
+						text: r.name,
+						action: async () => {
+							const { canceled, result: period } = await os.select({
+								title: i18n.ts.period + ': ' + r.name,
+								items: [{
+									value: 'indefinitely', text: i18n.ts.indefinitely,
+								}, {
+									value: 'oneHour', text: i18n.ts.oneHour,
+								}, {
+									value: 'oneDay', text: i18n.ts.oneDay,
+								}, {
+									value: 'oneWeek', text: i18n.ts.oneWeek,
+								}, {
+									value: 'oneMonth', text: i18n.ts.oneMonth,
+								}],
+								default: 'indefinitely',
+							});
+							if (canceled) return;
+
+							const expiresAt = period === 'indefinitely' ? null
+								: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
+								: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
+								: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
+								: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
+								: null;
+
+							os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt });
+						},
+					}));
+				},
+			});
+		}
+
+		// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
+		//if (user.isFollowing) {
+		const withRepliesRef = ref(user.withReplies ?? false);
+
+		menuItems.push({
+			type: 'switch',
+			icon: 'ti ti-messages',
+			text: i18n.ts.showRepliesToOthersInTimeline,
+			ref: withRepliesRef,
+		}, {
+			icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off',
+			text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes,
+			action: toggleNotify,
+		});
+
+		watch(withRepliesRef, (withReplies) => {
+			misskeyApi('following/update', {
+				userId: user.id,
+				withReplies,
+			}).then(() => {
+				user.withReplies = withReplies;
+			});
+		});
+		//}
+
+		menuItems.push({ type: 'divider' }, {
+			icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
+			text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
+			action: toggleMute,
+		}, {
+			icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off',
+			text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute,
+			action: toggleRenoteMute,
+		}, {
+			icon: 'ti ti-ban',
+			text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
+			action: toggleBlock,
+		});
+
+		if (user.isFollowed) {
+			menuItems.push({
+				icon: 'ti ti-link-off',
+				text: i18n.ts.breakFollow,
+				action: invalidateFollow,
+			});
+		}
+
+		menuItems.push({ type: 'divider' }, {
+			icon: 'ti ti-exclamation-circle',
+			text: i18n.ts.reportAbuse,
+			action: reportAbuse,
+		});
+	}
+
+	if (user.host !== null) {
+		menuItems.push({ type: 'divider' }, {
+			icon: 'ti ti-refresh',
+			text: i18n.ts.updateRemoteUser,
+			action: userInfoUpdate,
+		});
+	}
+
+	if (prefer.s.devMode) {
+		menuItems.push({ type: 'divider' }, {
+			icon: 'ti ti-id',
+			text: i18n.ts.copyUserId,
+			action: () => {
+				copyToClipboard(user.id);
+			},
+		});
+	}
+
+	if ($i && meId === user.id) {
+		menuItems.push({ type: 'divider' }, {
+			icon: 'ti ti-pencil',
+			text: i18n.ts.editProfile,
+			action: () => {
+				router.push('/settings/profile');
+			},
+		});
+	}
+
+	if (userActions.length > 0) {
+		menuItems.push({ type: 'divider' }, ...userActions.map(action => ({
+			icon: 'ti ti-plug',
+			text: action.title,
+			action: () => {
+				action.handler(user);
+			},
+		})));
+	}
+
+	return {
+		menu: menuItems,
+		cleanup: () => {
+			if (_DEV_) console.log('user menu cleanup', cleanups);
+			for (const cl of cleanups) {
+				cl();
+			}
+		},
+	};
+}
diff --git a/packages/frontend/src/utility/get-user-name.ts b/packages/frontend/src/utility/get-user-name.ts
new file mode 100644
index 0000000000..56e91abba0
--- /dev/null
+++ b/packages/frontend/src/utility/get-user-name.ts
@@ -0,0 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export default function(user: { name?: string | null, username: string }): string {
+	return user.name === '' ? user.username : user.name ?? user.username;
+}
diff --git a/packages/frontend/src/utility/hotkey.ts b/packages/frontend/src/utility/hotkey.ts
new file mode 100644
index 0000000000..fe62139a74
--- /dev/null
+++ b/packages/frontend/src/utility/hotkey.ts
@@ -0,0 +1,172 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { getHTMLElementOrNull } from "@/utility/get-dom-node-or-null.js";
+
+//#region types
+export type Keymap = Record<string, CallbackFunction | CallbackObject>;
+
+type CallbackFunction = (ev: KeyboardEvent) => unknown;
+
+type CallbackObject = {
+	callback: CallbackFunction;
+	allowRepeat?: boolean;
+};
+
+type Pattern = {
+	which: string[];
+	ctrl: boolean;
+	alt: boolean;
+	shift: boolean;
+};
+
+type Action = {
+	patterns: Pattern[];
+	callback: CallbackFunction;
+	options: Required<Omit<CallbackObject, 'callback'>>;
+};
+//#endregion
+
+//#region consts
+const KEY_ALIASES = {
+	'esc': 'Escape',
+	'enter': 'Enter',
+	'space': ' ',
+	'up': 'ArrowUp',
+	'down': 'ArrowDown',
+	'left': 'ArrowLeft',
+	'right': 'ArrowRight',
+	'plus': ['+', ';'],
+};
+
+const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
+
+const IGNORE_ELEMENTS = ['input', 'textarea'];
+//#endregion
+
+//#region store
+let latestHotkey: Pattern & { callback: CallbackFunction } | null = null;
+//#endregion
+
+//#region impl
+export const makeHotkey = (keymap: Keymap) => {
+	const actions = parseKeymap(keymap);
+	return (ev: KeyboardEvent) => {
+		if ('pswp' in window && window.pswp != null) return;
+		if (document.activeElement != null) {
+			if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return;
+			if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return;
+		}
+		for (const action of actions) {
+			if (matchPatterns(ev, action)) {
+				ev.preventDefault();
+				ev.stopPropagation();
+				action.callback(ev);
+				storePattern(ev, action.callback);
+			}
+		}
+	};
+};
+
+const parseKeymap = (keymap: Keymap) => {
+	return Object.entries(keymap).map(([rawPatterns, rawCallback]) => {
+		const patterns = parsePatterns(rawPatterns);
+		const callback = parseCallback(rawCallback);
+		const options = parseOptions(rawCallback);
+		return { patterns, callback, options } as const satisfies Action;
+	});
+};
+
+const parsePatterns = (rawPatterns: keyof Keymap) => {
+	return rawPatterns.split('|').map(part => {
+		const keys = part.split('+').map(trimLower);
+		const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x)));
+		const ctrl = keys.includes('ctrl');
+		const alt = keys.includes('alt');
+		const shift = keys.includes('shift');
+		return { which, ctrl, alt, shift } as const satisfies Pattern;
+	});
+};
+
+const parseCallback = (rawCallback: Keymap[keyof Keymap]) => {
+	if (typeof rawCallback === 'object') {
+		return rawCallback.callback;
+	}
+	return rawCallback;
+};
+
+const parseOptions = (rawCallback: Keymap[keyof Keymap]) => {
+	const defaultOptions = {
+		allowRepeat: false,
+	} as const satisfies Action['options'];
+	if (typeof rawCallback === 'object') {
+		const { callback, ...rawOptions } = rawCallback;
+		const options = { ...defaultOptions, ...rawOptions };
+		return { ...options } as const satisfies Action['options'];
+	}
+	return { ...defaultOptions } as const satisfies Action['options'];
+};
+
+const matchPatterns = (ev: KeyboardEvent, action: Action) => {
+	const { patterns, options, callback } = action;
+	if (ev.repeat && !options.allowRepeat) return false;
+	const key = ev.key.toLowerCase();
+	return patterns.some(({ which, ctrl, shift, alt }) => {
+		if (
+			options.allowRepeat === false &&
+			latestHotkey != null &&
+			latestHotkey.which.includes(key) &&
+			latestHotkey.ctrl === ctrl &&
+			latestHotkey.alt === alt &&
+			latestHotkey.shift === shift &&
+			latestHotkey.callback === callback
+		) {
+			return false;
+		}
+		if (!which.includes(key)) return false;
+		if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false;
+		if (alt !== ev.altKey) return false;
+		if (shift !== ev.shiftKey) return false;
+		return true;
+	});
+};
+
+let lastHotKeyStoreTimer: number | null = null;
+
+const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => {
+	if (lastHotKeyStoreTimer != null) {
+		clearTimeout(lastHotKeyStoreTimer);
+	}
+
+	latestHotkey = {
+		which: [ev.key.toLowerCase()],
+		ctrl: ev.ctrlKey || ev.metaKey,
+		alt: ev.altKey,
+		shift: ev.shiftKey,
+		callback,
+	};
+
+	lastHotKeyStoreTimer = window.setTimeout(() => {
+		latestHotkey = null;
+	}, 500);
+};
+
+const parseKeyCode = (input?: string | null) => {
+	if (input == null) return [];
+	const raw = getValueByKey(KEY_ALIASES, input);
+	if (raw == null) return [input];
+	if (typeof raw === 'string') return [trimLower(raw)];
+	return raw.map(trimLower);
+};
+
+const getValueByKey = <
+	T extends Record<keyof any, unknown>,
+	K extends keyof T | keyof any,
+	R extends K extends keyof T ? T[K] : T[keyof T] | undefined,
+>(obj: T, key: K) => {
+	return obj[key] as R;
+};
+
+const trimLower = (str: string) => str.trim().toLowerCase();
+//#endregion
diff --git a/packages/frontend/src/utility/idb-proxy.ts b/packages/frontend/src/utility/idb-proxy.ts
new file mode 100644
index 0000000000..20f51660c7
--- /dev/null
+++ b/packages/frontend/src/utility/idb-proxy.ts
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、
+// indexedDBが使えない環境ではlocalStorageを使う
+import {
+	get as iget,
+	set as iset,
+	del as idel,
+} from 'idb-keyval';
+import { miLocalStorage } from '@/local-storage.js';
+
+const PREFIX = 'idbfallback::';
+
+let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true;
+
+// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
+// バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと
+// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-expect-error
+if (window.Cypress) {
+	idbAvailable = false;
+	console.log('Cypress detected. It will use localStorage.');
+}
+
+if (idbAvailable) {
+	await iset('idb-test', 'test')
+		.catch(err => {
+			console.error('idb error', err);
+			console.error('indexedDB is unavailable. It will use localStorage.');
+			idbAvailable = false;
+		});
+} else {
+	console.error('indexedDB is unavailable. It will use localStorage.');
+}
+
+export async function get(key: string) {
+	if (idbAvailable) return iget(key);
+	return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
+}
+
+export async function set(key: string, val: any) {
+	if (idbAvailable) return iset(key, val);
+	return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
+}
+
+export async function del(key: string) {
+	if (idbAvailable) return idel(key);
+	return miLocalStorage.removeItem(`${PREFIX}${key}`);
+}
diff --git a/packages/frontend/src/utility/idle-render.ts b/packages/frontend/src/utility/idle-render.ts
new file mode 100644
index 0000000000..6adfedcb9f
--- /dev/null
+++ b/packages/frontend/src/utility/idle-render.ts
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => {
+	const start = performance.now();
+	const timeoutId = setTimeout(() => {
+		callback({
+			didTimeout: false, // polyfill でタイムアウト発火することはない
+			timeRemaining() {
+				const diff = performance.now() - start;
+				return Math.max(0, 50 - diff); // <https://www.w3.org/TR/requestidlecallback/#idle-periods>
+			},
+		});
+	});
+	return timeoutId;
+});
+const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => {
+	clearTimeout(timeoutId);
+});
+
+class IdlingRenderScheduler {
+	#renderers: Set<FrameRequestCallback>;
+	#rafId: number;
+	#ricId: number;
+
+	constructor() {
+		this.#renderers = new Set();
+		this.#rafId = 0;
+		this.#ricId = requestIdleCallback((deadline) => this.#schedule(deadline));
+	}
+
+	#schedule(deadline: IdleDeadline): void {
+		if (deadline.timeRemaining()) {
+			this.#rafId = requestAnimationFrame((time) => {
+				for (const renderer of this.#renderers) {
+					renderer(time);
+				}
+			});
+		}
+		this.#ricId = requestIdleCallback((arg) => this.#schedule(arg));
+	}
+
+	add(renderer: FrameRequestCallback): void {
+		this.#renderers.add(renderer);
+	}
+
+	delete(renderer: FrameRequestCallback): void {
+		this.#renderers.delete(renderer);
+	}
+
+	dispose(): void {
+		this.#renderers.clear();
+		cancelAnimationFrame(this.#rafId);
+		cancelIdleCallback(this.#ricId);
+	}
+}
+
+export const defaultIdlingRenderScheduler = new IdlingRenderScheduler();
diff --git a/packages/frontend/src/utility/init-chart.ts b/packages/frontend/src/utility/init-chart.ts
new file mode 100644
index 0000000000..037b0d9567
--- /dev/null
+++ b/packages/frontend/src/utility/init-chart.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import {
+	Chart,
+	ArcElement,
+	LineElement,
+	BarElement,
+	PointElement,
+	BarController,
+	LineController,
+	DoughnutController,
+	CategoryScale,
+	LinearScale,
+	TimeScale,
+	Legend,
+	Title,
+	Tooltip,
+	SubTitle,
+	Filler,
+} from 'chart.js';
+import gradient from 'chartjs-plugin-gradient';
+import zoomPlugin from 'chartjs-plugin-zoom';
+import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
+import { store } from '@/store.js';
+import 'chartjs-adapter-date-fns';
+
+export function initChart() {
+	Chart.register(
+		ArcElement,
+		LineElement,
+		BarElement,
+		PointElement,
+		BarController,
+		LineController,
+		DoughnutController,
+		CategoryScale,
+		LinearScale,
+		TimeScale,
+		Legend,
+		Title,
+		Tooltip,
+		SubTitle,
+		Filler,
+		MatrixController, MatrixElement,
+		zoomPlugin,
+		gradient,
+	);
+
+	// フォントカラー
+	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
+
+	Chart.defaults.borderColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+
+	Chart.defaults.animation = false;
+}
diff --git a/packages/frontend/src/utility/initialize-sw.ts b/packages/frontend/src/utility/initialize-sw.ts
new file mode 100644
index 0000000000..867ebf19ed
--- /dev/null
+++ b/packages/frontend/src/utility/initialize-sw.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { lang } from '@@/js/config.js';
+
+export async function initializeSw() {
+	if (!('serviceWorker' in navigator)) return;
+
+	navigator.serviceWorker.register('/sw.js', { scope: '/', type: 'classic' });
+	navigator.serviceWorker.ready.then(registration => {
+		registration.active?.postMessage({
+			msg: 'initialize',
+			lang,
+		});
+	});
+}
diff --git a/packages/frontend/src/utility/intl-const.ts b/packages/frontend/src/utility/intl-const.ts
new file mode 100644
index 0000000000..385f59ec39
--- /dev/null
+++ b/packages/frontend/src/utility/intl-const.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { lang } from '@@/js/config.js';
+
+export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
+
+let _dateTimeFormat: Intl.DateTimeFormat;
+try {
+	_dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
+		year: 'numeric',
+		month: 'numeric',
+		day: 'numeric',
+		hour: 'numeric',
+		minute: 'numeric',
+		second: 'numeric',
+	});
+} catch (err) {
+	console.warn(err);
+	if (_DEV_) console.log('[Intl] Fallback to en-US');
+
+	// Fallback to en-US
+	_dateTimeFormat = new Intl.DateTimeFormat('en-US', {
+		year: 'numeric',
+		month: 'numeric',
+		day: 'numeric',
+		hour: 'numeric',
+		minute: 'numeric',
+		second: 'numeric',
+	});
+}
+export const dateTimeFormat = _dateTimeFormat;
+
+export const timeZone = dateTimeFormat.resolvedOptions().timeZone;
+
+export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N';
+
+let _numberFormat: Intl.NumberFormat;
+try {
+	_numberFormat = new Intl.NumberFormat(versatileLang);
+} catch (err) {
+	console.warn(err);
+	if (_DEV_) console.log('[Intl] Fallback to en-US');
+
+	// Fallback to en-US
+	_numberFormat = new Intl.NumberFormat('en-US');
+}
+export const numberFormat = _numberFormat;
diff --git a/packages/frontend/src/utility/intl-string.ts b/packages/frontend/src/utility/intl-string.ts
new file mode 100644
index 0000000000..a5b5bbb592
--- /dev/null
+++ b/packages/frontend/src/utility/intl-string.ts
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { versatileLang } from '@@/js/intl-const.js';
+import type { toHiragana as toHiraganaType } from 'wanakana';
+
+let toHiragana: typeof toHiraganaType = (str?: string) => str ?? '';
+let isWanakanaLoaded = false;
+
+/** 
+ * ローマ字変換のセットアップ（日本語以外の環境で読み込まないのでlazy-loading）
+ * 
+ * ここの比較系関数を使う際は事前に呼び出す必要がある
+ */
+export async function initIntlString(forceWanakana = false) {
+	if ((!versatileLang.includes('ja') && !forceWanakana) || isWanakanaLoaded) return;
+	const { toHiragana: _toHiragana } = await import('wanakana');
+	toHiragana = _toHiragana;
+	isWanakanaLoaded = true;
+}
+
+/**
+ * - 全角英数字を半角に
+ * - 半角カタカナを全角に
+ * - 濁点・半濁点がリガチャになっている（例: `か` ＋ `゛` ）ひらがな・カタカナを結合
+ * - 異体字を正規化
+ * - 小文字に揃える
+ * - 文字列のトリム
+ */
+export function normalizeString(str: string) {
+	const segmenter = new Intl.Segmenter(versatileLang, { granularity: 'grapheme' });
+	return [...segmenter.segment(str)].map(({ segment }) => segment.normalize('NFKC')).join('').toLowerCase().trim();
+}
+
+// https://qiita.com/non-caffeine/items/77360dda05c8ce510084
+const hyphens = [
+	0x002d, // hyphen-minus
+	0x02d7, // modifier letter minus sign
+	0x1173, // hangul jongseong eu
+	0x1680, // ogham space mark
+	0x1b78, // balinese musical symbol left-hand open pang
+	0x2010, // hyphen
+	0x2011, // non-breaking hyphen
+	0x2012, // figure dash
+	0x2013, // en dash
+	0x2014, // em dash
+	0x2015, // horizontal bar
+	0x2043, // hyphen bullet
+	0x207b, // superscript minus
+	0x2212, // minus sign
+	0x25ac, // black rectangle
+	0x2500, // box drawings light horizontal
+	0x2501, // box drawings heavy horizontal
+	0x2796, // heavy minus sign
+	0x30fc, // katakana-hiragana prolonged sound mark
+	0x3161, // hangul letter eu
+	0xfe58, // small em dash
+	0xfe63, // small hyphen-minus
+	0xff0d, // fullwidth hyphen-minus
+	0xff70, // halfwidth katakana-hiragana prolonged sound mark
+	0x10110, // aegean number ten
+	0x10191, // roman uncia sign
+];
+
+const hyphensCodePoints = hyphens.map(code => `\\u{${code.toString(16).padStart(4, '0')}}`);
+
+/** ハイフンを統一（ローマ字半角入力時に`ー`と`-`が判定できない問題の調整） */
+export function normalizeHyphens(str: string) {
+	return str.replace(new RegExp(`[${hyphensCodePoints.join('')}]`, 'ug'), '\u002d');
+}
+
+/**
+ * `normalizeString` に加えて、カタカナ・ローマ字をひらがなに揃え、ハイフンを統一
+ *
+ * （ローマ字じゃないものもローマ字として認識され変換されるので、文字列比較の際は `normalizeString` を併用する必要あり）
+ */
+export function normalizeStringWithHiragana(str: string) {
+	return normalizeHyphens(toHiragana(normalizeString(str), { convertLongVowelMark: false }));
+}
+
+/** aとbが同じかどうか */
+export function compareStringEquals(a: string, b: string) {
+	return (
+		normalizeString(a) === normalizeString(b) ||
+		normalizeStringWithHiragana(a) === normalizeStringWithHiragana(b)
+	);
+}
+
+/** baseにqueryが含まれているかどうか */
+export function compareStringIncludes(base: string, query: string) {
+	return (
+		normalizeString(base).includes(normalizeString(query)) ||
+		normalizeStringWithHiragana(base).includes(normalizeStringWithHiragana(query))
+	);
+}
diff --git a/packages/frontend/src/utility/is-device-darkmode.ts b/packages/frontend/src/utility/is-device-darkmode.ts
new file mode 100644
index 0000000000..4f487c7cb9
--- /dev/null
+++ b/packages/frontend/src/utility/is-device-darkmode.ts
@@ -0,0 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function isDeviceDarkmode() {
+	return window.matchMedia('(prefers-color-scheme: dark)').matches;
+}
diff --git a/packages/frontend/src/utility/isFfVisibleForMe.ts b/packages/frontend/src/utility/isFfVisibleForMe.ts
new file mode 100644
index 0000000000..e28e5725bc
--- /dev/null
+++ b/packages/frontend/src/utility/isFfVisibleForMe.ts
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { $i } from '@/account.js';
+
+export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
+	if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
+
+	if (user.followingVisibility === 'private') return false;
+	if (user.followingVisibility === 'followers' && !user.isFollowing) return false;
+
+	return true;
+}
+export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
+	if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
+
+	if (user.followersVisibility === 'private') return false;
+	if (user.followersVisibility === 'followers' && !user.isFollowing) return false;
+
+	return true;
+}
diff --git a/packages/frontend/src/utility/key-event.ts b/packages/frontend/src/utility/key-event.ts
new file mode 100644
index 0000000000..020a6c2174
--- /dev/null
+++ b/packages/frontend/src/utility/key-event.ts
@@ -0,0 +1,153 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * {@link KeyboardEvent.code} の値を表す文字列。不足分は適宜追加する
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
+ */
+export type KeyCode = (
+	| 'Backspace'
+	| 'Tab'
+	| 'Enter'
+	| 'Shift'
+	| 'Control'
+	| 'Alt'
+	| 'Pause'
+	| 'CapsLock'
+	| 'Escape'
+	| 'Space'
+	| 'PageUp'
+	| 'PageDown'
+	| 'End'
+	| 'Home'
+	| 'ArrowLeft'
+	| 'ArrowUp'
+	| 'ArrowRight'
+	| 'ArrowDown'
+	| 'Insert'
+	| 'Delete'
+	| 'Digit0'
+	| 'Digit1'
+	| 'Digit2'
+	| 'Digit3'
+	| 'Digit4'
+	| 'Digit5'
+	| 'Digit6'
+	| 'Digit7'
+	| 'Digit8'
+	| 'Digit9'
+	| 'KeyA'
+	| 'KeyB'
+	| 'KeyC'
+	| 'KeyD'
+	| 'KeyE'
+	| 'KeyF'
+	| 'KeyG'
+	| 'KeyH'
+	| 'KeyI'
+	| 'KeyJ'
+	| 'KeyK'
+	| 'KeyL'
+	| 'KeyM'
+	| 'KeyN'
+	| 'KeyO'
+	| 'KeyP'
+	| 'KeyQ'
+	| 'KeyR'
+	| 'KeyS'
+	| 'KeyT'
+	| 'KeyU'
+	| 'KeyV'
+	| 'KeyW'
+	| 'KeyX'
+	| 'KeyY'
+	| 'KeyZ'
+	| 'MetaLeft'
+	| 'MetaRight'
+	| 'ContextMenu'
+	| 'F1'
+	| 'F2'
+	| 'F3'
+	| 'F4'
+	| 'F5'
+	| 'F6'
+	| 'F7'
+	| 'F8'
+	| 'F9'
+	| 'F10'
+	| 'F11'
+	| 'F12'
+	| 'NumLock'
+	| 'ScrollLock'
+	| 'Semicolon'
+	| 'Equal'
+	| 'Comma'
+	| 'Minus'
+	| 'Period'
+	| 'Slash'
+	| 'Backquote'
+	| 'BracketLeft'
+	| 'Backslash'
+	| 'BracketRight'
+	| 'Quote'
+	| 'Meta'
+	| 'AltGraph'
+);
+
+/**
+ * 修飾キーを表す文字列。不足分は適宜追加する。
+ */
+export type KeyModifier = (
+	| 'Shift'
+	| 'Control'
+	| 'Alt'
+	| 'Meta'
+);
+
+/**
+ * 押下されたキー以外の状態を表す文字列。不足分は適宜追加する。
+ */
+export type KeyState = (
+	| 'composing'
+	| 'repeat'
+);
+
+export type KeyEventHandler = {
+	modifiers?: KeyModifier[];
+	states?: KeyState[];
+	code: KeyCode | 'any';
+	handler: (event: KeyboardEvent) => void;
+};
+
+export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) {
+	function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) {
+		if (modifiers) {
+			return modifiers.every(modifier => ev.getModifierState(modifier));
+		}
+		return true;
+	}
+
+	function checkState(ev: KeyboardEvent, states?: KeyState[]) {
+		if (states) {
+			return states.every(state => ev.getModifierState(state));
+		}
+		return true;
+	}
+
+	let hit = false;
+	for (const handler of handlers.filter(it => it.code === event.code)) {
+		if (checkModifier(event, handler.modifiers) && checkState(event, handler.states)) {
+			handler.handler(event);
+			hit = true;
+			break;
+		}
+	}
+
+	if (!hit) {
+		for (const handler of handlers.filter(it => it.code === 'any')) {
+			handler.handler(event);
+		}
+	}
+}
diff --git a/packages/frontend/src/utility/langmap.ts b/packages/frontend/src/utility/langmap.ts
new file mode 100644
index 0000000000..b32de15963
--- /dev/null
+++ b/packages/frontend/src/utility/langmap.ts
@@ -0,0 +1,671 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// TODO: sharedに置いてバックエンドのと統合したい
+export const langmap = {
+	'ach': {
+		nativeName: 'Lwo',
+	},
+	'ady': {
+		nativeName: 'Адыгэбзэ',
+	},
+	'af': {
+		nativeName: 'Afrikaans',
+	},
+	'af-NA': {
+		nativeName: 'Afrikaans (Namibia)',
+	},
+	'af-ZA': {
+		nativeName: 'Afrikaans (South Africa)',
+	},
+	'ak': {
+		nativeName: 'Tɕɥi',
+	},
+	'ar': {
+		nativeName: 'العربية',
+	},
+	'ar-AR': {
+		nativeName: 'العربية',
+	},
+	'ar-MA': {
+		nativeName: 'العربية',
+	},
+	'ar-SA': {
+		nativeName: 'العربية (السعودية)',
+	},
+	'ay-BO': {
+		nativeName: 'Aymar aru',
+	},
+	'az': {
+		nativeName: 'Azərbaycan dili',
+	},
+	'az-AZ': {
+		nativeName: 'Azərbaycan dili',
+	},
+	'be-BY': {
+		nativeName: 'Беларуская',
+	},
+	'bg': {
+		nativeName: 'Български',
+	},
+	'bg-BG': {
+		nativeName: 'Български',
+	},
+	'bn': {
+		nativeName: 'বাংলা',
+	},
+	'bn-IN': {
+		nativeName: 'বাংলা (ভারত)',
+	},
+	'bn-BD': {
+		nativeName: 'বাংলা(বাংলাদেশ)',
+	},
+	'br': {
+		nativeName: 'Brezhoneg',
+	},
+	'bs-BA': {
+		nativeName: 'Bosanski',
+	},
+	'ca': {
+		nativeName: 'Català',
+	},
+	'ca-ES': {
+		nativeName: 'Català',
+	},
+	'cak': {
+		nativeName: 'Maya Kaqchikel',
+	},
+	'ck-US': {
+		nativeName: 'ᏣᎳᎩ (tsalagi)',
+	},
+	'cs': {
+		nativeName: 'Čeština',
+	},
+	'cs-CZ': {
+		nativeName: 'Čeština',
+	},
+	'cy': {
+		nativeName: 'Cymraeg',
+	},
+	'cy-GB': {
+		nativeName: 'Cymraeg',
+	},
+	'da': {
+		nativeName: 'Dansk',
+	},
+	'da-DK': {
+		nativeName: 'Dansk',
+	},
+	'de': {
+		nativeName: 'Deutsch',
+	},
+	'de-AT': {
+		nativeName: 'Deutsch (Österreich)',
+	},
+	'de-DE': {
+		nativeName: 'Deutsch (Deutschland)',
+	},
+	'de-CH': {
+		nativeName: 'Deutsch (Schweiz)',
+	},
+	'dsb': {
+		nativeName: 'Dolnoserbšćina',
+	},
+	'el': {
+		nativeName: 'Ελληνικά',
+	},
+	'el-GR': {
+		nativeName: 'Ελληνικά',
+	},
+	'en': {
+		nativeName: 'English',
+	},
+	'en-GB': {
+		nativeName: 'English (UK)',
+	},
+	'en-AU': {
+		nativeName: 'English (Australia)',
+	},
+	'en-CA': {
+		nativeName: 'English (Canada)',
+	},
+	'en-IE': {
+		nativeName: 'English (Ireland)',
+	},
+	'en-IN': {
+		nativeName: 'English (India)',
+	},
+	'en-PI': {
+		nativeName: 'English (Pirate)',
+	},
+	'en-SG': {
+		nativeName: 'English (Singapore)',
+	},
+	'en-UD': {
+		nativeName: 'English (Upside Down)',
+	},
+	'en-US': {
+		nativeName: 'English (US)',
+	},
+	'en-ZA': {
+		nativeName: 'English (South Africa)',
+	},
+	'en@pirate': {
+		nativeName: 'English (Pirate)',
+	},
+	'eo': {
+		nativeName: 'Esperanto',
+	},
+	'eo-EO': {
+		nativeName: 'Esperanto',
+	},
+	'es': {
+		nativeName: 'Español',
+	},
+	'es-AR': {
+		nativeName: 'Español (Argentine)',
+	},
+	'es-419': {
+		nativeName: 'Español (Latinoamérica)',
+	},
+	'es-CL': {
+		nativeName: 'Español (Chile)',
+	},
+	'es-CO': {
+		nativeName: 'Español (Colombia)',
+	},
+	'es-EC': {
+		nativeName: 'Español (Ecuador)',
+	},
+	'es-ES': {
+		nativeName: 'Español (España)',
+	},
+	'es-LA': {
+		nativeName: 'Español (Latinoamérica)',
+	},
+	'es-NI': {
+		nativeName: 'Español (Nicaragua)',
+	},
+	'es-MX': {
+		nativeName: 'Español (México)',
+	},
+	'es-US': {
+		nativeName: 'Español (Estados Unidos)',
+	},
+	'es-VE': {
+		nativeName: 'Español (Venezuela)',
+	},
+	'et': {
+		nativeName: 'eesti keel',
+	},
+	'et-EE': {
+		nativeName: 'Eesti (Estonia)',
+	},
+	'eu': {
+		nativeName: 'Euskara',
+	},
+	'eu-ES': {
+		nativeName: 'Euskara',
+	},
+	'fa': {
+		nativeName: 'فارسی',
+	},
+	'fa-IR': {
+		nativeName: 'فارسی',
+	},
+	'fb-LT': {
+		nativeName: 'Leet Speak',
+	},
+	'ff': {
+		nativeName: 'Fulah',
+	},
+	'fi': {
+		nativeName: 'Suomi',
+	},
+	'fi-FI': {
+		nativeName: 'Suomi',
+	},
+	'fo': {
+		nativeName: 'Føroyskt',
+	},
+	'fo-FO': {
+		nativeName: 'Føroyskt (Færeyjar)',
+	},
+	'fr': {
+		nativeName: 'Français',
+	},
+	'fr-CA': {
+		nativeName: 'Français (Canada)',
+	},
+	'fr-FR': {
+		nativeName: 'Français (France)',
+	},
+	'fr-BE': {
+		nativeName: 'Français (Belgique)',
+	},
+	'fr-CH': {
+		nativeName: 'Français (Suisse)',
+	},
+	'fy-NL': {
+		nativeName: 'Frysk',
+	},
+	'ga': {
+		nativeName: 'Gaeilge',
+	},
+	'ga-IE': {
+		nativeName: 'Gaeilge',
+	},
+	'gd': {
+		nativeName: 'Gàidhlig',
+	},
+	'gl': {
+		nativeName: 'Galego',
+	},
+	'gl-ES': {
+		nativeName: 'Galego',
+	},
+	'gn-PY': {
+		nativeName: 'Avañe\'ẽ',
+	},
+	'gu-IN': {
+		nativeName: 'ગુજરાતી',
+	},
+	'gv': {
+		nativeName: 'Gaelg',
+	},
+	'gx-GR': {
+		nativeName: 'Ἑλληνική ἀρχαία',
+	},
+	'he': {
+		nativeName: 'עברית‏',
+	},
+	'he-IL': {
+		nativeName: 'עברית‏',
+	},
+	'hi': {
+		nativeName: 'हिन्दी',
+	},
+	'hi-IN': {
+		nativeName: 'हिन्दी',
+	},
+	'hr': {
+		nativeName: 'Hrvatski',
+	},
+	'hr-HR': {
+		nativeName: 'Hrvatski',
+	},
+	'hsb': {
+		nativeName: 'Hornjoserbšćina',
+	},
+	'ht': {
+		nativeName: 'Kreyòl',
+	},
+	'hu': {
+		nativeName: 'Magyar',
+	},
+	'hu-HU': {
+		nativeName: 'Magyar',
+	},
+	'hy': {
+		nativeName: 'Հայերեն',
+	},
+	'hy-AM': {
+		nativeName: 'Հայերեն (Հայաստան)',
+	},
+	'id': {
+		nativeName: 'Bahasa Indonesia',
+	},
+	'id-ID': {
+		nativeName: 'Bahasa Indonesia',
+	},
+	'is': {
+		nativeName: 'Íslenska',
+	},
+	'is-IS': {
+		nativeName: 'Íslenska (Iceland)',
+	},
+	'it': {
+		nativeName: 'Italiano',
+	},
+	'it-IT': {
+		nativeName: 'Italiano',
+	},
+	'ja': {
+		nativeName: '日本語',
+	},
+	'ja-JP': {
+		nativeName: '日本語 (日本)',
+	},
+	'jv-ID': {
+		nativeName: 'Basa Jawa',
+	},
+	'ka-GE': {
+		nativeName: 'ქართული',
+	},
+	'kk-KZ': {
+		nativeName: 'Қазақша',
+	},
+	'km': {
+		nativeName: 'ភាសាខ្មែរ',
+	},
+	'kl': {
+		nativeName: 'kalaallisut',
+	},
+	'km-KH': {
+		nativeName: 'ភាសាខ្មែរ',
+	},
+	'kab': {
+		nativeName: 'Taqbaylit',
+	},
+	'kn': {
+		nativeName: 'ಕನ್ನಡ',
+	},
+	'kn-IN': {
+		nativeName: 'ಕನ್ನಡ (India)',
+	},
+	'ko': {
+		nativeName: '한국어',
+	},
+	'ko-KR': {
+		nativeName: '한국어 (한국)',
+	},
+	'ku-TR': {
+		nativeName: 'Kurdî',
+	},
+	'kw': {
+		nativeName: 'Kernewek',
+	},
+	'la': {
+		nativeName: 'Latin',
+	},
+	'la-VA': {
+		nativeName: 'Latin',
+	},
+	'lb': {
+		nativeName: 'Lëtzebuergesch',
+	},
+	'li-NL': {
+		nativeName: 'Lèmbörgs',
+	},
+	'lt': {
+		nativeName: 'Lietuvių',
+	},
+	'lt-LT': {
+		nativeName: 'Lietuvių',
+	},
+	'lv': {
+		nativeName: 'Latviešu',
+	},
+	'lv-LV': {
+		nativeName: 'Latviešu',
+	},
+	'mai': {
+		nativeName: 'मैथिली, মৈথিলী',
+	},
+	'mg-MG': {
+		nativeName: 'Malagasy',
+	},
+	'mk': {
+		nativeName: 'Македонски',
+	},
+	'mk-MK': {
+		nativeName: 'Македонски (Македонски)',
+	},
+	'ml': {
+		nativeName: 'മലയാളം',
+	},
+	'ml-IN': {
+		nativeName: 'മലയാളം',
+	},
+	'mn-MN': {
+		nativeName: 'Монгол',
+	},
+	'mr': {
+		nativeName: 'मराठी',
+	},
+	'mr-IN': {
+		nativeName: 'मराठी',
+	},
+	'ms': {
+		nativeName: 'Bahasa Melayu',
+	},
+	'ms-MY': {
+		nativeName: 'Bahasa Melayu',
+	},
+	'mt': {
+		nativeName: 'Malti',
+	},
+	'mt-MT': {
+		nativeName: 'Malti',
+	},
+	'my': {
+		nativeName: 'ဗမာစကာ',
+	},
+	'no': {
+		nativeName: 'Norsk',
+	},
+	'nb': {
+		nativeName: 'Norsk (bokmål)',
+	},
+	'nb-NO': {
+		nativeName: 'Norsk (bokmål)',
+	},
+	'ne': {
+		nativeName: 'नेपाली',
+	},
+	'ne-NP': {
+		nativeName: 'नेपाली',
+	},
+	'nl': {
+		nativeName: 'Nederlands',
+	},
+	'nl-BE': {
+		nativeName: 'Nederlands (België)',
+	},
+	'nl-NL': {
+		nativeName: 'Nederlands (Nederland)',
+	},
+	'nn-NO': {
+		nativeName: 'Norsk (nynorsk)',
+	},
+	'oc': {
+		nativeName: 'Occitan',
+	},
+	'or-IN': {
+		nativeName: 'ଓଡ଼ିଆ',
+	},
+	'pa': {
+		nativeName: 'ਪੰਜਾਬੀ',
+	},
+	'pa-IN': {
+		nativeName: 'ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)',
+	},
+	'pl': {
+		nativeName: 'Polski',
+	},
+	'pl-PL': {
+		nativeName: 'Polski',
+	},
+	'ps-AF': {
+		nativeName: 'پښتو',
+	},
+	'pt': {
+		nativeName: 'Português',
+	},
+	'pt-BR': {
+		nativeName: 'Português (Brasil)',
+	},
+	'pt-PT': {
+		nativeName: 'Português (Portugal)',
+	},
+	'qu-PE': {
+		nativeName: 'Qhichwa',
+	},
+	'rm-CH': {
+		nativeName: 'Rumantsch',
+	},
+	'ro': {
+		nativeName: 'Română',
+	},
+	'ro-RO': {
+		nativeName: 'Română',
+	},
+	'ru': {
+		nativeName: 'Русский',
+	},
+	'ru-RU': {
+		nativeName: 'Русский',
+	},
+	'sa-IN': {
+		nativeName: 'संस्कृतम्',
+	},
+	'se-NO': {
+		nativeName: 'Davvisámegiella',
+	},
+	'sh': {
+		nativeName: 'српскохрватски',
+	},
+	'si-LK': {
+		nativeName: 'සිංහල',
+	},
+	'sk': {
+		nativeName: 'Slovenčina',
+	},
+	'sk-SK': {
+		nativeName: 'Slovenčina (Slovakia)',
+	},
+	'sl': {
+		nativeName: 'Slovenščina',
+	},
+	'sl-SI': {
+		nativeName: 'Slovenščina',
+	},
+	'so-SO': {
+		nativeName: 'Soomaaliga',
+	},
+	'sq': {
+		nativeName: 'Shqip',
+	},
+	'sq-AL': {
+		nativeName: 'Shqip',
+	},
+	'sr': {
+		nativeName: 'Српски',
+	},
+	'sr-RS': {
+		nativeName: 'Српски (Serbia)',
+	},
+	'su': {
+		nativeName: 'Basa Sunda',
+	},
+	'sv': {
+		nativeName: 'Svenska',
+	},
+	'sv-SE': {
+		nativeName: 'Svenska',
+	},
+	'sw': {
+		nativeName: 'Kiswahili',
+	},
+	'sw-KE': {
+		nativeName: 'Kiswahili',
+	},
+	'ta': {
+		nativeName: 'தமிழ்',
+	},
+	'ta-IN': {
+		nativeName: 'தமிழ்',
+	},
+	'te': {
+		nativeName: 'తెలుగు',
+	},
+	'te-IN': {
+		nativeName: 'తెలుగు',
+	},
+	'tg': {
+		nativeName: 'забо́ни тоҷикӣ́',
+	},
+	'tg-TJ': {
+		nativeName: 'тоҷикӣ',
+	},
+	'th': {
+		nativeName: 'ภาษาไทย',
+	},
+	'th-TH': {
+		nativeName: 'ภาษาไทย (ประเทศไทย)',
+	},
+	'fil': {
+		nativeName: 'Filipino',
+	},
+	'tlh': {
+		nativeName: 'tlhIngan-Hol',
+	},
+	'tr': {
+		nativeName: 'Türkçe',
+	},
+	'tr-TR': {
+		nativeName: 'Türkçe',
+	},
+	'tt-RU': {
+		nativeName: 'татарча',
+	},
+	'uk': {
+		nativeName: 'Українська',
+	},
+	'uk-UA': {
+		nativeName: 'Українська',
+	},
+	'ur': {
+		nativeName: 'اردو',
+	},
+	'ur-PK': {
+		nativeName: 'اردو',
+	},
+	'uz': {
+		nativeName: 'O\'zbek',
+	},
+	'uz-UZ': {
+		nativeName: 'O\'zbek',
+	},
+	'vi': {
+		nativeName: 'Tiếng Việt',
+	},
+	'vi-VN': {
+		nativeName: 'Tiếng Việt',
+	},
+	'xh-ZA': {
+		nativeName: 'isiXhosa',
+	},
+	'yi': {
+		nativeName: 'ייִדיש',
+	},
+	'yi-DE': {
+		nativeName: 'ייִדיש (German)',
+	},
+	'zh': {
+		nativeName: '中文',
+	},
+	'zh-Hans': {
+		nativeName: '中文简体',
+	},
+	'zh-Hant': {
+		nativeName: '中文繁體',
+	},
+	'zh-CN': {
+		nativeName: '中文（中国大陆）',
+	},
+	'zh-HK': {
+		nativeName: '中文（香港）',
+	},
+	'zh-SG': {
+		nativeName: '中文（新加坡）',
+	},
+	'zh-TW': {
+		nativeName: '中文（台灣）',
+	},
+	'zu-ZA': {
+		nativeName: 'isiZulu',
+	},
+};
diff --git a/packages/frontend/src/utility/login-id.ts b/packages/frontend/src/utility/login-id.ts
new file mode 100644
index 0000000000..b52735caa0
--- /dev/null
+++ b/packages/frontend/src/utility/login-id.ts
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function getUrlWithLoginId(url: string, loginId: string) {
+	const u = new URL(url, origin);
+	u.searchParams.append('loginId', loginId);
+	return u.toString();
+}
+
+export function getUrlWithoutLoginId(url: string) {
+	const u = new URL(url);
+	u.searchParams.delete('loginId');
+	return u.toString();
+}
diff --git a/packages/frontend/src/utility/lookup.ts b/packages/frontend/src/utility/lookup.ts
new file mode 100644
index 0000000000..d3a2d854a0
--- /dev/null
+++ b/packages/frontend/src/utility/lookup.ts
@@ -0,0 +1,84 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { Router } from '@/nirax.js';
+import { mainRouter } from '@/router/main.js';
+
+export async function lookup(router?: Router) {
+	const _router = router ?? mainRouter;
+
+	const { canceled, result: temp } = await os.inputText({
+		title: i18n.ts.lookup,
+	});
+	const query = temp ? temp.trim() : '';
+	if (canceled || query.length <= 1) return;
+
+	if (query.startsWith('@') && !query.includes(' ')) {
+		_router.push(`/${query}`);
+		return;
+	}
+
+	if (query.startsWith('#')) {
+		_router.push(`/tags/${encodeURIComponent(query.substring(1))}`);
+		return;
+	}
+
+	if (query.startsWith('https://')) {
+		const res = await apLookup(query);
+
+		if (res.type === 'User') {
+			_router.push(`/@${res.object.username}@${res.object.host}`);
+		} else if (res.type === 'Note') {
+			_router.push(`/notes/${res.object.id}`);
+		}
+
+		return;
+	}
+}
+
+export async function apLookup(query: string) {
+	const promise = misskeyApi('ap/show', {
+		uri: query,
+	});
+
+	os.promiseDialog(promise, null, (err) => {
+		let title = i18n.ts.somethingHappened;
+		let text = err.message + '\n' + err.id;
+
+		switch (err.id) {
+			case '974b799e-1a29-4889-b706-18d4dd93e266':
+				title = i18n.ts._remoteLookupErrors._federationNotAllowed.title;
+				text = i18n.ts._remoteLookupErrors._federationNotAllowed.description;
+				break;
+			case '1a5eab56-e47b-48c2-8d5e-217b897d70db':
+				title = i18n.ts._remoteLookupErrors._uriInvalid.title;
+				text = i18n.ts._remoteLookupErrors._uriInvalid.description;
+				break;
+			case '81b539cf-4f57-4b29-bc98-032c33c0792e':
+				title = i18n.ts._remoteLookupErrors._requestFailed.title;
+				text = i18n.ts._remoteLookupErrors._requestFailed.description;
+				break;
+			case '70193c39-54f3-4813-82f0-70a680f7495b':
+				title = i18n.ts._remoteLookupErrors._responseInvalid.title;
+				text = i18n.ts._remoteLookupErrors._responseInvalid.description;
+				break;
+			case 'dc94d745-1262-4e63-a17d-fecaa57efc82':
+				title = i18n.ts._remoteLookupErrors._noSuchObject.title;
+				text = i18n.ts._remoteLookupErrors._noSuchObject.description;
+				break;
+		}
+
+		os.alert({
+			type: 'error',
+			title,
+			text,
+		});
+	}, i18n.ts.fetchingAsApObject);
+
+	return await promise;
+}
diff --git a/packages/frontend/src/utility/media-has-audio.ts b/packages/frontend/src/utility/media-has-audio.ts
new file mode 100644
index 0000000000..4bf3ee5d97
--- /dev/null
+++ b/packages/frontend/src/utility/media-has-audio.ts
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export default async function hasAudio(media: HTMLMediaElement) {
+	const cloned = media.cloneNode() as HTMLMediaElement;
+	cloned.muted = (cloned as typeof cloned & Partial<HTMLVideoElement>).playsInline = true;
+	cloned.play();
+	await new Promise((resolve) => cloned.addEventListener('playing', resolve));
+	const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount;
+	cloned.remove();
+	return result;
+}
diff --git a/packages/frontend/src/utility/media-proxy.ts b/packages/frontend/src/utility/media-proxy.ts
new file mode 100644
index 0000000000..78eba35ead
--- /dev/null
+++ b/packages/frontend/src/utility/media-proxy.ts
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { MediaProxy } from '@@/js/media-proxy.js';
+import { url } from '@@/js/config.js';
+import { instance } from '@/instance.js';
+
+let _mediaProxy: MediaProxy | null = null;
+
+export function getProxiedImageUrl(...args: Parameters<MediaProxy['getProxiedImageUrl']>): string {
+	if (_mediaProxy == null) {
+		_mediaProxy = new MediaProxy(instance, url);
+	}
+
+	return _mediaProxy.getProxiedImageUrl(...args);
+}
+
+export function getProxiedImageUrlNullable(...args: Parameters<MediaProxy['getProxiedImageUrlNullable']>): string | null {
+	if (_mediaProxy == null) {
+		_mediaProxy = new MediaProxy(instance, url);
+	}
+
+	return _mediaProxy.getProxiedImageUrlNullable(...args);
+}
+
+export function getStaticImageUrl(...args: Parameters<MediaProxy['getStaticImageUrl']>): string {
+	if (_mediaProxy == null) {
+		_mediaProxy = new MediaProxy(instance, url);
+	}
+
+	return _mediaProxy.getStaticImageUrl(...args);
+}
diff --git a/packages/frontend/src/utility/merge.ts b/packages/frontend/src/utility/merge.ts
new file mode 100644
index 0000000000..004b6d42a4
--- /dev/null
+++ b/packages/frontend/src/utility/merge.ts
@@ -0,0 +1,35 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { deepClone } from './clone.js';
+import type { Cloneable } from './clone.js';
+
+export type DeepPartial<T> = {
+	[P in keyof T]?: T[P] extends Record<PropertyKey, unknown> ? DeepPartial<T[P]> : T[P];
+};
+
+function isPureObject(value: unknown): value is Record<PropertyKey, unknown> {
+	return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+/**
+ * valueにないキーをdefからもらう（再帰的）\
+ * nullはそのまま、undefinedはdefの値
+ **/
+export function deepMerge<X extends Record<PropertyKey, unknown>>(value: DeepPartial<X>, def: X): X {
+	if (isPureObject(value) && isPureObject(def)) {
+		const result = deepClone(value as Cloneable) as X;
+		for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
+			if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
+				result[k] = v;
+			} else if (isPureObject(v) && isPureObject(result[k])) {
+				const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<PropertyKey, unknown>>;
+				result[k] = deepMerge<typeof v>(child, v);
+			}
+		}
+		return result;
+	}
+	throw new Error('deepMerge: value and def must be pure objects');
+}
diff --git a/packages/frontend/src/utility/mfm-function-picker.ts b/packages/frontend/src/utility/mfm-function-picker.ts
new file mode 100644
index 0000000000..a2f777f623
--- /dev/null
+++ b/packages/frontend/src/utility/mfm-function-picker.ts
@@ -0,0 +1,57 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { nextTick } from 'vue';
+import type { Ref } from 'vue';
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { MFM_TAGS } from '@@/js/const.js';
+import type { MenuItem } from '@/types/menu.js';
+
+/**
+ * MFMの装飾のリストを表示する
+ */
+export function mfmFunctionPicker(src: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
+	os.popupMenu([{
+		text: i18n.ts.addMfmFunction,
+		type: 'label',
+	}, ...getFunctionList(textArea, textRef)], src);
+}
+
+function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>): MenuItem[] {
+	return MFM_TAGS.map(tag => ({
+		text: tag,
+		icon: 'ti ti-icons',
+		action: () => add(textArea, textRef, tag),
+	}));
+}
+
+function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, type: string) {
+	const caretStart: number = textArea.selectionStart as number;
+	const caretEnd: number = textArea.selectionEnd as number;
+
+	MFM_TAGS.forEach(tag => {
+		if (type === tag) {
+			if (caretStart === caretEnd) {
+				// 単純にFunctionを追加
+				const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ]${textRef.value.substring(caretEnd)}`;
+				textRef.value = trimmedText;
+			} else {
+				// 選択範囲を囲むようにFunctionを追加
+				const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ${textRef.value.substring(caretStart, caretEnd)}]${textRef.value.substring(caretEnd)}`;
+				textRef.value = trimmedText;
+			}
+		}
+	});
+
+	const nextCaretStart: number = caretStart + 3 + type.length;
+	const nextCaretEnd: number = caretEnd + 3 + type.length;
+
+	// キャレットを戻す
+	nextTick(() => {
+		textArea.focus();
+		textArea.setSelectionRange(nextCaretStart, nextCaretEnd);
+	});
+}
diff --git a/packages/frontend/src/utility/misskey-api.ts b/packages/frontend/src/utility/misskey-api.ts
new file mode 100644
index 0000000000..dc07ad477b
--- /dev/null
+++ b/packages/frontend/src/utility/misskey-api.ts
@@ -0,0 +1,116 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { ref } from 'vue';
+import { apiUrl } from '@@/js/config.js';
+import { $i } from '@/account.js';
+export const pendingApiRequestsCount = ref(0);
+
+export type Endpoint = keyof Misskey.Endpoints;
+
+export type Request<E extends Endpoint> = Misskey.Endpoints[E]['req'];
+
+export type AnyRequest<E extends Endpoint | (string & unknown)> =
+	(E extends Endpoint ? Request<E> : never) | object;
+
+export type Response<E extends Endpoint | (string & unknown), P extends AnyRequest<E>> =
+	E extends Endpoint
+	? P extends Request<E> ? Misskey.api.SwitchCaseResponseType<E, P> : never
+	: object;
+
+// Implements Misskey.api.ApiClient.request
+export function misskeyApi<
+	ResT = void,
+	E extends Endpoint | NonNullable<string> = Endpoint,
+	P extends AnyRequest<E> = E extends Endpoint ? Request<E> : never,
+	_ResT = ResT extends void ? Response<E, P> : ResT,
+>(
+	endpoint: E,
+	data: P & { i?: string | null; } = {} as any,
+	token?: string | null | undefined,
+	signal?: AbortSignal,
+): Promise<_ResT> {
+	if (endpoint.includes('://')) throw new Error('invalid endpoint');
+	pendingApiRequestsCount.value++;
+
+	const onFinally = () => {
+		pendingApiRequestsCount.value--;
+	};
+
+	const promise = new Promise<_ResT>((resolve, reject) => {
+		// Append a credential
+		if ($i) data.i = $i.token;
+		if (token !== undefined) data.i = token;
+
+		// Send request
+		window.fetch(`${apiUrl}/${endpoint}`, {
+			method: 'POST',
+			body: JSON.stringify(data),
+			credentials: 'omit',
+			cache: 'no-cache',
+			headers: {
+				'Content-Type': 'application/json',
+			},
+			signal,
+		}).then(async (res) => {
+			const body = res.status === 204 ? null : await res.json();
+
+			if (res.status === 200) {
+				resolve(body);
+			} else if (res.status === 204) {
+				resolve(undefined as _ResT); // void -> undefined
+			} else {
+				reject(body.error);
+			}
+		}).catch(reject);
+	});
+
+	promise.then(onFinally, onFinally);
+
+	return promise;
+}
+
+// Implements Misskey.api.ApiClient.request
+export function misskeyApiGet<
+	ResT = void,
+	E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
+	P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
+	_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
+>(
+	endpoint: E,
+	data: P = {} as any,
+): Promise<_ResT> {
+	pendingApiRequestsCount.value++;
+
+	const onFinally = () => {
+		pendingApiRequestsCount.value--;
+	};
+
+	const query = new URLSearchParams(data as any);
+
+	const promise = new Promise<_ResT>((resolve, reject) => {
+		// Send request
+		window.fetch(`${apiUrl}/${endpoint}?${query}`, {
+			method: 'GET',
+			credentials: 'omit',
+			cache: 'default',
+		}).then(async (res) => {
+			const body = res.status === 204 ? null : await res.json();
+
+			if (res.status === 200) {
+				resolve(body);
+			} else if (res.status === 204) {
+				resolve(undefined as _ResT); // void -> undefined
+			} else {
+				reject(body.error);
+			}
+		}).catch(reject);
+	});
+
+	promise.then(onFinally, onFinally);
+
+	return promise;
+}
diff --git a/packages/frontend/src/utility/navigator.ts b/packages/frontend/src/utility/navigator.ts
new file mode 100644
index 0000000000..ffc0a457f4
--- /dev/null
+++ b/packages/frontend/src/utility/navigator.ts
@@ -0,0 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function isSupportShare(): boolean {
+	return 'share' in navigator;
+}
diff --git a/packages/frontend/src/utility/page-metadata.ts b/packages/frontend/src/utility/page-metadata.ts
new file mode 100644
index 0000000000..671751147c
--- /dev/null
+++ b/packages/frontend/src/utility/page-metadata.ts
@@ -0,0 +1,71 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { inject, isRef, onActivated, onBeforeUnmount, provide, ref, toValue, watch } from 'vue';
+import type { MaybeRefOrGetter, Ref } from 'vue';
+
+export type PageMetadata = {
+	title: string;
+	subtitle?: string;
+	icon?: string | null;
+	avatar?: Misskey.entities.User | null;
+	userName?: Misskey.entities.User | null;
+	needWideArea?: boolean;
+};
+
+type PageMetadataGetter = () => PageMetadata;
+type PageMetadataReceiver = (getter: PageMetadataGetter) => void;
+
+const RECEIVER_KEY = Symbol('ReceiverKey');
+const setReceiver = (v: PageMetadataReceiver): void => {
+	provide<PageMetadataReceiver>(RECEIVER_KEY, v);
+};
+const getReceiver = (): PageMetadataReceiver | undefined => {
+	return inject<PageMetadataReceiver>(RECEIVER_KEY);
+};
+
+const METADATA_KEY = Symbol('MetadataKey');
+const setMetadata = (v: Ref<PageMetadata | null>): void => {
+	provide<Ref<PageMetadata | null>>(METADATA_KEY, v);
+};
+const getMetadata = (): Ref<PageMetadata | null> | undefined => {
+	return inject<Ref<PageMetadata | null>>(METADATA_KEY);
+};
+
+export const definePageMetadata = (maybeRefOrGetterMetadata: MaybeRefOrGetter<PageMetadata>): void => {
+	const metadataRef = ref(toValue(maybeRefOrGetterMetadata));
+	const metadataGetter = () => metadataRef.value;
+	const receiver = getReceiver();
+
+	// setup handler
+	receiver?.(metadataGetter);
+
+	// update handler
+	onBeforeUnmount(watch(
+		() => toValue(maybeRefOrGetterMetadata),
+		(metadata) => {
+			metadataRef.value = metadata;
+			receiver?.(metadataGetter);
+		},
+		{ deep: true },
+	));
+	onActivated(() => {
+		receiver?.(metadataGetter);
+	});
+};
+
+export const provideMetadataReceiver = (receiver: PageMetadataReceiver): void => {
+	setReceiver(receiver);
+};
+
+export const provideReactiveMetadata = (metadataRef: Ref<PageMetadata | null>): void => {
+	setMetadata(metadataRef);
+};
+
+export const injectReactiveMetadata = (): Ref<PageMetadata | null> => {
+	const metadataRef = getMetadata();
+	return isRef(metadataRef) ? metadataRef : ref(null);
+};
diff --git a/packages/frontend/src/utility/physics.ts b/packages/frontend/src/utility/physics.ts
new file mode 100644
index 0000000000..8a4e9319b3
--- /dev/null
+++ b/packages/frontend/src/utility/physics.ts
@@ -0,0 +1,157 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Matter from 'matter-js';
+
+export function physics(container: HTMLElement) {
+	const containerWidth = container.offsetWidth;
+	const containerHeight = container.offsetHeight;
+	const containerCenterX = containerWidth / 2;
+
+	// サイズ固定化(要らないかも？)
+	container.style.position = 'relative';
+	container.style.boxSizing = 'border-box';
+	container.style.width = `${containerWidth}px`;
+	container.style.height = `${containerHeight}px`;
+
+	// create engine
+	const engine = Matter.Engine.create({
+		constraintIterations: 4,
+		positionIterations: 8,
+		velocityIterations: 8,
+	});
+
+	const world = engine.world;
+
+	// create renderer
+	const render = Matter.Render.create({
+		engine: engine,
+		//element: document.getElementById('debug'),
+		options: {
+			width: containerWidth,
+			height: containerHeight,
+			background: 'transparent', // transparent to hide
+			wireframeBackground: 'transparent', // transparent to hide
+		},
+	});
+
+	// Disable to hide debug
+	Matter.Render.run(render);
+
+	// create runner
+	const runner = Matter.Runner.create();
+	Matter.Runner.run(runner, engine);
+
+	const groundThickness = 1024;
+	const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, {
+		isStatic: true,
+		restitution: 0.1,
+		friction: 2,
+	});
+
+	//const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts);
+	//const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts);
+
+	Matter.World.add(world, [
+		ground,
+		//wallRight,
+		//wallLeft,
+	]);
+
+	const objEls = Array.from(container.children) as HTMLElement[];
+	const objs: Matter.Body[] = [];
+	for (const objEl of objEls) {
+		const left = objEl.dataset.physicsX ? parseInt(objEl.dataset.physicsX) : objEl.offsetLeft;
+		const top = objEl.dataset.physicsY ? parseInt(objEl.dataset.physicsY) : objEl.offsetTop;
+
+		let obj: Matter.Body;
+		if (objEl.classList.contains('_physics_circle_')) {
+			obj = Matter.Bodies.circle(
+				left + (objEl.offsetWidth / 2),
+				top + (objEl.offsetHeight / 2),
+				Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2,
+				{
+					restitution: 0.5,
+				},
+			);
+		} else {
+			const style = window.getComputedStyle(objEl);
+			obj = Matter.Bodies.rectangle(
+				left + (objEl.offsetWidth / 2),
+				top + (objEl.offsetHeight / 2),
+				objEl.offsetWidth,
+				objEl.offsetHeight,
+				{
+					chamfer: { radius: parseInt(style.borderRadius || '0', 10) },
+					restitution: 0.5,
+				},
+			);
+		}
+		objEl.id = obj.id.toString();
+		objs.push(obj);
+	}
+
+	Matter.World.add(engine.world, objs);
+
+	// Add mouse control
+
+	const mouse = Matter.Mouse.create(container);
+	const mouseConstraint = Matter.MouseConstraint.create(engine, {
+		mouse: mouse,
+		constraint: {
+			stiffness: 0.1,
+			render: {
+				visible: false,
+			},
+		},
+	});
+
+	Matter.World.add(engine.world, mouseConstraint);
+
+	// keep the mouse in sync with rendering
+	render.mouse = mouse;
+
+	for (const objEl of objEls) {
+		objEl.style.position = 'absolute';
+		objEl.style.top = '0';
+		objEl.style.left = '0';
+		objEl.style.margin = '0';
+	}
+
+	window.requestAnimationFrame(update);
+
+	let stop = false;
+
+	function update() {
+		for (const objEl of objEls) {
+			const obj = objs.find(obj => obj.id.toString() === objEl.id.toString());
+			if (obj == null) continue;
+
+			const x = (obj.position.x - objEl.offsetWidth / 2);
+			const y = (obj.position.y - objEl.offsetHeight / 2);
+			const angle = obj.angle;
+			objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`;
+		}
+
+		if (!stop) {
+			window.requestAnimationFrame(update);
+		}
+	}
+
+	// 奈落に落ちたオブジェクトは消す
+	const intervalId = window.setInterval(() => {
+		for (const obj of objs) {
+			if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj);
+		}
+	}, 1000 * 10);
+
+	return {
+		stop: () => {
+			stop = true;
+			Matter.Runner.stop(runner);
+			window.clearInterval(intervalId);
+		},
+	};
+}
diff --git a/packages/frontend/src/utility/player-url-transform.ts b/packages/frontend/src/utility/player-url-transform.ts
new file mode 100644
index 0000000000..39c6df6500
--- /dev/null
+++ b/packages/frontend/src/utility/player-url-transform.ts
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { hostname } from '@@/js/config.js';
+
+export function transformPlayerUrl(url: string): string {
+	const urlObj = new URL(url);
+	if (!['https:', 'http:'].includes(urlObj.protocol)) throw new Error('Invalid protocol');
+
+	const urlParams = new URLSearchParams(urlObj.search);
+
+	if (urlObj.hostname === 'player.twitch.tv') {
+		// TwitchはCSPの制約あり
+		// https://dev.twitch.tv/docs/embed/video-and-clips/
+		urlParams.set('parent', hostname);
+		urlParams.set('allowfullscreen', '');
+		urlParams.set('autoplay', 'true');
+	} else {
+		urlParams.set('autoplay', '1');
+		urlParams.set('auto_play', '1');
+	}
+	urlObj.search = urlParams.toString();
+
+	return urlObj.toString();
+}
diff --git a/packages/frontend/src/utility/please-login.ts b/packages/frontend/src/utility/please-login.ts
new file mode 100644
index 0000000000..a8a330eb6d
--- /dev/null
+++ b/packages/frontend/src/utility/please-login.ts
@@ -0,0 +1,76 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent } from 'vue';
+import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
+import { i18n } from '@/i18n.js';
+import { popup } from '@/os.js';
+
+export type OpenOnRemoteOptions = {
+	/**
+	 * 外部のMisskey Webで特定のパスを開く
+	 */
+	type: 'web';
+
+	/**
+	 * 内部パス（例: `/settings`）
+	 */
+	path: string;
+} | {
+	/**
+	 * 外部のMisskey Webで照会する
+	 */
+	type: 'lookup';
+
+	/**
+	 * 照会したいエンティティのURL
+	 *
+	 * （例: `https://misskey.example.com/notes/abcdexxxxyz`）
+	 */
+	url: string;
+} | {
+	/**
+	 * 外部のMisskeyでノートする
+	 */
+	type: 'share';
+
+	/**
+	 * `/share` ページに渡すクエリストリング
+	 *
+	 * @see https://go.misskey-hub.net/spec/share/
+	 */
+	params: Record<string, string>;
+};
+
+export function pleaseLogin(opts: {
+	path?: string;
+	message?: string;
+	openOnRemote?: OpenOnRemoteOptions;
+} = {}) {
+	if ($i) return;
+
+	let _openOnRemote: OpenOnRemoteOptions | undefined = undefined;
+
+	// 連合できる場合と、（連合ができなくても）共有する場合は外部連携オプションを設定
+	if (opts.openOnRemote != null && (instance.federation !== 'none' || opts.openOnRemote.type === 'share')) {
+		_openOnRemote = opts.openOnRemote;
+	}
+
+	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
+		autoSet: true,
+		message: opts.message ?? (_openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
+		openOnRemote: _openOnRemote,
+	}, {
+		cancelled: () => {
+			if (opts.path) {
+				window.location.href = opts.path;
+			}
+		},
+		closed: () => dispose(),
+	});
+
+	throw new Error('signin required');
+}
diff --git a/packages/frontend/src/utility/popout.ts b/packages/frontend/src/utility/popout.ts
new file mode 100644
index 0000000000..5b141222e8
--- /dev/null
+++ b/packages/frontend/src/utility/popout.ts
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { appendQuery } from '@@/js/url.js';
+import * as config from '@@/js/config.js';
+
+export function popout(path: string, w?: HTMLElement) {
+	let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
+	url = appendQuery(url, 'zen');
+	if (w) {
+		const position = w.getBoundingClientRect();
+		const width = parseInt(getComputedStyle(w, '').width, 10);
+		const height = parseInt(getComputedStyle(w, '').height, 10);
+		const x = window.screenX + position.left;
+		const y = window.screenY + position.top;
+		window.open(url, url,
+			`width=${width}, height=${height}, top=${y}, left=${x}`);
+	} else {
+		const width = 400;
+		const height = 500;
+		const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2);
+		const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2);
+		window.open(url, url,
+			`width=${width}, height=${height}, top=${x}, left=${y}`);
+	}
+}
diff --git a/packages/frontend/src/utility/popup-position.ts b/packages/frontend/src/utility/popup-position.ts
new file mode 100644
index 0000000000..3dad41a8b3
--- /dev/null
+++ b/packages/frontend/src/utility/popup-position.ts
@@ -0,0 +1,161 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function calcPopupPosition(el: HTMLElement, props: {
+	anchorElement?: HTMLElement | null;
+	innerMargin: number;
+	direction: 'top' | 'bottom' | 'left' | 'right';
+	align: 'top' | 'bottom' | 'left' | 'right' | 'center';
+	alignOffset?: number;
+	x?: number;
+	y?: number;
+}): { top: number; left: number; transformOrigin: string; } {
+	const contentWidth = el.offsetWidth;
+	const contentHeight = el.offsetHeight;
+
+	let rect: DOMRect;
+
+	if (props.anchorElement) {
+		rect = props.anchorElement.getBoundingClientRect();
+	}
+
+	const calcPosWhenTop = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
+			top = (rect.top + window.scrollY - contentHeight) - props.innerMargin;
+		} else {
+			left = props.x;
+			top = (props.y - contentHeight) - props.innerMargin;
+		}
+
+		left -= (el.offsetWidth / 2);
+
+		if (left + contentWidth - window.scrollX > window.innerWidth) {
+			left = window.innerWidth - contentWidth + window.scrollX - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenBottom = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
+			top = (rect.top + window.scrollY + props.anchorElement.offsetHeight) + props.innerMargin;
+		} else {
+			left = props.x;
+			top = (props.y) + props.innerMargin;
+		}
+
+		left -= (el.offsetWidth / 2);
+
+		if (left + contentWidth - window.scrollX > window.innerWidth) {
+			left = window.innerWidth - contentWidth + window.scrollX - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenLeft = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = (rect.left + window.scrollX - contentWidth) - props.innerMargin;
+			top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
+		} else {
+			left = (props.x - contentWidth) - props.innerMargin;
+			top = props.y;
+		}
+
+		top -= (el.offsetHeight / 2);
+
+		if (top + contentHeight - window.scrollY > window.innerHeight) {
+			top = window.innerHeight - contentHeight + window.scrollY - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calcPosWhenRight = () => {
+		let left: number;
+		let top: number;
+
+		if (props.anchorElement) {
+			left = (rect.left + props.anchorElement.offsetWidth + window.scrollX) + props.innerMargin;
+
+			if (props.align === 'top') {
+				top = rect.top + window.scrollY;
+				if (props.alignOffset != null) top += props.alignOffset;
+			} else if (props.align === 'bottom') {
+				// TODO
+			} else { // center
+				top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
+				top -= (el.offsetHeight / 2);
+			}
+		} else {
+			left = props.x + props.innerMargin;
+			top = props.y;
+			top -= (el.offsetHeight / 2);
+		}
+
+		if (top + contentHeight - window.scrollY > window.innerHeight) {
+			top = window.innerHeight - contentHeight + window.scrollY - 1;
+		}
+
+		return [left, top];
+	};
+
+	const calc = (): {
+		left: number;
+		top: number;
+		transformOrigin: string;
+	} => {
+		switch (props.direction) {
+			case 'top': {
+				const [left, top] = calcPosWhenTop();
+
+				// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
+				if (top - window.scrollY < 0) {
+					const [left, top] = calcPosWhenBottom();
+					return { left, top, transformOrigin: 'center top' };
+				}
+
+				return { left, top, transformOrigin: 'center bottom' };
+			}
+
+			case 'bottom': {
+				const [left, top] = calcPosWhenBottom();
+				// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
+				return { left, top, transformOrigin: 'center top' };
+			}
+
+			case 'left': {
+				const [left, top] = calcPosWhenLeft();
+
+				// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
+				if (left - window.scrollX < 0) {
+					const [left, top] = calcPosWhenRight();
+					return { left, top, transformOrigin: 'left center' };
+				}
+
+				return { left, top, transformOrigin: 'right center' };
+			}
+
+			case 'right': {
+				const [left, top] = calcPosWhenRight();
+				// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
+				return { left, top, transformOrigin: 'left center' };
+			}
+		}
+	};
+
+	return calc();
+}
diff --git a/packages/frontend/src/utility/post-message.ts b/packages/frontend/src/utility/post-message.ts
new file mode 100644
index 0000000000..11b6f52ddd
--- /dev/null
+++ b/packages/frontend/src/utility/post-message.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const postMessageEventTypes = [
+	'misskey:shareForm:shareCompleted',
+] as const;
+
+export type PostMessageEventType = typeof postMessageEventTypes[number];
+
+export type MiPostMessageEvent = {
+	type: PostMessageEventType;
+	payload?: any;
+};
+
+/**
+ * 親フレームにイベントを送信
+ */
+export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
+	window.parent.postMessage({
+		type,
+		payload,
+	}, '*');
+}
diff --git a/packages/frontend/src/utility/reaction-picker.ts b/packages/frontend/src/utility/reaction-picker.ts
new file mode 100644
index 0000000000..81f6c02dcf
--- /dev/null
+++ b/packages/frontend/src/utility/reaction-picker.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { defineAsyncComponent, ref } from 'vue';
+import type { Ref } from 'vue';
+import { popup } from '@/os.js';
+import { store } from '@/store.js';
+
+class ReactionPicker {
+	private src: Ref<HTMLElement | null> = ref(null);
+	private manualShowing = ref(false);
+	private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
+	private onChosen?: (reaction: string) => void;
+	private onClosed?: () => void;
+
+	constructor() {
+		// nop
+	}
+
+	public async init() {
+		const reactionsRef = store.reactiveState.reactions;
+		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
+			src: this.src,
+			pinnedEmojis: reactionsRef,
+			asReactionPicker: true,
+			targetNote: this.targetNote,
+			manualShowing: this.manualShowing,
+		}, {
+			done: reaction => {
+				if (this.onChosen) this.onChosen(reaction);
+			},
+			close: () => {
+				this.manualShowing.value = false;
+			},
+			closed: () => {
+				this.src.value = null;
+				if (this.onClosed) this.onClosed();
+			},
+		});
+	}
+
+	public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
+		this.src.value = src;
+		this.targetNote.value = targetNote;
+		this.manualShowing.value = true;
+		this.onChosen = onChosen;
+		this.onClosed = onClosed;
+	}
+}
+
+export const reactionPicker = new ReactionPicker();
diff --git a/packages/frontend/src/utility/reload-ask.ts b/packages/frontend/src/utility/reload-ask.ts
new file mode 100644
index 0000000000..057f57471a
--- /dev/null
+++ b/packages/frontend/src/utility/reload-ask.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { unisonReload } from '@/utility/unison-reload.js';
+
+let isReloadConfirming = false;
+
+export async function reloadAsk(opts: {
+	unison?: boolean;
+	reason?: string;
+}) {
+	if (isReloadConfirming) {
+		return;
+	}
+
+	isReloadConfirming = true;
+
+	const { canceled } = await os.confirm(opts.reason == null ? {
+		type: 'info',
+		text: i18n.ts.reloadConfirm,
+	} : {
+		type: 'info',
+		title: i18n.ts.reloadConfirm,
+		text: opts.reason,
+	}).finally(() => {
+		isReloadConfirming = false;
+	});
+
+	if (canceled) return;
+
+	if (opts.unison) {
+		unisonReload();
+	} else {
+		location.reload();
+	}
+}
diff --git a/packages/frontend/src/utility/search-emoji.ts b/packages/frontend/src/utility/search-emoji.ts
new file mode 100644
index 0000000000..371f69b9a7
--- /dev/null
+++ b/packages/frontend/src/utility/search-emoji.ts
@@ -0,0 +1,106 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type EmojiDef = {
+	emoji: string;
+	name: string;
+	url: string;
+	aliasOf?: string;
+} | {
+	emoji: string;
+	name: string;
+	aliasOf?: string;
+	isCustomEmoji?: true;
+};
+type EmojiScore = { emoji: EmojiDef, score: number };
+
+export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
+	if (!query) {
+		return [];
+	}
+
+	const matched = new Map<string, EmojiScore>();
+	// 完全一致（エイリアスなし）
+	emojiDb.some(x => {
+		if (x.name === query && !x.aliasOf) {
+			matched.set(x.name, { emoji: x, score: query.length + 3 });
+		}
+		return matched.size === max;
+	});
+
+	// 完全一致（エイリアス込み）
+	if (matched.size < max) {
+		emojiDb.some(x => {
+			if (x.name === query && !matched.has(x.aliasOf ?? x.name)) {
+				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
+			}
+			return matched.size === max;
+		});
+	}
+
+	// 前方一致（エイリアスなし）
+	if (matched.size < max) {
+		emojiDb.some(x => {
+			if (x.name.startsWith(query) && !x.aliasOf && !matched.has(x.name)) {
+				matched.set(x.name, { emoji: x, score: query.length + 1 });
+			}
+			return matched.size === max;
+		});
+	}
+
+	// 前方一致（エイリアス込み）
+	if (matched.size < max) {
+		emojiDb.some(x => {
+			if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
+				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
+			}
+			return matched.size === max;
+		});
+	}
+
+	// 部分一致（エイリアス込み）
+	if (matched.size < max) {
+		emojiDb.some(x => {
+			if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) {
+				matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
+			}
+			return matched.size === max;
+		});
+	}
+
+	// 簡易あいまい検索（3文字以上）
+	if (matched.size < max && query.length > 3) {
+		const queryChars = [...query];
+		const hitEmojis = new Map<string, EmojiScore>();
+
+		for (const x of emojiDb) {
+			// 文字列の位置を進めながら、クエリの文字を順番に探す
+
+			let pos = 0;
+			let hit = 0;
+			for (const c of queryChars) {
+				pos = x.name.indexOf(c, pos);
+				if (pos <= -1) break;
+				hit++;
+			}
+
+			// 半分以上の文字が含まれていればヒットとする
+			if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
+				hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
+			}
+		}
+
+		// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく（6件＝オートコンプリートのポップアップのサイズ分）
+		[...hitEmojis.values()]
+			.sort((x, y) => y.score - x.score)
+			.slice(0, 6)
+			.forEach(it => matched.set(it.emoji.name, it));
+	}
+
+	return [...matched.values()]
+		.sort((x, y) => y.score - x.score)
+		.slice(0, max)
+		.map(it => it.emoji);
+}
diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts
new file mode 100644
index 0000000000..1bee4986f6
--- /dev/null
+++ b/packages/frontend/src/utility/select-file.ts
@@ -0,0 +1,130 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { useStream } from '@/stream.js';
+import { i18n } from '@/i18n.js';
+import { uploadFile } from '@/utility/upload.js';
+import { prefer } from '@/preferences.js';
+
+export function chooseFileFromPc(
+	multiple: boolean,
+	options?: {
+		uploadFolder?: string | null;
+		keepOriginal?: boolean;
+		nameConverter?: (file: File) => string | undefined;
+	},
+): Promise<Misskey.entities.DriveFile[]> {
+	const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder;
+	const keepOriginal = options?.keepOriginal ?? prefer.s.keepOriginalUploading;
+	const nameConverter = options?.nameConverter ?? (() => undefined);
+
+	return new Promise((res, rej) => {
+		const input = document.createElement('input');
+		input.type = 'file';
+		input.multiple = multiple;
+		input.onchange = () => {
+			if (!input.files) return res([]);
+			const promises = Array.from(
+				input.files,
+				file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal),
+			);
+
+			Promise.all(promises).then(driveFiles => {
+				res(driveFiles);
+			}).catch(err => {
+				// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
+			});
+
+			// 一応廃棄
+			(window as any).__misskey_input_ref__ = null;
+		};
+
+		// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
+		// iOS Safari で正常に動かす為のおまじない
+		(window as any).__misskey_input_ref__ = input;
+
+		input.click();
+	});
+}
+
+export function chooseFileFromDrive(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
+	return new Promise((res, rej) => {
+		os.selectDriveFile(multiple).then(files => {
+			res(files);
+		});
+	});
+}
+
+export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
+	return new Promise((res, rej) => {
+		os.inputText({
+			title: i18n.ts.uploadFromUrl,
+			type: 'url',
+			placeholder: i18n.ts.uploadFromUrlDescription,
+		}).then(({ canceled, result: url }) => {
+			if (canceled) return;
+
+			const marker = Math.random().toString(); // TODO: UUIDとか使う
+
+			const connection = useStream().useChannel('main');
+			connection.on('urlUploadFinished', urlResponse => {
+				if (urlResponse.marker === marker) {
+					res(urlResponse.file);
+					connection.dispose();
+				}
+			});
+
+			misskeyApi('drive/files/upload-from-url', {
+				url: url,
+				folderId: prefer.s.uploadFolder,
+				marker,
+			});
+
+			os.alert({
+				title: i18n.ts.uploadFromUrlRequested,
+				text: i18n.ts.uploadFromUrlMayTakeTime,
+			});
+		});
+	});
+}
+
+function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
+	return new Promise((res, rej) => {
+		const keepOriginal = ref(prefer.s.keepOriginalUploading);
+
+		os.popupMenu([label ? {
+			text: label,
+			type: 'label',
+		} : undefined, {
+			type: 'switch',
+			text: i18n.ts.keepOriginalUploading,
+			ref: keepOriginal,
+		}, {
+			text: i18n.ts.upload,
+			icon: 'ti ti-upload',
+			action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)),
+		}, {
+			text: i18n.ts.fromDrive,
+			icon: 'ti ti-cloud',
+			action: () => chooseFileFromDrive(multiple).then(files => res(files)),
+		}, {
+			text: i18n.ts.fromUrl,
+			icon: 'ti ti-link',
+			action: () => chooseFileFromUrl().then(file => res([file])),
+		}], src);
+	});
+}
+
+export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
+	return select(src, label, false).then(files => files[0]);
+}
+
+export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
+	return select(src, label, true);
+}
diff --git a/packages/frontend/src/utility/show-moved-dialog.ts b/packages/frontend/src/utility/show-moved-dialog.ts
new file mode 100644
index 0000000000..35b3ef79d8
--- /dev/null
+++ b/packages/frontend/src/utility/show-moved-dialog.ts
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as os from '@/os.js';
+import { $i } from '@/account.js';
+import { i18n } from '@/i18n.js';
+
+export function showMovedDialog() {
+	if (!$i) return;
+	if (!$i.movedTo) return;
+
+	os.alert({
+		type: 'error',
+		title: i18n.ts.accountMovedShort,
+		text: i18n.ts.operationForbidden,
+	});
+
+	throw new Error('account moved');
+}
diff --git a/packages/frontend/src/utility/show-suspended-dialog.ts b/packages/frontend/src/utility/show-suspended-dialog.ts
new file mode 100644
index 0000000000..8b89dbb936
--- /dev/null
+++ b/packages/frontend/src/utility/show-suspended-dialog.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+
+export function showSuspendedDialog() {
+	return os.alert({
+		type: 'error',
+		title: i18n.ts.yourAccountSuspendedTitle,
+		text: i18n.ts.yourAccountSuspendedDescription,
+	});
+}
diff --git a/packages/frontend/src/utility/shuffle.ts b/packages/frontend/src/utility/shuffle.ts
new file mode 100644
index 0000000000..1f6ef1928c
--- /dev/null
+++ b/packages/frontend/src/utility/shuffle.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * 配列をシャッフル (破壊的)
+ */
+export function shuffle<T extends unknown[]>(array: T): T {
+	let currentIndex = array.length;
+	let randomIndex: number;
+
+	// While there remain elements to shuffle.
+	while (currentIndex !== 0) {
+		// Pick a remaining element.
+		randomIndex = Math.floor(Math.random() * currentIndex);
+		currentIndex--;
+
+		// And swap it with the current element.
+		[array[currentIndex], array[randomIndex]] = [
+			array[randomIndex], array[currentIndex]];
+	}
+
+	return array;
+}
diff --git a/packages/frontend/src/utility/snowfall-effect.ts b/packages/frontend/src/utility/snowfall-effect.ts
new file mode 100644
index 0000000000..d88bdb6660
--- /dev/null
+++ b/packages/frontend/src/utility/snowfall-effect.ts
@@ -0,0 +1,490 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class SnowfallEffect {
+	private VERTEX_SOURCE = `#version 300 es
+		in vec4 a_position;
+		in vec4 a_color;
+		in vec3 a_rotation;
+		in vec3 a_speed;
+		in float a_size;
+		out vec4 v_color;
+		out float v_rotation;
+		uniform float u_time;
+		uniform mat4 u_projection;
+		uniform vec3 u_worldSize;
+		uniform float u_gravity;
+		uniform float u_wind;
+		uniform float u_spin_factor;
+		uniform float u_turbulence;
+
+		void main() {
+			v_color = a_color;
+			v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y;
+
+			vec3 pos = a_position.xyz;
+
+			pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
+			pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
+
+			pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z;
+			pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z;
+
+			gl_Position = u_projection * vec4(pos.xyz, a_position.w);
+			gl_PointSize = (a_size / gl_Position.w) * 100.0;
+		}
+	`;
+
+	private FRAGMENT_SOURCE = `#version 300 es
+		precision highp float;
+
+		in vec4 v_color;
+		in float v_rotation;
+		uniform sampler2D u_texture;
+		out vec4 out_color;
+
+		void main() {
+			vec2 rotated = vec2(
+				cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5,
+				cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5
+			);
+
+			vec4 snowflake = texture(u_texture, rotated);
+
+			out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a);
+		}
+	`;
+
+	private gl: WebGLRenderingContext;
+	private program: WebGLProgram;
+	private canvas: HTMLCanvasElement;
+	private buffers: Record<string, {
+		size: number;
+		value: number[] | Float32Array;
+		location: number;
+		ref: WebGLBuffer;
+	}>;
+	private uniforms: Record<string, {
+		type: string;
+		value: number[] | Float32Array;
+		location: WebGLUniformLocation;
+	}>;
+	private texture: WebGLTexture;
+	private camera: {
+		fov: number;
+		near: number;
+		far: number;
+		aspect: number;
+		z: number;
+	};
+	private wind: {
+		current: number;
+		force: number;
+		target: number;
+		min: number;
+		max: number;
+		easing: number;
+	};
+	private time: {
+		start: number;
+		previous: number;
+	} = {
+			start: 0,
+			previous: 0,
+		};
+	private raf = 0;
+
+	private density: number = 1 / 90;
+	private depth = 100;
+	private count = 1000;
+	private gravity = 100;
+	private speed: number = 1 / 10000;
+	private color: number[] = [1, 1, 1];
+	private opacity = 1;
+	private size = 4;
+	private snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAErRJREFUeAHdmgnYlmPax5MShaxRKRElPmXJXpaSsRxDU0bTZ+kt65RloiRDltEMQsxYKmS+zzYjxCCamCzV2LchResMIxFRQ1G93+93Pdf5dL9v7zuf4/hm0fc/jt9znddy3/e1nNd53c/7vHXq/AtVWVnZA/bzkaQjoWG298DeMdvrmP6/EIOqC4fBsbAx7Arz4TaYBPXgWVDnO2jSBrB2T0IMIA9mCmmoE8aonPkR6WPZHlp9xSlfeyeBzq9bHBD5feEdUGfDXBgBqnde+a2wvw/dYdNctvZNAp1PnTaFttA6JgP7eVgBM0CNzgO9HNvy0AcYDda6SaDTdXOnz8X+IkZDugAGQmOYA+ob6Ah/MIOMDRPhJjgJ6uV7pXtWt81/50SnY/Wvwn4ZDHAvwJ9ATYcxyaqsnEnqZCyCPaE80BgYZXG/5A3VyyP/b08LHa11z9KmFUwA5eqruRBHYX1s8WSI1Xcbme8Mt8PWUCU+kF8XbFN+dtH+p06OD4IU8EjD/VOZ5bnezq0XHcHuC2oV7BDlkVIWq56uIX8UjAO31GRIMYW0Vo/xXtSXJyTuXVO6xk1qalRTmQ9AfqzEvog2XYpllnsd6Qr4unCPT7NtByu0uU7vuAaOoy1JuvfXpJdTvSX0gI1gCXwGZdFmEFxoQb7Wid8s7lNu+I8wuHGsTqz2zpQ9DAa5R6HC55A2gvCMXthvwi25bjx26H0M9/9f4Rnok9s0zulFlC2HzzP9cnld8nH/p7DVrbmuIfYs6JLz9U3/z+KGadDeCDsmwre7GyEifn/su8HVSsL2HeBn8CK8AW+B7u9R5yrPgyOjvSn5DWAaXAG2UU7CE9Ayt4k4sR1lX4LaLdd9gn2ftsL+Vtuh1Dp/elH1C8lvCdUj8kDK3gbP8XdhCnSC86rcsNSR9pQvhc/gVlB9bUfqoFNAy/mLrUROrpMwCtpBxBbTtLqkF4K6IF9rf57I9pnYekx5AS0P1VhopXso9pR5buC7+kewU86nFcB+BT4EXdIvNO73sRBubGTXLZtTtgp+DEb++bACdqBuJOlAaMMzLVM3whegNznQDtCb+pW5b8YY76euB5+7pxm0IbzCfS8m3Zf2q4T8/+4JNArXGoptpxz8LqDmQJq0Qnostt/sfIn5GygD4/Zeq7B7wljQO2yjB/QGj0Pjxz4wGdqXrkjXtCT/ISyDa6EPpHrSraFjvnecFpMoMx40Br3xSlD262rYObevddHTs2kYwWUG9uP5It/f1eU5Xw9btwoXPALbwYXcg+unG/KB3Rq8n9ddAOpn4Kr8BAaBcltcDo9D7Ouavig1o34x7F94xqPk74eLQH0MH8HvwS3SLPe9iheEG6f70KiuLpZv6sxG/Va5bFJOabaO7ucAvGEbeAH+AN1hV7iDOidQFz4A2oJb6D1YDhXZHkTqpL8EbqHDYRtwW20AsdIb8syl5N2e6dTAPB2mWYa+hE4Qk7I59iMwFZ70GlJlfyuTVfygs7Hyw7HbwI0w3Tak14BqEtdg7wVdIx8pZbtBUbrjZeA3vUPBANkU+sEehev8O4Db6QpwYm+D8II0KPKHwUFeQ3oLDIMN4WgID1yOPQ+MAXMhNAtju3ztmtuAypiAw7EXwo/Am+0NfUG5mknYc6GfGVIjsoFNuyuoh8COuDcd2LmwA9jWE8bB3Q7N4XrwWAz5XOXR+Tx4n6FgdHeB6sF/w2QwhlSXdXvl/jixx4NH8GW5LDzb7GrR4ES4F5QddB99CieAwStOAPegdUZ2B71F3AXbQSn3vJ1bYaYWrayh3NUPTcbYFExVW3CfXwlvgfoavMbnDAY9dxGo6dCt0LeaB54H4UydDEPA2R4PDlrFLB9XuNmTlO+Xr7X9ZNBr9J4+EN8AMcv6ButpMND9FM6EnTOHkLrSnvtzwbbq3vwMB2ow/qWFSC8ZC++ZQaldbquH2afQWbl8TdcvVtC6LtipifAuOKt6gA9Tzqgzb5R2gP1hX3DVtZVHVvdklY5DA5beIkVPuZn8LOgAnWEfeAaUkxCan/voBNkfF+U5cFu5z5XlxZU20OmZtgm1K45VO4naNCukrcBZVk/CD+E/YBjoYjXJY8Zg9DxsDrbbBHTRotxOrug4eBs+hHgWZtKzfHrdXHBi9gDvqzxFHNA5KVfyBCf0ExgB7nkXStLLEKkniNf0AzUs5+ublkVFKiC9FBZAvGxshT0NnN3zoSUYSJQPcjAvm0HmjcIPemNS96F6E36drFLwugx7EEzNZV/l9IjoEPkW4B7eFtYH9QKcBcfA/aCWgpPQOT+zMbb9fS3nDbYR2MdgV0S5aVlUhLs0w45IHi7sqnnGJ2E7CXqHWgZXgJ1y8KqpDUmfSLmSV5yB/XrpDqVP8ofmehNdOv7I0ShfP4yyJdl2a4SchI1gCXgkHgljYfvc1i3cs/SU1A9jQRpfri/b0Sal1RrtSj4ULyHprY5C6+6E1+EBULq0E+DK7A96iwqX0z4td8B3dCdob5gD3UB3j9fUcNuDKFOvgc+bZAZFf4Zgu/q/AGPMgfm+5ShPWay+k6I31BwAvVDRYL2cuqfUVTkfnTqvVFx5ai7/MXn3tp1UrtRkDWRsaAMjzaD08uJ1irz7+8ps/6ZYj90V3FKrQBkvmubULbN7vs7tZRyJV9w0ePLbQ4PcJspqXnkbhbgoGk/AVptZRxpB0hU7Mpc1x34cdgKPm1dzeTts9XPwlFAO5Au4BDbO7ZycO7J9A/Zh2b4A2+ucALefWpTrflDKVq4kHQBOoi9PO1qvsDeGd6AxXAJbQ5VxlFrW8EnDcJlTsOPcjElxL7WNy7AduC4f2+A/rSN/Hyg7YMBTxgqPUT3F2HAqtIb58GvQW86GqyG+ff4UWz0FBuH4UhaTal1vmAGfg98dfP4d4HPGwmwYAg+D2/J7uU0ap/YaolHZVbBj5d1DaSK8ADsmqiH2JIhgNRhbPZrbhSdZ5heVJGw7477VfYuaagMK2sM8iMloga1HXAt/AeWELgQnR/0Z7k3W6pe3xTn/JamTFPGnPMZSj6p90rA8YOziwHcnH/EgTovJlJ0LPSHkyrTKmZNJ+8KrYKBsCQeB0pWdBFNleieMgzjL44jejTK1CPSY0CiMdyOT09g6ni5O3Ceg51U4VNLaPSA3SDNEwwiKFdgHgANNrpjb7UVejYTYCuZ92DR42HYh8gfDJfAMqBi4dqxk+RrKGkD0YXNsA6AT5qCUXhBe5CR0gPCC4dhqKFwI1m1qX0hr94CotDE4aAd3PCyBX4Jyn+sNL5tBDsRAp3S7b5KVYwa2A0nHaO5AXBeDtnlMxizsW+HomLh8zX9R5sTeBSEn/cqc2Tvak9eDXCyP2PgbYWzn2gefHxT7+0Qu/h18DO7XmPWYcYqSXuHz2myb6G7RNs7meLgeMxXugbiPA3clQx0xtgNPGN819L7+oCzvm6zSx+EkI+Du3Pe0LbOd/jqc7dhG9Wib+mJ5jaJBuL8e4B5aAMpAomKlb8d+KZWUVnw+dgzKSdDtvKaLDyJ1ReZB7O0J2EV5Xwd8OsTJExNpu7Q1SJ8zgy7K93UCX4P4mr4udoyhPGDKygOP+tomIFarMw2d+cfgF2DnDVAGoBvzw33YTHgPDoXQ7Fx/Wy6YkdMrcrmrehO4Pz3WvP90cIVPgonwITg4973yu0XTZK0+ZQaQd+K816twVAwKO71ZRj9zeg7lcVzXHghpVN4n2G3BAHQ1NILx4MBjoppgLwL3Ww8IHZsf6vGk3O8fwx9heK7rhD0o2zdg75JtT6GzQQ8KzcZwElSr3M5J85ktYCzEG+Gx2NNzm/Cm5pSp+K2gfLrZbg3RcB2IQcZN1qPM3+l06SjbAltX/TiXe1wtg7+AdR+AcgIs7xUPw94XxuTrnOD4E1bEoe9Rptw+DWGOGeQi7JOs1SfKKfk+epcakPNxbI8uFVdem8vT6aJdq7jASYjOFPdQDP4Q6t+Em8HVutmbkbYH9Tv4LcQW+H6ujy9Wrtxc6A7vQnznb5TbHUPZ0mw7CeoaOBAegmfBIKw8WZzs34M/oNiPGPzB2KHdrVMUlD29VFLLpw2jMWmnaIbdDNxXur+dWgVumTMglI4zMgbUEV5LmjqW7XnRkDS9qhbu/xZlZ8LWuc3UfM22Of80aVcYDJ/lstdIWxXu0TGXm/TO19vveHWuOglUxOo6iMfyBe7JOEp01ech9puuuBCMA8pVcUUNUB5lqgMYwJyE1oXOGTh9v1gO6kmogKEwHtREMHYofz5zAl3lJ2AWqJfgfohJiKB8HWWfg54YA9Zr1fn5Xmm80SdvHhNwVmq2umF8vWxA+WRwwE9BPNhOulrq0nxz97j6Go6DF8HYcBfYyer6MwWuoINeDG6roq4iE97QCtsJuxWc2JrkCeKEbgX7waOgnLiavxdQEWfohtgRwCrygIoxoQv1K0FNgR7gAKPTB+dr5lAWMliqmbAb7AzbgCs42vYK21NmOiwHJ9atpdxqDlhdA75QdYJT4XUYDfbBiVRe5ySoZTAbBpeekp6T4lo5uFnBz0fpJ6P8E9SJufEdXHipdRA/mw2hzmvfhrfgfjCKPwJnwn2g3igldb4hNaD5a6/fz7eHVuAb2wPwPs+4DB7E/hTagd64BbgoC6Ab9IAfgn+OX0p/ppAaGxZjnw6+Ep8DK8Cj0IDrmHw3GaeN9EZ/AlxFfk1RuVGUYu8K00D9Fa6EvrAUVKzO29gXg9vC1VW3g540w0xBcU2hKJnz+FxYvTCXWaduK/StuTZlLcD6JjnfEvsb6A56m32z78q4FMGw1gA4lEa60WmwMeiSnsljIBSDmEOBE3RdfvggbMuMIbNhItgJtbyUpE9ddjA0Bid1sderXDaQ1OdPAO9zH6hDcpuG2Ml7SQfArHRx6Xpf3JTluySrsrIP6Seg9/iMqsEvF6YZoXIDeAZCRmpneAHEnnLQnaEuXATX53schR3n/e7YyuvOT1bpnyV107Io3xZ6QWs4EirAyXkEqqvK3xa9CQ0c5C5xQ+zN8kWjcr2xZxTsBHfmsipbP671ZmW3wHYA58DdEPobhtwVF2HfBE9H3pT8xjkdja3iiDK4PQBO8Dx4B9wiH8JKeANcKTUW9IITwKNMeYrcArfDhVDsb1pVyty26le5D97/zWzrzVUGXyVjI0WjHUgq4CjoAuGiRuuJkN7mSJX7cn+uaZNyfBBgDHZqXvqsU2cZ6aPwChgE/ap8M9wLbSH+0DKOaw18z8N12GPAyf4BfADbwBmwCbxAHY9NvxQXx2GgVLZXPvurZDE0rqk5+NmAm8U2aIbdH9yDalgpSS80ltlB29fPqW9c8XLUHnsIuGquqt8gN7edwtazrOsAn4MysLryX8BD4Ap3y+0dZROIwPsl9h/hHjgit4lXdrdvHN8dc91wyk7JdvIS7VpF46Jb2ZGz4WJIRyBpBKQW3oR8lZuSvwQMhKtAfQUpYuf27cgbNx6EEeDAzgMHPwYMYi2gEcSfxC7B9qicDMoo/1vQI8p9IG88WAY/yeVpYrJdHpf5vytu4Ky7X46xIamrvjDb52OrG3K+HrZt4xq9wYEZPGPVfp7bhsdE2os2ylV6J1n5mbYPUX4S7AkGX+OAk2t6mm1Iw3PtQ+O4LuooK26RYvW3s7nBLZDiAGlbUHYiRV/S5AWk28DTEFqB4eo+B+n1M55Ivhu4kspj92uYCm6Px0Gv61lor0fcDQNBrQQnOr71lVeYsm894L/bkBuFe/u93eBngJtJMlwTDIDKyfDt6n3se8Dt8jHoNU0o70waq34obZ8lPx4coG+LbifrP6Pt0aQvwn65LFzcAHY8ZUtgAnwExp2WoMpeQLvaA12p7bf/pLPFmS3a/ajr750cfE43wX4YYmU9wi7IddHBCsrc69vm8uuwQydYVhQVvmsUn7s+ebfD0GhXrI+yf2jqA4oPKdo+iHxMwHbYRmgjta4cUTqCWXkg0UHatIR4SxxWKK9PeXhgKiZfxWOthzXuGff4p6b54bH3Y3W3pNxJcK8ebgdI44iys0G0N/8qKGOAGg9Ni50n3yjy2GkxSKtMRtT/21I7Fg/H9lRIX6qK5YX6zSjvDL4BGiBfBnUNmFdzwfKX4Ct40OtJv1sDj0Hlzrk6xbM3tob7uCf4amyk96VHvQg7gltGzQG9wpcwX6BCesfJ3/kJiMmgs+Gm4errUeZqF+Up4IoOzoWLcmqETyLve/2BsKkFpGUvK7VYCz6j06RbQx+ogHhN3Qdb3QF+a/wVKF94OhSHR77sWcXytcKm82usHGW9QE2B3skq/QB7APaqnJ9NuvaufnF1GIhxYH3LSAeA+hM0hMfgNzATdHvjgDHDv+qkP8gW77XW2gwmYsJe2F3zZDgxI7NteTo+/1WD/B9Au3Zjh2RyrgAAAABJRU5ErkJggg==';
+	private mode = 'snow';
+
+	private INITIAL_BUFFERS = () => ({
+		position: { size: 3, value: [] },
+		color: { size: 4, value: [] },
+		size: { size: 1, value: [] },
+		rotation: { size: 3, value: [] },
+		speed: { size: 3, value: [] },
+	});
+
+	private INITIAL_UNIFORMS = () => ({
+		time: { type: 'float', value: 0 },
+		worldSize: { type: 'vec3', value: [0, 0, 0] },
+		gravity: { type: 'float', value: this.gravity },
+		wind: { type: 'float', value: 0 },
+		spin_factor: { type: 'float', value: this.mode === 'sakura' ? 8 : 1 },
+		turbulence: { type: 'float', value: this.mode === 'sakura' ? 2 : 1 },
+		projection: {
+			type: 'mat4',
+			value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
+		},
+	});
+
+	private UNIFORM_SETTERS = {
+		int: 'uniform1i',
+		float: 'uniform1f',
+		vec2: 'uniform2fv',
+		vec3: 'uniform3fv',
+		vec4: 'uniform4fv',
+		mat2: 'uniformMatrix2fv',
+		mat3: 'uniformMatrix3fv',
+		mat4: 'uniformMatrix4fv',
+	};
+
+	private CAMERA = {
+		fov: 60,
+		near: 5,
+		far: 10000,
+		aspect: 1,
+		z: 100,
+	};
+
+	private WIND = {
+		current: 0,
+		force: 0.01,
+		target: 0.01,
+		min: 0,
+		max: 0.125,
+		easing: 0.0005,
+	};
+	/**
+	 * @throws {Error} - Thrown when it fails to get WebGL context for the canvas 
+	 */
+	constructor(options: {
+		sakura?: boolean;
+	}) {
+		if (options.sakura) {
+			this.mode = 'sakura';
+			this.snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDItMDFUMTQ6Mzk6NTYrMDkwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyNC0wMi0wMVQxNDozOTo1NiswOTAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI2NCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjY0IgogICBleGlmOkNvbG9yU3BhY2U9IjEiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iNjQiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjY0IgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIgogICB0aWZmOlhSZXNvbHV0aW9uPSI3Mi8xIgogICB0aWZmOllSZXNvbHV0aW9uPSI3Mi8xIj4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0icHJvZHVjZWQiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IFBob3RvIDIgMi4zLjEiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/PhldI30AAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWRu0sDQRCHP6Mh4oOIWlhYBPHRJBIjiDYWEV+gFjGCr+ZyuUuEJB53JyK2gq2gINr4KvQv0FawFgRFEcTaWtFG5ZwzgQQxs+zst7+dGXZnwRPPqFmrKgzZnG3GRqOB2bn5gO8FDxV46aJRUS1jcnokTln7uJdYsduQW6t83L9Wm9QsFSqqhQdVw7SFx4QnVm3D5R3hZjWtJIXPhIOmXFD4ztUTeX5xOZXnL5fNeGwIPA3CgVQJJ0pYTZtZYXk57dnMilq4j/uSOi03My1rm8xWLGKMEiXAOMMM0UcPA+L7CBGhW3aUyQ//5k+xLLmqeIM1TJZIkcYmKOqKVNdk1UXXZGRYc/v/t6+W3hvJV6+LgvfZcd46wLcN31uO83nkON/HUPkEl7li/vIh9L+LvlXU2g/AvwHnV0UtsQsXm9DyaCim8itVyvToOryeQv0cNN1AzUK+Z4VzTh4gvi5fdQ17+9Ap8f7FHyc6Z8kcDq1+AAAACXBIWXMAAAsTAAALEwEAmpwYAAADwElEQVR4nO2bT4hWVRjGf75TkhoEkhSa/9ocRIIwCsrE1pVnLbkYdFdGgQRS6caVm3CVy2oRuqmQ2yJXKTJh4GqCGs/CJCcLccAJ/yDpnGnxHYeZ4TrNfOc55y78nuWdc3/ve57v+b65f86BgQaqotiE5bEJKxYx7onYhOU1egKwGkViE/YCN4Cx2ITNC4xbDVwAJmMT9tXobVnpArEJe4CvZx0aB7aZdxPzxhkwArw66/Ae8+5Eyf6KJiA2YRPw+bzD64EjLcP3MXfyAMdjEzYWaG1GxRIQmzAEnAVeb/nzFPCSeTeaxj4FBOCZlrEjwBvm3VSJPksm4BPaJw8wBHwXm/BibMIW4HvaJ09ifFygP6BQAtKkfgEeEyHvAy+YdxdFvBmVSsBBdJMnsQ4KeTOSJyA2YT1wCXhcjL4HPG/e/amElkjAAfSTJzEPqKHSBKQLmSvAKiV3lm4BG8y7GyqgOgHvU27yAE+mGjLJEhCbsBL4A3haxXyIJoCN5t0dBUyZgF2UnzypxtsqmNKAt4SsarUkX4F0I3ONOgkAuA48a97FXJAqAa9Qb/IAa4CXFSCVATXjL635yBuQ/RsQm7AWuCroZamaBtaZd3/nQBQJeFPA6EfLFLUVBrwmYPSr7bkAhQHPCRj9al0uQGHAWgGjs9oKA7I/hS5rZ/0XSC86JDclGVph3t3t9+TcBHT56T9QVg+5BnT5/X+grB4GCcgs/sgnYCjzfIWyesg14Hrm+Qpl9ZBrwMT/DymurB4GCeiyuEidGnCN3n15V5pOPfStLAPMu1vAWA4jU7+Zd7dzAIqboREBo7PaCgN+EjA6qz1IQDbAu9/prQeorUvm3eVciOqx+JcizlL0hQKiMuAreiu/amkq1cyWxADz7ipwWsFapH4w7/5SgJRvh+cviCyp4yqQeonMOWCHktmic+bdThVMvUSmyFK2kjWkBph354FTSuY8nTLvflYCSyyT+xD4pwB3EvhADZUbYN5dAfarucB+825cDS25WvwksFuEO2nevSNizVHJ1eLvAoplrePAewJOq4oZYN5NAsPkPTCZBoYTq4iK7hgx734EjmUgjpl3Z1T9tKnGpqlP6e+p0Vg6t6iKG5De3A6ztJul+/Si3/db38WqyrY58+4CcHQJpxxN5xRXFQOSjgCjixg3SvuusiKqZoB59y+964KbCwy7Cew27+7V6apuAkibnhbaEbq3xMaohVTVAADz7hvgMHN/FKeAQ+bdt7X7Kb519mGKTdgKfEbvYucj8+7XLvr4DxAA134c0w/5AAAAAElFTkSuQmCC';
+			this.size = 10;
+			this.density = 1 / 280;
+		}
+
+		const canvas = this.initCanvas();
+		const gl = canvas.getContext('webgl2', { antialias: true });
+		if (gl == null) throw new Error('Failed to get WebGL context');
+
+		document.body.append(canvas);
+
+		this.canvas = canvas;
+		this.gl = gl;
+		this.program = this.initProgram();
+		this.buffers = this.initBuffers();
+		this.uniforms = this.initUniforms();
+		this.texture = this.initTexture();
+		this.camera = this.initCamera();
+		this.wind = this.initWind();
+
+		this.resize = this.resize.bind(this);
+		this.update = this.update.bind(this);
+
+		window.addEventListener('resize', () => this.resize());
+	}
+
+	private initCanvas(): HTMLCanvasElement {
+		const canvas = document.createElement('canvas');
+
+		Object.assign(canvas.style, {
+			position: 'fixed',
+			top: 0,
+			left: 0,
+			width: '100vw',
+			height: '100vh',
+			background: 'transparent',
+			'pointer-events': 'none',
+			'z-index': 2147483647,
+		});
+
+		return canvas;
+	}
+
+	private initCamera() {
+		return { ...this.CAMERA };
+	}
+
+	private initWind() {
+		return { ...this.WIND };
+	}
+
+	private initShader(type, source): WebGLShader {
+		const { gl } = this;
+		const shader = gl.createShader(type);
+		if (shader == null) throw new Error('Failed to create shader');
+
+		gl.shaderSource(shader, source);
+		gl.compileShader(shader);
+
+		return shader;
+	}
+
+	private initProgram(): WebGLProgram {
+		const { gl } = this;
+		const vertex = this.initShader(gl.VERTEX_SHADER, this.VERTEX_SOURCE);
+		const fragment = this.initShader(gl.FRAGMENT_SHADER, this.FRAGMENT_SOURCE);
+		const program = gl.createProgram();
+		if (program == null) throw new Error('Failed to create program');
+
+		gl.attachShader(program, vertex);
+		gl.attachShader(program, fragment);
+		gl.linkProgram(program);
+		gl.useProgram(program);
+
+		return program;
+	}
+
+	private initBuffers(): SnowfallEffect['buffers'] {
+		const { gl, program } = this;
+		const buffers = this.INITIAL_BUFFERS() as unknown as SnowfallEffect['buffers'];
+
+		for (const [name, buffer] of Object.entries(buffers)) {
+			buffer.location = gl.getAttribLocation(program, `a_${name}`);
+			buffer.ref = gl.createBuffer()!;
+
+			gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref);
+			gl.enableVertexAttribArray(buffer.location);
+			gl.vertexAttribPointer(
+				buffer.location,
+				buffer.size,
+				gl.FLOAT,
+				false,
+				0,
+				0,
+			);
+		}
+
+		return buffers;
+	}
+
+	private updateBuffers() {
+		const { buffers } = this;
+
+		for (const name of Object.keys(buffers)) {
+			this.setBuffer(name);
+		}
+	}
+
+	private setBuffer(name: string, value?) {
+		const { gl, buffers } = this;
+		const buffer = buffers[name];
+
+		buffer.value = new Float32Array(value ?? buffer.value);
+
+		gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref);
+		gl.bufferData(gl.ARRAY_BUFFER, buffer.value, gl.STATIC_DRAW);
+	}
+
+	private initUniforms(): SnowfallEffect['uniforms'] {
+		const { gl, program } = this;
+		const uniforms = this.INITIAL_UNIFORMS() as unknown as SnowfallEffect['uniforms'];
+
+		for (const [name, uniform] of Object.entries(uniforms)) {
+			uniform.location = gl.getUniformLocation(program, `u_${name}`)!;
+		}
+
+		return uniforms;
+	}
+
+	private updateUniforms() {
+		const { uniforms } = this;
+
+		for (const name of Object.keys(uniforms)) {
+			this.setUniform(name);
+		}
+	}
+
+	private setUniform(name: string, value?) {
+		const { gl, uniforms } = this;
+		const uniform = uniforms[name];
+		const setter = this.UNIFORM_SETTERS[uniform.type];
+		const isMatrix = /^mat[2-4]$/i.test(uniform.type);
+
+		uniform.value = value ?? uniform.value;
+
+		if (isMatrix) {
+			gl[setter](uniform.location, false, uniform.value);
+		} else {
+			gl[setter](uniform.location, uniform.value);
+		}
+	}
+
+	private initTexture() {
+		const { gl } = this;
+		const texture = gl.createTexture();
+		if (texture == null) throw new Error('Failed to create texture');
+		const image = new Image();
+
+		gl.bindTexture(gl.TEXTURE_2D, texture);
+		gl.texImage2D(
+			gl.TEXTURE_2D,
+			0,
+			gl.RGBA,
+			1,
+			1,
+			0,
+			gl.RGBA,
+			gl.UNSIGNED_BYTE,
+			new Uint8Array([0, 0, 0, 0]),
+		);
+
+		image.onload = () => {
+			gl.bindTexture(gl.TEXTURE_2D, texture);
+			gl.texImage2D(
+				gl.TEXTURE_2D,
+				0,
+				gl.RGBA,
+				gl.RGBA,
+				gl.UNSIGNED_BYTE,
+				image,
+			);
+			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+		};
+
+		image.src = this.snowflake;
+
+		return texture;
+	}
+
+	private initSnowflakes(vw: number, vh: number, dpi: number) {
+		const position: number[] = [];
+		const color: number[] = [];
+		const size: number[] = [];
+		const rotation: number[] = [];
+		const speed: number[] = [];
+
+		const height = 1 / this.density;
+		const width = (vw / vh) * height;
+		const depth = this.depth;
+		const count = this.count;
+		const length = (vw / vh) * count;
+
+		for (let i = 0; i < length; ++i) {
+			position.push(
+				-width + Math.random() * width * 2,
+				-height + Math.random() * height * 2,
+				Math.random() * depth * 2,
+			);
+
+			speed.push(1 + Math.random(), 1 + Math.random(), Math.random() * 10);
+
+			rotation.push(
+				Math.random() * 2 * Math.PI,
+				Math.random() * 20,
+				Math.random() * 10,
+			);
+
+			color.push(...this.color, 0.1 + Math.random() * this.opacity);
+			//size.push((this.size * Math.random() * this.size * vh * dpi) / 1000);
+			size.push((this.size * vh * dpi) / 1000);
+		}
+
+		this.setUniform('worldSize', [width, height, depth]);
+
+		this.setBuffer('position', position);
+		this.setBuffer('color', color);
+		this.setBuffer('rotation', rotation);
+		this.setBuffer('size', size);
+		this.setBuffer('speed', speed);
+	}
+
+	private setProjection(aspect: number) {
+		const { camera } = this;
+
+		camera.aspect = aspect;
+
+		const fovRad = (camera.fov * Math.PI) / 180;
+		const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad);
+		const rangeInv = 1.0 / (camera.near - camera.far);
+
+		const m0 = f / camera.aspect;
+		const m5 = f;
+		const m10 = (camera.near + camera.far) * rangeInv;
+		const m11 = -1;
+		const m14 = camera.near * camera.far * rangeInv * 2 + camera.z;
+		const m15 = camera.z;
+
+		return [m0, 0, 0, 0, 0, m5, 0, 0, 0, 0, m10, m11, 0, 0, m14, m15];
+	}
+
+	public render() {
+		const { gl } = this;
+
+		gl.enable(gl.BLEND);
+		gl.enable(gl.CULL_FACE);
+		gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
+		gl.disable(gl.DEPTH_TEST);
+
+		this.updateBuffers();
+		this.updateUniforms();
+		this.resize(true);
+
+		this.time = {
+			start: window.performance.now(),
+			previous: window.performance.now(),
+		};
+
+		if (this.raf) window.cancelAnimationFrame(this.raf);
+		this.raf = window.requestAnimationFrame(this.update);
+
+		return this;
+	}
+
+	private resize(updateSnowflakes = false) {
+		const { canvas, gl } = this;
+		const vw = canvas.offsetWidth;
+		const vh = canvas.offsetHeight;
+		const aspect = vw / vh;
+		const dpi = window.devicePixelRatio;
+
+		canvas.width = vw * dpi;
+		canvas.height = vh * dpi;
+
+		gl.viewport(0, 0, vw * dpi, vh * dpi);
+		gl.clearColor(0, 0, 0, 0);
+
+		if (updateSnowflakes === true) {
+			this.initSnowflakes(vw, vh, dpi);
+		}
+
+		this.setUniform('projection', this.setProjection(aspect));
+	}
+
+	private update(timestamp: number) {
+		const { gl, buffers, wind } = this;
+		const elapsed = (timestamp - this.time.start) * this.speed;
+		const delta = timestamp - this.time.previous;
+
+		gl.clear(gl.COLOR_BUFFER_BIT);
+		gl.drawArrays(
+			gl.POINTS,
+			0,
+			buffers.position.value.length / buffers.position.size,
+		);
+
+		if (Math.random() > 0.995) {
+			wind.target =
+				(wind.min + Math.random() * (wind.max - wind.min)) *
+				(Math.random() > 0.5 ? -1 : 1);
+		}
+
+		wind.force += (wind.target - wind.force) * wind.easing;
+		wind.current += wind.force * (delta * 0.2);
+
+		this.setUniform('wind', wind.current);
+		this.setUniform('time', elapsed);
+
+		this.time.previous = timestamp;
+
+		this.raf = window.requestAnimationFrame(this.update);
+	}
+}
diff --git a/packages/frontend/src/utility/sound.ts b/packages/frontend/src/utility/sound.ts
new file mode 100644
index 0000000000..436c2b75f0
--- /dev/null
+++ b/packages/frontend/src/utility/sound.ts
@@ -0,0 +1,258 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { SoundStore } from '@/preferences/def.js';
+import { prefer } from '@/preferences.js';
+import { PREF_DEF } from '@/preferences/def.js';
+
+let ctx: AudioContext;
+const cache = new Map<string, AudioBuffer>();
+let canPlay = true;
+
+export const soundsTypes = [
+	// 音声なし
+	null,
+
+	// ドライブの音声
+	'_driveFile_',
+
+	// プリインストール
+	'syuilo/n-aec',
+	'syuilo/n-aec-4va',
+	'syuilo/n-aec-4vb',
+	'syuilo/n-aec-8va',
+	'syuilo/n-aec-8vb',
+	'syuilo/n-cea',
+	'syuilo/n-cea-4va',
+	'syuilo/n-cea-4vb',
+	'syuilo/n-cea-8va',
+	'syuilo/n-cea-8vb',
+	'syuilo/n-eca',
+	'syuilo/n-eca-4va',
+	'syuilo/n-eca-4vb',
+	'syuilo/n-eca-8va',
+	'syuilo/n-eca-8vb',
+	'syuilo/n-ea',
+	'syuilo/n-ea-4va',
+	'syuilo/n-ea-4vb',
+	'syuilo/n-ea-8va',
+	'syuilo/n-ea-8vb',
+	'syuilo/n-ea-harmony',
+	'syuilo/up',
+	'syuilo/down',
+	'syuilo/pope1',
+	'syuilo/pope2',
+	'syuilo/waon',
+	'syuilo/popo',
+	'syuilo/triple',
+	'syuilo/bubble1',
+	'syuilo/bubble2',
+	'syuilo/poi1',
+	'syuilo/poi2',
+	'syuilo/pirori',
+	'syuilo/pirori-wet',
+	'syuilo/pirori-square-wet',
+	'syuilo/square-pico',
+	'syuilo/reverved',
+	'syuilo/ryukyu',
+	'syuilo/kick',
+	'syuilo/snare',
+	'syuilo/queue-jammed',
+	'aisha/1',
+	'aisha/2',
+	'aisha/3',
+	'noizenecio/kick_gaba1',
+	'noizenecio/kick_gaba2',
+	'noizenecio/kick_gaba3',
+	'noizenecio/kick_gaba4',
+	'noizenecio/kick_gaba5',
+	'noizenecio/kick_gaba6',
+	'noizenecio/kick_gaba7',
+] as const;
+
+export const operationTypes = [
+	'noteMy',
+	'note',
+	'notification',
+	'reaction',
+] as const;
+
+/** サウンドの種類 */
+export type SoundType = typeof soundsTypes[number];
+
+/** スプライトの種類 */
+export type OperationType = typeof operationTypes[number];
+
+/**
+ * 音声を読み込む
+ * @param url url
+ * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
+ */
+export async function loadAudio(url: string, options?: { useCache?: boolean; }) {
+	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+	if (ctx == null) {
+		ctx = new AudioContext();
+
+		window.addEventListener('beforeunload', () => {
+			ctx.close();
+		});
+	}
+	if (options?.useCache ?? true) {
+		if (cache.has(url)) {
+			return cache.get(url) as AudioBuffer;
+		}
+	}
+
+	let response: Response;
+
+	try {
+		response = await fetch(url);
+	} catch (err) {
+		return;
+	}
+
+	const arrayBuffer = await response.arrayBuffer();
+	const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
+
+	if (options?.useCache ?? true) {
+		cache.set(url, audioBuffer);
+	}
+
+	return audioBuffer;
+}
+
+/**
+ * 既定のスプライトを再生する
+ * @param type スプライトの種類を指定
+ */
+export function playMisskeySfx(operationType: OperationType) {
+	const sound = prefer.s[`sound.on.${operationType}`];
+	playMisskeySfxFile(sound).then((succeed) => {
+		if (!succeed && sound.type === '_driveFile_') {
+			// ドライブファイルが存在しない場合はデフォルトのサウンドを再生する
+			const soundName = PREF_DEF[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>;
+			if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`);
+			playMisskeySfxFileInternal({
+				type: soundName,
+				volume: sound.volume,
+			});
+		}
+	});
+}
+
+/**
+ * サウンド設定形式で指定された音声を再生する
+ * @param soundStore サウンド設定
+ */
+export async function playMisskeySfxFile(soundStore: SoundStore): Promise<boolean> {
+	// 連続して再生しない
+	if (!canPlay) return false;
+	// ユーザーアクティベーションが必要な場合はそれがない場合は再生しない
+	if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return false;
+	// サウンドがない場合は再生しない
+	if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return false;
+
+	canPlay = false;
+	return await playMisskeySfxFileInternal(soundStore).finally(() => {
+		// ごく短時間に音が重複しないように
+		setTimeout(() => {
+			canPlay = true;
+		}, 25);
+	});
+}
+
+async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise<boolean> {
+	if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
+		return false;
+	}
+	const masterVolume = prefer.s['sound.masterVolume'];
+	if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
+		return true; // ミュート時は成功として扱う
+	}
+	const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
+	const buffer = await loadAudio(url).catch(() => {
+		return undefined;
+	});
+	if (!buffer) return false;
+	const volume = soundStore.volume * masterVolume;
+	createSourceNode(buffer, { volume }).soundSource.start();
+	return true;
+}
+
+export async function playUrl(url: string, opts: {
+	volume?: number;
+	pan?: number;
+	playbackRate?: number;
+}) {
+	if (opts.volume === 0) {
+		return;
+	}
+	const buffer = await loadAudio(url);
+	if (!buffer) return;
+	createSourceNode(buffer, opts).soundSource.start();
+}
+
+export function createSourceNode(buffer: AudioBuffer, opts: {
+	volume?: number;
+	pan?: number;
+	playbackRate?: number;
+}): {
+		soundSource: AudioBufferSourceNode;
+		panNode: StereoPannerNode;
+		gainNode: GainNode;
+	} {
+	const panNode = ctx.createStereoPanner();
+	panNode.pan.value = opts.pan ?? 0;
+
+	const gainNode = ctx.createGain();
+
+	gainNode.gain.value = opts.volume ?? 1;
+
+	const soundSource = ctx.createBufferSource();
+	soundSource.buffer = buffer;
+	soundSource.playbackRate.value = opts.playbackRate ?? 1;
+	soundSource
+		.connect(panNode)
+		.connect(gainNode)
+		.connect(ctx.destination);
+
+	return { soundSource, panNode, gainNode };
+}
+
+/**
+ * 音声の長さをミリ秒で取得する
+ * @param file ファイルのURL（ドライブIDではない）
+ */
+export async function getSoundDuration(file: string): Promise<number> {
+	const audioEl = document.createElement('audio');
+	audioEl.src = file;
+	return new Promise((resolve) => {
+		const si = setInterval(() => {
+			if (audioEl.readyState > 0) {
+				resolve(audioEl.duration * 1000);
+				clearInterval(si);
+				audioEl.remove();
+			}
+		}, 100);
+	});
+}
+
+/**
+ * ミュートすべきかどうかを判断する
+ */
+export function isMute(): boolean {
+	if (prefer.s['sound.notUseSound']) {
+		// サウンドを出力しない
+		return true;
+	}
+
+	// noinspection RedundantIfStatementJS
+	if (prefer.s['sound.useSoundOnlyWhenActive'] && document.visibilityState === 'hidden') {
+		// ブラウザがアクティブな時のみサウンドを出力する
+		return true;
+	}
+
+	return false;
+}
diff --git a/packages/frontend/src/utility/sticky-sidebar.ts b/packages/frontend/src/utility/sticky-sidebar.ts
new file mode 100644
index 0000000000..50f1e6ecc8
--- /dev/null
+++ b/packages/frontend/src/utility/sticky-sidebar.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class StickySidebar {
+	private lastScrollTop = 0;
+	private container: HTMLElement;
+	private el: HTMLElement;
+	private spacer: HTMLElement;
+	private marginTop: number;
+	private isTop = false;
+	private isBottom = false;
+	private offsetTop: number;
+	private globalHeaderHeight = 59;
+
+	constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) {
+		this.container = container;
+		this.el = this.container.children[0] as HTMLElement;
+		this.el.style.position = 'sticky';
+		this.spacer = document.createElement('div');
+		this.container.prepend(this.spacer);
+		this.marginTop = marginTop;
+		this.offsetTop = this.container.getBoundingClientRect().top;
+		this.globalHeaderHeight = globalHeaderHeight;
+	}
+
+	public calc(scrollTop: number) {
+		if (scrollTop > this.lastScrollTop) { // downscroll
+			const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight);
+			this.el.style.bottom = null;
+			this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`;
+
+			this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight);
+
+			if (this.isTop) {
+				this.isTop = false;
+				this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`;
+			}
+		} else { // upscroll
+			const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight;
+			this.el.style.top = null;
+			this.el.style.bottom = `${-overflow}px`;
+
+			this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop;
+
+			if (this.isBottom) {
+				this.isBottom = false;
+				this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`;
+			}
+		}
+
+		this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
+	}
+}
diff --git a/packages/frontend/src/utility/stream-mock.ts b/packages/frontend/src/utility/stream-mock.ts
new file mode 100644
index 0000000000..9b1b368de4
--- /dev/null
+++ b/packages/frontend/src/utility/stream-mock.ts
@@ -0,0 +1,81 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { EventEmitter } from 'eventemitter3';
+import * as Misskey from 'misskey-js';
+import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js';
+
+type AnyOf<T extends Record<any, any>> = T[keyof T];
+type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never;
+
+/**
+ * Websocket無効化時に使うStreamのモック（なにもしない）
+ */
+export class StreamMock extends EventEmitter<StreamEvents> implements IStream {
+	public readonly state = 'initializing';
+
+	constructor(...args: ConstructorParameters<typeof Misskey.Stream>) {
+		super();
+		// do nothing
+	}
+
+	public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock<Channels[C]> {
+		return new ChannelConnectionMock(this, channel, name);
+	}
+
+	public removeSharedConnection(connection: any): void {
+		// do nothing
+	}
+
+	public removeSharedConnectionPool(pool: any): void {
+		// do nothing
+	}
+
+	public disconnectToChannel(): void {
+		// do nothing
+	}
+
+	public send(typeOrPayload: string): void;
+	public send(typeOrPayload: string, payload: any): void;
+	public send(typeOrPayload: Record<string, any> | any[]): void;
+	public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
+		// do nothing
+	}
+
+	public ping(): void {
+		// do nothing
+	}
+
+	public heartbeat(): void {
+		// do nothing
+	}
+
+	public close(): void {
+		// do nothing
+	}
+}
+
+class ChannelConnectionMock<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
+	public id = '';
+	public name?: string; // for debug
+	public inCount = 0; // for debug
+	public outCount = 0; // for debug
+	public channel: string;
+
+	constructor(stream: IStream, ...args: OmitFirst<ConstructorParameters<typeof Misskey.ChannelConnection<Channel>>>) {
+		super();
+
+		this.channel = args[0];
+		this.name = args[1];
+	}
+
+	public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void {
+		// do nothing
+	}
+
+	public dispose(): void {
+		// do nothing
+	}
+}
diff --git a/packages/frontend/src/utility/test-utils.ts b/packages/frontend/src/utility/test-utils.ts
new file mode 100644
index 0000000000..52bb2d94e0
--- /dev/null
+++ b/packages/frontend/src/utility/test-utils.ts
@@ -0,0 +1,9 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export async function tick(): Promise<void> {
+	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+	await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never);
+}
diff --git a/packages/frontend/src/utility/theme-editor.ts b/packages/frontend/src/utility/theme-editor.ts
new file mode 100644
index 0000000000..0206e378bf
--- /dev/null
+++ b/packages/frontend/src/utility/theme-editor.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { v4 as uuid } from 'uuid';
+
+import { themeProps } from './theme.js';
+import type { Theme } from './theme.js';
+
+export type Default = null;
+export type Color = string;
+export type FuncName = 'alpha' | 'darken' | 'lighten';
+export type Func = { type: 'func'; name: FuncName; arg: number; value: string; };
+export type RefProp = { type: 'refProp'; key: string; };
+export type RefConst = { type: 'refConst'; key: string; };
+export type Css = { type: 'css'; value: string; };
+
+export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default;
+
+export type ThemeViewModel = [ string, ThemeValue ][];
+
+export const fromThemeString = (str?: string) : ThemeValue => {
+	if (!str) return null;
+	if (str.startsWith(':')) {
+		const parts = str.slice(1).split('<');
+		const name = parts[0] as FuncName;
+		const arg = parseFloat(parts[1]);
+		const value = parts[2].startsWith('@') ? parts[2].slice(1) : '';
+		return { type: 'func', name, arg, value };
+	} else if (str.startsWith('@')) {
+		return {
+			type: 'refProp',
+			key: str.slice(1),
+		};
+	} else if (str.startsWith('$')) {
+		return {
+			type: 'refConst',
+			key: str.slice(1),
+		};
+	} else if (str.startsWith('"')) {
+		return {
+			type: 'css',
+			value: str.substring(1).trim(),
+		};
+	} else {
+		return str;
+	}
+};
+
+export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => {
+	if (typeof value === 'string') return value;
+	switch (value.type) {
+		case 'func': return `:${value.name}<${value.arg}<@${value.value}`;
+		case 'refProp': return `@${value.key}`;
+		case 'refConst': return `$${value.key}`;
+		case 'css': return `" ${value.value}`;
+	}
+};
+
+export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => {
+	const props = { } as { [key: string]: string };
+	for (const [key, value] of vm) {
+		if (value === null) continue;
+		props[key] = toThemeString(value);
+	}
+
+	return {
+		id: uuid(),
+		name, desc, author, props, base,
+	};
+};
+
+export const convertToViewModel = (theme: Theme): ThemeViewModel => {
+	const vm: ThemeViewModel = [];
+	// プロパティの登録
+	vm.push(...themeProps.map(key => [key, fromThemeString(theme.props[key])] as [ string, ThemeValue ]));
+
+	// 定数の登録
+	const consts = Object
+		.keys(theme.props)
+		.filter(k => k.startsWith('$'))
+		.map(k => [k, fromThemeString(theme.props[k])] as [ string, ThemeValue ]);
+
+	vm.push(...consts);
+	return vm;
+};
diff --git a/packages/frontend/src/utility/theme.ts b/packages/frontend/src/utility/theme.ts
new file mode 100644
index 0000000000..851ba41e61
--- /dev/null
+++ b/packages/frontend/src/utility/theme.ts
@@ -0,0 +1,189 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref } from 'vue';
+import tinycolor from 'tinycolor2';
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
+import JSON5 from 'json5';
+import { deepClone } from './clone.js';
+import type { BundledTheme } from 'shiki/themes';
+import { globalEvents } from '@/events.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { addTheme, getThemes } from '@/theme-store.js';
+
+export type Theme = {
+	id: string;
+	name: string;
+	author: string;
+	desc?: string;
+	base?: 'dark' | 'light';
+	props: Record<string, string>;
+	codeHighlighter?: {
+		base: BundledTheme;
+		overrides?: Record<string, any>;
+	} | {
+		base: '_none_';
+		overrides: Record<string, any>;
+	};
+};
+
+export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
+
+export const getBuiltinThemes = () => Promise.all(
+	[
+		'l-light',
+		'l-coffee',
+		'l-apricot',
+		'l-rainy',
+		'l-botanical',
+		'l-vivid',
+		'l-cherry',
+		'l-sushi',
+		'l-u0',
+
+		'd-dark',
+		'd-persimmon',
+		'd-astro',
+		'd-future',
+		'd-botanical',
+		'd-green-lime',
+		'd-green-orange',
+		'd-cherry',
+		'd-ice',
+		'd-u0',
+	].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
+);
+
+export const getBuiltinThemesRef = () => {
+	const builtinThemes = ref<Theme[]>([]);
+	getBuiltinThemes().then(themes => builtinThemes.value = themes);
+	return builtinThemes;
+};
+
+let timeout: number | null = null;
+
+export function applyTheme(theme: Theme, persist = true) {
+	if (timeout) window.clearTimeout(timeout);
+
+	document.documentElement.classList.add('_themeChanging_');
+
+	timeout = window.setTimeout(() => {
+		document.documentElement.classList.remove('_themeChanging_');
+	}, 1000);
+
+	const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
+
+	document.documentElement.dataset.colorScheme = colorScheme;
+
+	// Deep copy
+	const _theme = deepClone(theme);
+
+	if (_theme.base) {
+		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
+		if (base) _theme.props = Object.assign({}, base.props, _theme.props);
+	}
+
+	const props = compile(_theme);
+
+	for (const tag of document.head.children) {
+		if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+			tag.setAttribute('content', props['htmlThemeColor']);
+			break;
+		}
+	}
+
+	for (const [k, v] of Object.entries(props)) {
+		document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
+	}
+
+	document.documentElement.style.setProperty('color-scheme', colorScheme);
+
+	if (persist) {
+		miLocalStorage.setItem('theme', JSON.stringify(props));
+		miLocalStorage.setItem('themeId', theme.id);
+		miLocalStorage.setItem('colorScheme', colorScheme);
+	}
+
+	// 色計算など再度行えるようにクライアント全体に通知
+	globalEvents.emit('themeChanged');
+}
+
+function compile(theme: Theme): Record<string, string> {
+	function getColor(val: string): tinycolor.Instance {
+		if (val[0] === '@') { // ref (prop)
+			return getColor(theme.props[val.substring(1)]);
+		} else if (val[0] === '$') { // ref (const)
+			return getColor(theme.props[val]);
+		} else if (val[0] === ':') { // func
+			const parts = val.split('<');
+			const func = parts.shift().substring(1);
+			const arg = parseFloat(parts.shift());
+			const color = getColor(parts.join('<'));
+
+			switch (func) {
+				case 'darken': return color.darken(arg);
+				case 'lighten': return color.lighten(arg);
+				case 'alpha': return color.setAlpha(arg);
+				case 'hue': return color.spin(arg);
+				case 'saturate': return color.saturate(arg);
+			}
+		}
+
+		// other case
+		return tinycolor(val);
+	}
+
+	const props = {};
+
+	for (const [k, v] of Object.entries(theme.props)) {
+		if (k.startsWith('$')) continue; // ignore const
+
+		props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
+	}
+
+	return props;
+}
+
+function genValue(c: tinycolor.Instance): string {
+	return c.toRgbString();
+}
+
+export function validateTheme(theme: Record<string, any>): boolean {
+	if (theme.id == null || typeof theme.id !== 'string') return false;
+	if (theme.name == null || typeof theme.name !== 'string') return false;
+	if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
+	if (theme.props == null || typeof theme.props !== 'object') return false;
+	return true;
+}
+
+export function parseThemeCode(code: string): Theme {
+	let theme;
+
+	try {
+		theme = JSON5.parse(code);
+	} catch (err) {
+		throw new Error('Failed to parse theme json');
+	}
+	if (!validateTheme(theme)) {
+		throw new Error('This theme is invaild');
+	}
+	if (getThemes().some(t => t.id === theme.id)) {
+		throw new Error('This theme is already installed');
+	}
+
+	return theme;
+}
+
+export function previewTheme(code: string): void {
+	const theme = parseThemeCode(code);
+	if (theme) applyTheme(theme, false);
+}
+
+export async function installTheme(code: string): Promise<void> {
+	const theme = parseThemeCode(code);
+	if (!theme) return;
+	await addTheme(theme);
+}
diff --git a/packages/frontend/src/utility/time.ts b/packages/frontend/src/utility/time.ts
new file mode 100644
index 0000000000..275b67ed00
--- /dev/null
+++ b/packages/frontend/src/utility/time.ts
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const dateTimeIntervals = {
+	'day': 86400000,
+	'hour': 3600000,
+	'ms': 1,
+};
+
+export function dateUTC(time: number[]): Date {
+	const d =
+		time.length === 2 ? Date.UTC(time[0], time[1])
+		: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
+		: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
+		: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
+		: time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
+		: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
+		: null;
+
+	if (!d) throw new Error('wrong number of arguments');
+
+	return new Date(d);
+}
+
+export function isTimeSame(a: Date, b: Date): boolean {
+	return a.getTime() === b.getTime();
+}
+
+export function isTimeBefore(a: Date, b: Date): boolean {
+	return (a.getTime() - b.getTime()) < 0;
+}
+
+export function isTimeAfter(a: Date, b: Date): boolean {
+	return (a.getTime() - b.getTime()) > 0;
+}
+
+export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
+	return new Date(x.getTime() + (value * dateTimeIntervals[span]));
+}
+
+export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
+	return new Date(x.getTime() - (value * dateTimeIntervals[span]));
+}
diff --git a/packages/frontend/src/utility/timezones.ts b/packages/frontend/src/utility/timezones.ts
new file mode 100644
index 0000000000..c7582e06da
--- /dev/null
+++ b/packages/frontend/src/utility/timezones.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const timezones = [{
+	name: 'UTC',
+	abbrev: 'UTC',
+	offset: 0,
+}, {
+	name: 'Europe/Berlin',
+	abbrev: 'CET',
+	offset: 60,
+}, {
+	name: 'Asia/Tokyo',
+	abbrev: 'JST',
+	offset: 540,
+}, {
+	name: 'Asia/Seoul',
+	abbrev: 'KST',
+	offset: 540,
+}, {
+	name: 'Asia/Shanghai',
+	abbrev: 'CST',
+	offset: 480,
+}, {
+	name: 'Australia/Sydney',
+	abbrev: 'AEST',
+	offset: 600,
+}, {
+	name: 'Australia/Darwin',
+	abbrev: 'ACST',
+	offset: 570,
+}, {
+	name: 'Australia/Perth',
+	abbrev: 'AWST',
+	offset: 480,
+}, {
+	name: 'America/New_York',
+	abbrev: 'EST',
+	offset: -300,
+}, {
+	name: 'America/Mexico_City',
+	abbrev: 'CST',
+	offset: -360,
+}, {
+	name: 'America/Phoenix',
+	abbrev: 'MST',
+	offset: -420,
+}, {
+	name: 'America/Los_Angeles',
+	abbrev: 'PST',
+	offset: -480,
+}];
diff --git a/packages/frontend/src/utility/touch.ts b/packages/frontend/src/utility/touch.ts
new file mode 100644
index 0000000000..adc2e4c093
--- /dev/null
+++ b/packages/frontend/src/utility/touch.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref } from 'vue';
+import { deviceKind } from '@/utility/device-kind.js';
+
+const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
+
+export let isTouchUsing = deviceKind === 'tablet' || deviceKind === 'smartphone';
+
+if (isTouchSupported && !isTouchUsing) {
+	window.addEventListener('touchstart', () => {
+		// maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも
+		// タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする
+		isTouchUsing = true;
+	}, { passive: true });
+}
+
+/** (MkHorizontalSwipe) 横スワイプ中か？ */
+export const isHorizontalSwipeSwiping = ref(false);
diff --git a/packages/frontend/src/utility/unison-reload.ts b/packages/frontend/src/utility/unison-reload.ts
new file mode 100644
index 0000000000..a24941d02e
--- /dev/null
+++ b/packages/frontend/src/utility/unison-reload.ts
@@ -0,0 +1,20 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// SafariがBroadcastChannel未実装なのでライブラリを使う
+import { BroadcastChannel } from 'broadcast-channel';
+
+export const reloadChannel = new BroadcastChannel<string | null>('reload');
+
+// BroadcastChannelを用いて、クライアントが一斉にreloadするようにします。
+export function unisonReload(path?: string) {
+	if (path !== undefined) {
+		reloadChannel.postMessage(path);
+		location.href = path;
+	} else {
+		reloadChannel.postMessage(null);
+		location.reload();
+	}
+}
diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts
new file mode 100644
index 0000000000..d105a318a7
--- /dev/null
+++ b/packages/frontend/src/utility/upload.ts
@@ -0,0 +1,162 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { reactive, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { v4 as uuid } from 'uuid';
+import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
+import { apiUrl } from '@@/js/config.js';
+import { getCompressionConfig } from './upload/compress-config.js';
+import { $i } from '@/account.js';
+import { alert } from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
+import { prefer } from '@/preferences.js';
+
+type Uploading = {
+	id: string;
+	name: string;
+	progressMax: number | undefined;
+	progressValue: number | undefined;
+	img: string;
+};
+export const uploads = ref<Uploading[]>([]);
+
+const mimeTypeMap = {
+	'image/webp': 'webp',
+	'image/jpeg': 'jpg',
+	'image/png': 'png',
+} as const;
+
+export function uploadFile(
+	file: File,
+	folder?: string | Misskey.entities.DriveFolder,
+	name?: string,
+	keepOriginal: boolean = prefer.s.keepOriginalUploading,
+): Promise<Misskey.entities.DriveFile> {
+	if ($i == null) throw new Error('Not logged in');
+
+	const _folder = typeof folder === 'string' ? folder : folder?.id;
+
+	if (file.size > instance.maxFileSize) {
+		alert({
+			type: 'error',
+			title: i18n.ts.failedToUpload,
+			text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
+		});
+		return Promise.reject();
+	}
+
+	return new Promise((resolve, reject) => {
+		const id = uuid();
+
+		const reader = new FileReader();
+		reader.onload = async (): Promise<void> => {
+			const filename = name ?? file.name ?? 'untitled';
+			const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
+
+			const ctx = reactive<Uploading>({
+				id,
+				name: prefer.s.keepOriginalFilename ? filename : id + extension,
+				progressMax: undefined,
+				progressValue: undefined,
+				img: window.URL.createObjectURL(file),
+			});
+
+			uploads.value.push(ctx);
+
+			const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
+			let resizedImage: Blob | undefined;
+			if (config) {
+				try {
+					const resized = await readAndCompressImage(file, config);
+					if (resized.size < file.size || file.type === 'image/webp') {
+						// The compression may not always reduce the file size
+						// (and WebP is not browser safe yet)
+						resizedImage = resized;
+					}
+					if (_DEV_) {
+						const saved = ((1 - resized.size / file.size) * 100).toFixed(2);
+						console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
+					}
+
+					ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name;
+				} catch (err) {
+					console.error('Failed to resize image', err);
+				}
+			}
+
+			const formData = new FormData();
+			formData.append('i', $i!.token);
+			formData.append('force', 'true');
+			formData.append('file', resizedImage ?? file);
+			formData.append('name', ctx.name);
+			if (_folder) formData.append('folderId', _folder);
+
+			const xhr = new XMLHttpRequest();
+			xhr.open('POST', apiUrl + '/drive/files/create', true);
+			xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => {
+				if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
+					// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
+					uploads.value = uploads.value.filter(x => x.id !== id);
+
+					if (xhr.status === 413) {
+						alert({
+							type: 'error',
+							title: i18n.ts.failedToUpload,
+							text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
+						});
+					} else if (ev.target?.response) {
+						const res = JSON.parse(ev.target.response);
+						if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
+							alert({
+								type: 'error',
+								title: i18n.ts.failedToUpload,
+								text: i18n.ts.cannotUploadBecauseInappropriate,
+							});
+						} else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
+							alert({
+								type: 'error',
+								title: i18n.ts.failedToUpload,
+								text: i18n.ts.cannotUploadBecauseNoFreeSpace,
+							});
+						} else {
+							alert({
+								type: 'error',
+								title: i18n.ts.failedToUpload,
+								text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
+							});
+						}
+					} else {
+						alert({
+							type: 'error',
+							title: 'Failed to upload',
+							text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
+						});
+					}
+
+					reject();
+					return;
+				}
+
+				const driveFile = JSON.parse(ev.target.response);
+
+				resolve(driveFile);
+
+				uploads.value = uploads.value.filter(x => x.id !== id);
+			}) as (ev: ProgressEvent<EventTarget>) => any;
+
+			xhr.upload.onprogress = ev => {
+				if (ev.lengthComputable) {
+					ctx.progressMax = ev.total;
+					ctx.progressValue = ev.loaded;
+				}
+			};
+
+			xhr.send(formData);
+		};
+		reader.readAsArrayBuffer(file);
+	});
+}
diff --git a/packages/frontend/src/utility/upload/compress-config.ts b/packages/frontend/src/utility/upload/compress-config.ts
new file mode 100644
index 0000000000..3046b7f518
--- /dev/null
+++ b/packages/frontend/src/utility/upload/compress-config.ts
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import isAnimated from 'is-file-animated';
+import { isWebpSupported } from './isWebpSupported.js';
+import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer';
+
+const compressTypeMap = {
+	'image/jpeg': { quality: 0.90, mimeType: 'image/webp' },
+	'image/png': { quality: 1, mimeType: 'image/webp' },
+	'image/webp': { quality: 0.90, mimeType: 'image/webp' },
+	'image/svg+xml': { quality: 1, mimeType: 'image/webp' },
+} as const;
+
+const compressTypeMapFallback = {
+	'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' },
+	'image/png': { quality: 1, mimeType: 'image/png' },
+	'image/webp': { quality: 0.85, mimeType: 'image/jpeg' },
+	'image/svg+xml': { quality: 1, mimeType: 'image/png' },
+} as const;
+
+export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfigWithConvertedOutput | undefined> {
+	const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
+	if (!imgConfig || await isAnimated(file)) {
+		return;
+	}
+
+	return {
+		maxWidth: 2048,
+		maxHeight: 2048,
+		debug: true,
+		...imgConfig,
+	};
+}
diff --git a/packages/frontend/src/utility/upload/isWebpSupported.ts b/packages/frontend/src/utility/upload/isWebpSupported.ts
new file mode 100644
index 0000000000..2511236ecc
--- /dev/null
+++ b/packages/frontend/src/utility/upload/isWebpSupported.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+let isWebpSupportedCache: boolean | undefined;
+export function isWebpSupported() {
+	if (isWebpSupportedCache === undefined) {
+		const canvas = document.createElement('canvas');
+		canvas.width = 1;
+		canvas.height = 1;
+		isWebpSupportedCache = canvas.toDataURL('image/webp').startsWith('data:image/webp');
+	}
+	return isWebpSupportedCache;
+}
diff --git a/packages/frontend/src/utility/use-chart-tooltip.ts b/packages/frontend/src/utility/use-chart-tooltip.ts
new file mode 100644
index 0000000000..bba64fc6ee
--- /dev/null
+++ b/packages/frontend/src/utility/use-chart-tooltip.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { onUnmounted, onDeactivated, ref } from 'vue';
+import * as os from '@/os.js';
+import MkChartTooltip from '@/components/MkChartTooltip.vue';
+
+export function useChartTooltip(opts: { position: 'top' | 'middle' } = { position: 'top' }) {
+	const tooltipShowing = ref(false);
+	const tooltipX = ref(0);
+	const tooltipY = ref(0);
+	const tooltipTitle = ref<string | null>(null);
+	const tooltipSeries = ref<{
+		backgroundColor: string;
+		borderColor: string;
+		text: string;
+	}[] | null>(null);
+	const { dispose: disposeTooltipComponent } = os.popup(MkChartTooltip, {
+		showing: tooltipShowing,
+		x: tooltipX,
+		y: tooltipY,
+		title: tooltipTitle,
+		series: tooltipSeries,
+	}, {});
+
+	onUnmounted(() => {
+		disposeTooltipComponent();
+	});
+
+	onDeactivated(() => {
+		tooltipShowing.value = false;
+	});
+
+	function handler(context) {
+		if (context.tooltip.opacity === 0) {
+			tooltipShowing.value = false;
+			return;
+		}
+
+		tooltipTitle.value = context.tooltip.title[0];
+		tooltipSeries.value = context.tooltip.body.map((b, i) => ({
+			backgroundColor: context.tooltip.labelColors[i].backgroundColor,
+			borderColor: context.tooltip.labelColors[i].borderColor,
+			text: b.lines[0],
+		}));
+
+		const rect = context.chart.canvas.getBoundingClientRect();
+
+		tooltipShowing.value = true;
+		tooltipX.value = rect.left + window.scrollX + context.tooltip.caretX;
+		if (opts.position === 'top') {
+			tooltipY.value = rect.top + window.scrollY;
+		} else if (opts.position === 'middle') {
+			tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY;
+		}
+	}
+
+	return {
+		handler,
+	};
+}
diff --git a/packages/frontend/src/utility/use-form.ts b/packages/frontend/src/utility/use-form.ts
new file mode 100644
index 0000000000..26cca839c3
--- /dev/null
+++ b/packages/frontend/src/utility/use-form.ts
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed, reactive, watch } from 'vue';
+import type { Reactive } from 'vue';
+
+function copy<T>(v: T): T {
+	return JSON.parse(JSON.stringify(v));
+}
+
+function unwrapReactive<T>(v: Reactive<T>): T {
+	return JSON.parse(JSON.stringify(v));
+}
+
+export function useForm<T extends Record<string, any>>(initialState: T, save: (newState: T) => Promise<void>) {
+	const currentState = reactive<T>(copy(initialState));
+	const previousState = reactive<T>(copy(initialState));
+
+	const modifiedStates = reactive<Record<keyof T, boolean>>({} as any);
+	for (const key in currentState) {
+		modifiedStates[key] = false;
+	}
+	const modified = computed(() => Object.values(modifiedStates).some(v => v));
+	const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length);
+
+	watch([currentState, previousState], () => {
+		for (const key in modifiedStates) {
+			modifiedStates[key] = currentState[key] !== previousState[key];
+		}
+	}, { deep: true });
+
+	async function _save() {
+		await save(unwrapReactive(currentState));
+		for (const key in currentState) {
+			previousState[key] = copy(currentState[key]);
+		}
+	}
+
+	function discard() {
+		for (const key in currentState) {
+			currentState[key] = copy(previousState[key]);
+		}
+	}
+
+	return {
+		state: currentState,
+		savedState: previousState,
+		modifiedStates,
+		modified,
+		modifiedCount,
+		save: _save,
+		discard,
+	};
+}
diff --git a/packages/frontend/src/utility/use-leave-guard.ts b/packages/frontend/src/utility/use-leave-guard.ts
new file mode 100644
index 0000000000..395c12a756
--- /dev/null
+++ b/packages/frontend/src/utility/use-leave-guard.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Ref } from 'vue';
+
+export function useLeaveGuard(enabled: Ref<boolean>) {
+	/* TODO
+	const setLeaveGuard = inject('setLeaveGuard');
+
+	if (setLeaveGuard) {
+		setLeaveGuard(async () => {
+			if (!enabled.value) return false;
+
+			const { canceled } = await os.confirm({
+				type: 'warning',
+				text: i18n.ts.leaveConfirm,
+			});
+
+			return canceled;
+		});
+	} else {
+		onBeforeRouteLeave(async (to, from) => {
+			if (!enabled.value) return true;
+
+			const { canceled } = await os.confirm({
+				type: 'warning',
+				text: i18n.ts.leaveConfirm,
+			});
+
+			return !canceled;
+		});
+	}
+	*/
+
+	/*
+	function onBeforeLeave(ev: BeforeUnloadEvent) {
+		if (enabled.value) {
+			ev.preventDefault();
+			ev.returnValue = '';
+		}
+	}
+
+	window.addEventListener('beforeunload', onBeforeLeave);
+	onUnmounted(() => {
+		window.removeEventListener('beforeunload', onBeforeLeave);
+	});
+	*/
+}
diff --git a/packages/frontend/src/utility/use-note-capture.ts b/packages/frontend/src/utility/use-note-capture.ts
new file mode 100644
index 0000000000..0bc10e90e4
--- /dev/null
+++ b/packages/frontend/src/utility/use-note-capture.ts
@@ -0,0 +1,124 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { onUnmounted } from 'vue';
+import type { Ref, ShallowRef } from 'vue';
+import * as Misskey from 'misskey-js';
+import { useStream } from '@/stream.js';
+import { $i } from '@/account.js';
+
+export function useNoteCapture(props: {
+	rootEl: ShallowRef<HTMLElement | undefined>;
+	note: Ref<Misskey.entities.Note>;
+	pureNote: Ref<Misskey.entities.Note>;
+	isDeletedRef: Ref<boolean>;
+}) {
+	const note = props.note;
+	const pureNote = props.pureNote;
+	const connection = $i ? useStream() : null;
+
+	function onStreamNoteUpdated(noteData): void {
+		const { type, id, body } = noteData;
+
+		if ((id !== note.value.id) && (id !== pureNote.value.id)) return;
+
+		switch (type) {
+			case 'reacted': {
+				const reaction = body.reaction;
+
+				if (body.emoji && !(body.emoji.name in note.value.reactionEmojis)) {
+					note.value.reactionEmojis[body.emoji.name] = body.emoji.url;
+				}
+
+				// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
+				const currentCount = (note.value.reactions || {})[reaction] || 0;
+
+				note.value.reactions[reaction] = currentCount + 1;
+				note.value.reactionCount += 1;
+
+				if ($i && (body.userId === $i.id)) {
+					note.value.myReaction = reaction;
+				}
+				break;
+			}
+
+			case 'unreacted': {
+				const reaction = body.reaction;
+
+				// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
+				const currentCount = (note.value.reactions || {})[reaction] || 0;
+
+				note.value.reactions[reaction] = Math.max(0, currentCount - 1);
+				note.value.reactionCount = Math.max(0, note.value.reactionCount - 1);
+				if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction];
+
+				if ($i && (body.userId === $i.id)) {
+					note.value.myReaction = null;
+				}
+				break;
+			}
+
+			case 'pollVoted': {
+				const choice = body.choice;
+
+				const choices = [...note.value.poll.choices];
+				choices[choice] = {
+					...choices[choice],
+					votes: choices[choice].votes + 1,
+					...($i && (body.userId === $i.id) ? {
+						isVoted: true,
+					} : {}),
+				};
+
+				note.value.poll.choices = choices;
+				break;
+			}
+
+			case 'deleted': {
+				props.isDeletedRef.value = true;
+				break;
+			}
+		}
+	}
+
+	function capture(withHandler = false): void {
+		if (connection) {
+			// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+			connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id });
+			if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id });
+			if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
+		}
+	}
+
+	function decapture(withHandler = false): void {
+		if (connection) {
+			connection.send('un', {
+				id: note.value.id,
+			});
+			if (pureNote.value.id !== note.value.id) {
+				connection.send('un', {
+					id: pureNote.value.id,
+				});
+			}
+			if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
+		}
+	}
+
+	function onStreamConnected() {
+		capture(false);
+	}
+
+	capture(true);
+	if (connection) {
+		connection.on('_connected_', onStreamConnected);
+	}
+
+	onUnmounted(() => {
+		decapture(true);
+		if (connection) {
+			connection.off('_connected_', onStreamConnected);
+		}
+	});
+}
diff --git a/packages/frontend/src/utility/use-tooltip.ts b/packages/frontend/src/utility/use-tooltip.ts
new file mode 100644
index 0000000000..d9ddfc8b5d
--- /dev/null
+++ b/packages/frontend/src/utility/use-tooltip.ts
@@ -0,0 +1,106 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref, watch, onUnmounted } from 'vue';
+import type { Ref } from 'vue';
+
+export function useTooltip(
+	elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>,
+	onShow: (showing: Ref<boolean>) => void,
+	delay = 300,
+): void {
+	let isHovering = false;
+
+	// iOS(Androidも？)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ
+	// 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる
+	// TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...？
+	let shouldIgnoreMouseover = false;
+
+	let timeoutId: number;
+
+	let changeShowingState: (() => void) | null;
+
+	let autoHidingTimer;
+
+	const open = () => {
+		close();
+		if (!isHovering) return;
+		if (elRef.value == null) return;
+		const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el;
+		if (!document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため
+
+		const showing = ref(true);
+		onShow(showing);
+		changeShowingState = () => {
+			showing.value = false;
+		};
+
+		autoHidingTimer = window.setInterval(() => {
+			if (elRef.value == null || !document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) {
+				if (!isHovering) return;
+				isHovering = false;
+				window.clearTimeout(timeoutId);
+				close();
+				window.clearInterval(autoHidingTimer);
+			}
+		}, 1000);
+	};
+
+	const close = () => {
+		if (changeShowingState != null) {
+			changeShowingState();
+			changeShowingState = null;
+		}
+	};
+
+	const onMouseover = () => {
+		if (isHovering) return;
+		if (shouldIgnoreMouseover) return;
+		isHovering = true;
+		timeoutId = window.setTimeout(open, delay);
+	};
+
+	const onMouseleave = () => {
+		if (!isHovering) return;
+		isHovering = false;
+		window.clearTimeout(timeoutId);
+		window.clearInterval(autoHidingTimer);
+		close();
+	};
+
+	const onTouchstart = () => {
+		shouldIgnoreMouseover = true;
+		if (isHovering) return;
+		isHovering = true;
+		timeoutId = window.setTimeout(open, delay);
+	};
+
+	const onTouchend = () => {
+		if (!isHovering) return;
+		isHovering = false;
+		window.clearTimeout(timeoutId);
+		window.clearInterval(autoHidingTimer);
+		close();
+	};
+
+	const stop = watch(elRef, () => {
+		if (elRef.value) {
+			stop();
+			const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el;
+			el.addEventListener('mouseover', onMouseover, { passive: true });
+			el.addEventListener('mouseleave', onMouseleave, { passive: true });
+			el.addEventListener('touchstart', onTouchstart, { passive: true });
+			el.addEventListener('touchend', onTouchend, { passive: true });
+			el.addEventListener('click', close, { passive: true });
+		}
+	}, {
+		immediate: true,
+		flush: 'post',
+	});
+
+	onUnmounted(() => {
+		close();
+	});
+}
diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue
index cf1110da2b..d911e71ab2 100644
--- a/packages/frontend/src/widgets/WidgetActivity.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.vue
@@ -25,8 +25,8 @@ import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
 import XCalendar from './WidgetActivity.calendar.vue';
 import XChart from './WidgetActivity.chart.vue';
-import type { GetFormResultType } from '@/scripts/form.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import type { GetFormResultType } from '@/utility/form.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue
index 9b04c463ba..29e21ee6c3 100644
--- a/packages/frontend/src/widgets/WidgetAichan.vue
+++ b/packages/frontend/src/widgets/WidgetAichan.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, onUnmounted, shallowRef } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 
 const name = 'ai';
 
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index 80573d2dc4..b49041158f 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -23,10 +23,10 @@ import { ref } from 'vue';
 import { Interpreter, Parser, utils } from '@syuilo/aiscript';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
index f0f81a4a89..fb9dea1847 100644
--- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue
+++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
@@ -18,14 +18,14 @@ import type { Ref } from 'vue';
 import { Interpreter, Parser } from '@syuilo/aiscript';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { $i } from '@/account.js';
 import MkAsUi from '@/components/MkAsUi.vue';
 import MkContainer from '@/components/MkContainer.vue';
-import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
-import type { AsUiComponent, AsUiRoot } from '@/scripts/aiscript/ui.js';
+import { registerAsUiLib } from '@/aiscript/ui.js';
+import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
 
 const name = 'aiscriptApp';
 
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index 98e186f836..8c7507ef44 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -28,9 +28,9 @@ import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
 import { $i } from '@/account.js';
diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue
index 3e455bee3b..3f0f9eb9fd 100644
--- a/packages/frontend/src/widgets/WidgetButton.vue
+++ b/packages/frontend/src/widgets/WidgetButton.vue
@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { Interpreter, Parser } from '@syuilo/aiscript';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { $i } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
 
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index 94169d5e40..54f78469b2 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { i18n } from '@/i18n.js';
 import { useInterval } from '@@/js/use-interval.js';
 
diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue
index 2b3663d35b..87ffd3d732 100644
--- a/packages/frontend/src/widgets/WidgetClicker.vue
+++ b/packages/frontend/src/widgets/WidgetClicker.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkClickerGame from '@/components/MkClickerGame.vue';
 
diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue
index 2d3f57a92f..826ecf6e02 100644
--- a/packages/frontend/src/widgets/WidgetClock.vue
+++ b/packages/frontend/src/widgets/WidgetClock.vue
@@ -32,11 +32,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkAnalogClock from '@/components/MkAnalogClock.vue';
 import MkDigitalClock from '@/components/MkDigitalClock.vue';
-import { timezones } from '@/scripts/timezones.js';
+import { timezones } from '@/utility/timezones.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'clock';
diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue
index 6e0c9e6dfc..d79ec79d4f 100644
--- a/packages/frontend/src/widgets/WidgetDigitalClock.vue
+++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
-import { timezones } from '@/scripts/timezones.js';
+import type { GetFormResultType } from '@/utility/form.js';
+import { timezones } from '@/utility/timezones.js';
 import MkDigitalClock from '@/components/MkDigitalClock.vue';
 
 const name = 'digitalClock';
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index 6e56c9115e..ca6f27bd09 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -30,12 +30,12 @@ import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
 
 const name = 'federation';
diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
index aee9066731..c6724127fa 100644
--- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
@@ -22,13 +22,13 @@ import { shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkTagCloud from '@/components/MkTagCloud.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const name = 'instanceCloud';
 
diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
index 69832332b1..8d721298d5 100644
--- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { host } from '@@/js/config.js';
 import { instance } from '@/instance.js';
 
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index 00d0ec7fa5..485e532d51 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -54,11 +54,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onUnmounted, reactive, ref } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { useStream } from '@/stream.js';
 import kmg from '@/filters/kmg.js';
-import * as sound from '@/scripts/sound.js';
-import { deepClone } from '@/scripts/clone.js';
+import * as sound from '@/utility/sound.js';
+import { deepClone } from '@/utility/clone.js';
 import { prefer } from '@/preferences.js';
 
 const name = 'jobQueue';
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index 4705de2016..256cbc8021 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, watch } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue
index 8aaed2624d..c5e1324ef5 100644
--- a/packages/frontend/src/widgets/WidgetNotifications.vue
+++ b/packages/frontend/src/widgets/WidgetNotifications.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import XNotifications from '@/components/MkNotifications.vue';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
index 4a3cdb9ba3..f6bd4c0025 100644
--- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue
+++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import type { GetFormResultType } from '@/utility/form.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import number from '@/filters/number.js';
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 6c7c82f229..5d6e2ed48f 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -26,10 +26,10 @@ import { onUnmounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { useStream } from '@/stream.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/widgets/WidgetPostForm.vue b/packages/frontend/src/widgets/WidgetPostForm.vue
index b0a62d1be2..3170eab305 100644
--- a/packages/frontend/src/widgets/WidgetPostForm.vue
+++ b/packages/frontend/src/widgets/WidgetPostForm.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkPostForm from '@/components/MkPostForm.vue';
 
 const name = 'postForm';
diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue
index 9f006945ab..c86d1c9653 100644
--- a/packages/frontend/src/widgets/WidgetProfile.vue
+++ b/packages/frontend/src/widgets/WidgetProfile.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { $i } from '@/account.js';
 import { userPage } from '@/filters/user.js';
 
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 0d7ce55be3..82dfeacff1 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -27,7 +27,7 @@ import { ref, watch, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { url as base } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 5ecc1ab022..13b76533d7 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -32,9 +32,9 @@ import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import MarqueeText from '@/components/MkMarquee.vue';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { shuffle } from '@/scripts/shuffle.js';
+import { shuffle } from '@/utility/shuffle.js';
 import { url as base } from '@@/js/config.js';
 import { useInterval } from '@@/js/use-interval.js';
 
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 67d35d71db..2cbf3a8ef6 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -21,9 +21,9 @@ import { onMounted, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue
index 02db9454a8..47dec05303 100644
--- a/packages/frontend/src/widgets/WidgetTimeline.vue
+++ b/packages/frontend/src/widgets/WidgetTimeline.vue
@@ -34,9 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue
index 1e3ca74d40..db09031c33 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -29,10 +29,10 @@ import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue
index f85d27d6aa..f51ef12a2a 100644
--- a/packages/frontend/src/widgets/WidgetUnixClock.vue
+++ b/packages/frontend/src/widgets/WidgetUnixClock.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onUnmounted, ref, watch } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 
 const name = 'unixClock';
 
diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue
index 805e14c669..eb86732817 100644
--- a/packages/frontend/src/widgets/WidgetUserList.vue
+++ b/packages/frontend/src/widgets/WidgetUserList.vue
@@ -28,10 +28,10 @@ import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue
index 3264c8dc04..9026fefb20 100644
--- a/packages/frontend/src/widgets/server-metric/index.vue
+++ b/packages/frontend/src/widgets/server-metric/index.vue
@@ -30,8 +30,8 @@ import XCpu from './cpu.vue';
 import XMemory from './mem.vue';
 import XDisk from './disk.vue';
 import MkContainer from '@/components/MkContainer.vue';
-import type { GetFormResultType } from '@/scripts/form.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import type { GetFormResultType } from '@/utility/form.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts
index 98e1e44cd8..de4c369cbb 100644
--- a/packages/frontend/src/widgets/widget.ts
+++ b/packages/frontend/src/widgets/widget.ts
@@ -5,9 +5,9 @@
 
 import { reactive, watch } from 'vue';
 import { throttle } from 'throttle-debounce';
-import type { Form, GetFormResultType } from '@/scripts/form.js';
+import type { Form, GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 
 export type Widget<P extends Record<string, unknown>> = {
 	id: string;
diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts
index 2a15a74249..a569c0fa51 100644
--- a/packages/frontend/test/aiscript/api.test.ts
+++ b/packages/frontend/test/aiscript/api.test.ts
@@ -4,7 +4,7 @@
  */
 
 import { miLocalStorage } from '@/local-storage.js';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
 import {
 	afterAll,
@@ -59,7 +59,7 @@ vi.mock('@/os.js', () => {
 
 const misskeyApiMock = vi.hoisted(() => vi.fn());
 
-vi.mock('@/scripts/misskey-api.js', () => {
+vi.mock('@/utility/misskey-api.js', () => {
 	return { misskeyApi: misskeyApiMock };
 });
 
diff --git a/packages/frontend/test/aiscript/common.test.ts b/packages/frontend/test/aiscript/common.test.ts
index acc48826ea..c0c978001b 100644
--- a/packages/frontend/test/aiscript/common.test.ts
+++ b/packages/frontend/test/aiscript/common.test.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { assertStringAndIsIn } from "@/scripts/aiscript/common.js";
+import { assertStringAndIsIn } from "@/aiscript/common.js";
 import { values } from "@syuilo/aiscript";
 import { describe, expect, test } from "vitest";
 
diff --git a/packages/frontend/test/aiscript/ui.test.ts b/packages/frontend/test/aiscript/ui.test.ts
index 5f77edbb49..44a50aaa62 100644
--- a/packages/frontend/test/aiscript/ui.test.ts
+++ b/packages/frontend/test/aiscript/ui.test.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
+import { registerAsUiLib } from '@/aiscript/ui.js';
 import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
 import { describe, expect, test } from 'vitest';
 import { type Ref, ref } from 'vue';
@@ -19,7 +19,7 @@ import type {
 	AsUiText,
 	AsUiTextarea,
 	AsUiTextInput,
-} from '@/scripts/aiscript/ui.js';
+} from '@/aiscript/ui.js';
 
 type ExeResult = {
 	root: AsUiRoot;
diff --git a/packages/frontend/test/autocomplete.test.ts b/packages/frontend/test/autocomplete.test.ts
index 394ac3a821..38be35813f 100644
--- a/packages/frontend/test/autocomplete.test.ts
+++ b/packages/frontend/test/autocomplete.test.ts
@@ -4,7 +4,7 @@
  */
 
 import { assert, describe, test } from 'vitest';
-import { searchEmoji } from '@/scripts/search-emoji.js';
+import { searchEmoji } from '@/utility/search-emoji.js';
 
 describe('emoji autocomplete', () => {
 	test('名前の完全一致は名前の前方一致より優先される', async () => {
diff --git a/packages/frontend/test/intl-string.test.ts b/packages/frontend/test/intl-string.test.ts
index dd545b45d1..b52824db86 100644
--- a/packages/frontend/test/intl-string.test.ts
+++ b/packages/frontend/test/intl-string.test.ts
@@ -10,7 +10,7 @@ import {
 	normalizeStringWithHiragana,
 	compareStringEquals,
 	compareStringIncludes,
-} from '@/scripts/intl-string.js';
+} from '@/utility/intl-string.js';
 
 // 共通のテストを実行するヘルパー関数
 const runCommonTests = (normalizeFn: (str: string) => string) => {
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 1c094e272b..a28fc553f4 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -86,7 +86,7 @@ export function getConfig(): UserConfig {
 		plugins: [
 			pluginCreateSearchIndex({
 				targetFilePaths: ['src/pages/settings/*.vue'],
-				exportFilePath: './src/scripts/autogen/settings-search-index.ts',
+				exportFilePath: './src/utility/autogen/settings-search-index.ts',
 				verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
 			}),
 			pluginVue(),
-- 
cgit v1.2.3-freya


From 7b323031b774745ee2146c89ead2a9ebe628d613 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 19 Mar 2025 18:46:03 +0900
Subject: refactor(frontend): use useTemplateRef for DOM referencing

---
 .../src/components/MkAbuseReportWindow.vue         |  4 ++--
 packages/frontend/src/components/MkAnimBg.vue      |  4 ++--
 .../src/components/MkAnnouncementDialog.vue        |  6 +++---
 .../src/components/MkAntennaEditorDialog.vue       |  4 ++--
 .../frontend/src/components/MkAutocomplete.vue     |  4 ++--
 packages/frontend/src/components/MkButton.vue      |  6 +++---
 packages/frontend/src/components/MkCaptcha.vue     |  4 ++--
 packages/frontend/src/components/MkChart.vue       | 14 +++++--------
 packages/frontend/src/components/MkCodeEditor.vue  |  4 ++--
 packages/frontend/src/components/MkColorInput.vue  |  4 ++--
 packages/frontend/src/components/MkContainer.vue   |  8 ++++----
 packages/frontend/src/components/MkContextMenu.vue |  4 ++--
 .../frontend/src/components/MkCropperDialog.vue    |  6 +++---
 .../src/components/MkCustomEmojiDetailedDialog.vue |  6 +++---
 packages/frontend/src/components/MkDialog.vue      |  4 ++--
 packages/frontend/src/components/MkDrive.vue       |  6 +++---
 .../src/components/MkDriveSelectDialog.vue         |  4 ++--
 .../src/components/MkEmbedCodeGenDialog.vue        |  8 ++++----
 packages/frontend/src/components/MkEmojiPicker.vue |  6 +++---
 .../src/components/MkEmojiPickerDialog.vue         |  6 +++---
 .../src/components/MkFileCaptionEditWindow.vue     |  4 ++--
 .../frontend/src/components/MkFoldableSection.vue  |  4 ++--
 packages/frontend/src/components/MkFolder.vue      |  4 ++--
 packages/frontend/src/components/MkFormDialog.vue  |  4 ++--
 packages/frontend/src/components/MkHeatmap.vue     |  6 +++---
 .../frontend/src/components/MkHorizontalSwipe.vue  |  4 ++--
 .../frontend/src/components/MkImgWithBlurhash.vue  |  8 ++++----
 packages/frontend/src/components/MkInput.vue       |  8 ++++----
 .../frontend/src/components/MkInstanceStats.vue    |  8 ++++----
 packages/frontend/src/components/MkLaunchPad.vue   |  4 ++--
 packages/frontend/src/components/MkMediaAudio.vue  |  6 +++---
 packages/frontend/src/components/MkMediaList.vue   |  4 ++--
 packages/frontend/src/components/MkMediaVideo.vue  |  6 +++---
 packages/frontend/src/components/MkMenu.child.vue  |  4 ++--
 packages/frontend/src/components/MkMenu.vue        |  8 ++++----
 packages/frontend/src/components/MkModal.vue       |  6 +++---
 packages/frontend/src/components/MkModalWindow.vue |  8 ++++----
 packages/frontend/src/components/MkNote.vue        | 16 +++++++--------
 .../frontend/src/components/MkNoteDetailed.vue     | 16 +++++++--------
 packages/frontend/src/components/MkNotes.vue       |  4 ++--
 .../src/components/MkNotificationSelectWindow.vue  |  8 ++++----
 .../frontend/src/components/MkNotifications.vue    |  4 ++--
 packages/frontend/src/components/MkOmit.vue        |  4 ++--
 packages/frontend/src/components/MkPageWindow.vue  |  4 ++--
 packages/frontend/src/components/MkPagination.vue  |  4 ++--
 .../frontend/src/components/MkPasswordDialog.vue   |  6 +++---
 packages/frontend/src/components/MkPopupMenu.vue   |  4 ++--
 packages/frontend/src/components/MkPostForm.vue    | 12 +++++------
 .../frontend/src/components/MkPostFormDialog.vue   |  7 +++----
 .../frontend/src/components/MkPullToRefresh.vue    |  6 +++---
 packages/frontend/src/components/MkRange.vue       |  6 +++---
 .../frontend/src/components/MkReactionIcon.vue     |  4 ++--
 .../src/components/MkReactionsViewer.reaction.vue  |  4 ++--
 .../frontend/src/components/MkRetentionHeatmap.vue |  6 +++---
 .../src/components/MkRetentionLineChart.vue        |  4 ++--
 .../frontend/src/components/MkSigninDialog.vue     |  4 ++--
 .../frontend/src/components/MkSignupDialog.vue     |  4 ++--
 packages/frontend/src/components/MkSparkle.vue     |  4 ++--
 .../src/components/MkSystemWebhookEditor.vue       |  8 ++++----
 packages/frontend/src/components/MkTagCloud.vue    |  8 ++++----
 packages/frontend/src/components/MkTextarea.vue    |  4 ++--
 packages/frontend/src/components/MkTimeline.vue    |  6 +++---
 .../src/components/MkTokenGenerateWindow.vue       |  4 ++--
 packages/frontend/src/components/MkTooltip.vue     |  4 ++--
 .../frontend/src/components/MkTutorialDialog.vue   |  6 +++---
 packages/frontend/src/components/MkUpdated.vue     |  6 +++---
 .../frontend/src/components/MkUserSelectDialog.vue |  4 ++--
 .../frontend/src/components/MkUserSetupDialog.vue  |  9 ++++----
 .../frontend/src/components/MkVisibilityPicker.vue |  4 ++--
 .../MkVisitorDashboard.ActiveUsersChart.vue        |  4 ++--
 .../frontend/src/components/MkWaitingDialog.vue    |  4 ++--
 packages/frontend/src/components/MkWindow.vue      |  4 ++--
 packages/frontend/src/components/global/MkA.vue    |  4 ++--
 packages/frontend/src/components/global/MkLazy.vue |  4 ++--
 .../src/components/global/MkPageHeader.tabs.vue    |  6 +++---
 .../src/components/global/MkPageHeader.vue         |  4 ++--
 .../src/components/global/MkStickyContainer.vue    |  3 +--
 .../frontend/src/components/grid/MkDataCell.vue    | 24 ++++++++++++----------
 packages/frontend/src/pages/about-misskey.vue      |  4 ++--
 packages/frontend/src/pages/admin/_header_.vue     |  6 +++---
 .../abuse-report/notification-recipient.editor.vue |  6 +++---
 packages/frontend/src/pages/admin/abuses.vue       |  4 ++--
 packages/frontend/src/pages/admin/invites.vue      |  6 +++---
 packages/frontend/src/pages/admin/modlog.vue       |  4 ++--
 .../src/pages/admin/overview.active-users.vue      |  4 ++--
 .../src/pages/admin/overview.ap-requests.vue       |  6 +++---
 packages/frontend/src/pages/admin/overview.pie.vue |  4 ++--
 .../src/pages/admin/overview.queue.chart.vue       |  4 ++--
 .../frontend/src/pages/admin/overview.queue.vue    | 10 ++++-----
 packages/frontend/src/pages/admin/overview.vue     |  4 ++--
 .../frontend/src/pages/admin/queue.chart.chart.vue |  4 ++--
 packages/frontend/src/pages/admin/queue.chart.vue  | 10 ++++-----
 packages/frontend/src/pages/admin/users.vue        |  4 ++--
 packages/frontend/src/pages/antenna-timeline.vue   |  8 ++++----
 .../frontend/src/pages/custom-emojis-manager.vue   |  4 ++--
 .../frontend/src/pages/drop-and-fusion.game.vue    |  6 +++---
 packages/frontend/src/pages/explore.users.vue      |  4 ++--
 packages/frontend/src/pages/explore.vue            |  4 ++--
 packages/frontend/src/pages/follow-requests.vue    |  4 ++--
 packages/frontend/src/pages/invite.vue             |  4 ++--
 packages/frontend/src/pages/my-clips/index.vue     |  4 ++--
 .../pages/page-editor/els/page-editor.el.text.vue  |  5 ++---
 .../frontend/src/pages/settings/2fa.qrdialog.vue   |  4 ++--
 .../pages/settings/avatar-decoration.dialog.vue    |  4 ++--
 packages/frontend/src/pages/settings/index.vue     |  4 ++--
 .../frontend/src/pages/settings/notifications.vue  |  4 ++--
 packages/frontend/src/pages/timeline.vue           |  6 +++---
 packages/frontend/src/pages/user-list-timeline.vue |  8 ++++----
 .../frontend/src/pages/user/activity.following.vue |  8 ++++----
 .../frontend/src/pages/user/activity.notes.vue     |  8 ++++----
 packages/frontend/src/pages/user/activity.pv.vue   |  8 ++++----
 .../frontend/src/pages/welcome.timeline.note.vue   |  4 ++--
 packages/frontend/src/pages/welcome.timeline.vue   |  6 +++---
 packages/frontend/src/ui/classic.sidebar.vue       |  4 ++--
 packages/frontend/src/ui/classic.vue               |  4 ++--
 packages/frontend/src/ui/deck.vue                  |  4 ++--
 packages/frontend/src/ui/deck/antenna-column.vue   |  4 ++--
 packages/frontend/src/ui/deck/channel-column.vue   |  4 ++--
 packages/frontend/src/ui/deck/column.vue           |  4 ++--
 packages/frontend/src/ui/deck/list-column.vue      |  4 ++--
 .../frontend/src/ui/deck/notifications-column.vue  |  6 +++---
 .../frontend/src/ui/deck/role-timeline-column.vue  |  4 ++--
 packages/frontend/src/ui/deck/tl-column.vue        |  4 ++--
 packages/frontend/src/ui/universal.vue             |  4 ++--
 packages/frontend/src/widgets/WidgetAichan.vue     |  4 ++--
 .../frontend/src/widgets/WidgetInstanceCloud.vue   |  8 ++++----
 packages/frontend/src/widgets/WidgetSlideshow.vue  |  8 ++++----
 127 files changed, 353 insertions(+), 359 deletions(-)

(limited to 'packages/frontend/src/components/MkSystemWebhookEditor.vue')

diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index a634a748e9..dbac5e9dd7 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { ref, shallowRef } from 'vue';
+import { ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkWindow from '@/components/MkWindow.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
@@ -47,7 +47,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
+const uiWindow = useTemplateRef('uiWindow');
 const comment = ref(props.initialComment ?? '');
 
 function send() {
diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue
index 4bf6125af5..2938645557 100644
--- a/packages/frontend/src/components/MkAnimBg.vue
+++ b/packages/frontend/src/components/MkAnimBg.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, shallowRef } from 'vue';
+import { onMounted, onUnmounted, useTemplateRef } from 'vue';
 import isChromatic from 'chromatic/isChromatic';
 
-const canvasEl = shallowRef<HTMLCanvasElement>();
+const canvasEl = useTemplateRef('canvasEl');
 
 const props = withDefaults(defineProps<{
 	scale?: number;
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 582bb137bc..6e5b29654b 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef } from 'vue';
+import { onMounted, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -37,8 +37,8 @@ const props = withDefaults(defineProps<{
 }>(), {
 });
 
-const rootEl = shallowRef<HTMLDivElement>();
-const modal = shallowRef<InstanceType<typeof MkModal>>();
+const rootEl = useTemplateRef('rootEl');
+const modal = useTemplateRef('modal');
 
 async function ok() {
 	if (props.announcement.needConfirmationToRead) {
diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.vue b/packages/frontend/src/components/MkAntennaEditorDialog.vue
index 6d815d29f3..0ebf5abf4c 100644
--- a/packages/frontend/src/components/MkAntennaEditorDialog.vue
+++ b/packages/frontend/src/components/MkAntennaEditorDialog.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from 'vue';
+import { useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import XAntennaEditor from '@/components/MkAntennaEditor.vue';
@@ -40,7 +40,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void,
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 
 function onAntennaCreated(newAntenna: Misskey.entities.Antenna) {
 	emit('created', newAntenna);
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 7436d4f1b2..5b747a2a1a 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts">
-import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
+import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
 import sanitizeHtml from 'sanitize-html';
 import { emojilist, getEmojiName } from '@@/js/emojilist.js';
 import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
@@ -139,7 +139,7 @@ const emit = defineEmits<{
 }>();
 
 const suggests = ref<Element>();
-const rootEl = shallowRef<HTMLDivElement>();
+const rootEl = useTemplateRef('rootEl');
 
 const fetching = ref(true);
 const users = ref<any[]>([]);
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 5b32ec0fc8..c81edc2a73 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, shallowRef } from 'vue';
+import { nextTick, onMounted, useTemplateRef } from 'vue';
 
 const props = defineProps<{
 	type?: 'button' | 'submit' | 'reset';
@@ -64,8 +64,8 @@ const emit = defineEmits<{
 	(ev: 'click', payload: MouseEvent): void;
 }>();
 
-const el = shallowRef<HTMLElement | null>(null);
-const ripples = shallowRef<HTMLElement | null>(null);
+const el = useTemplateRef('el');
+const ripples = useTemplateRef('ripples');
 
 onMounted(() => {
 	if (props.autofocus) {
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index 30eafb7a5b..e4f953bda8 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
+import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
 import { store } from '@/store.js';
 
 // APIs provided by Captcha services
@@ -69,7 +69,7 @@ const emit = defineEmits<{
 
 const available = ref(false);
 
-const captchaEl = shallowRef<HTMLDivElement | undefined>();
+const captchaEl = useTemplateRef('captchaEl');
 const captchaWidgetId = ref<string | undefined>(undefined);
 const testcaptchaInput = ref('');
 const testcaptchaPassed = ref(false);
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index d9f4558014..7e164362c1 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -45,12 +45,8 @@ export type ChartSrc =
 </script>
 
 <script lang="ts" setup>
-/* eslint-disable id-denylist --
-  Chart.js has a `data` attribute in most chart definitions, which triggers the
-  id-denylist violation when setting it. This is causing about 60+ lint issues.
-  As this is part of Chart.js's API it makes sense to disable the check here.
-*/
-import { onMounted, ref, shallowRef, watch } from 'vue';
+
+import { onMounted, ref, useTemplateRef, watch } from 'vue';
 import { Chart } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import { misskeyApiGet } from '@/utility/misskey-api.js';
@@ -96,7 +92,7 @@ const props = withDefaults(defineProps<{
 	nowForChromatic: undefined,
 });
 
-const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
+const legendEl = useTemplateRef('legendEl');
 
 const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 const negate = arr => arr.map(x => -x);
@@ -134,7 +130,7 @@ let chartData: {
 	bytes?: boolean;
 } | null = null;
 
-const chartEl = shallowRef<HTMLCanvasElement | null>(null);
+const chartEl = useTemplateRef('chartEl');
 const fetching = ref(true);
 
 const getDate = (ago: number) => {
@@ -849,7 +845,7 @@ watch(() => [props.src, props.span], fetchAndRender);
 onMounted(() => {
 	fetchAndRender();
 });
-/* eslint-enable id-denylist */
+
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index 5bf2301e72..46fdf15b5d 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, watch, toRefs, shallowRef, nextTick } from 'vue';
+import { ref, watch, toRefs, useTemplateRef, nextTick } from 'vue';
 import { debounce } from 'throttle-debounce';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
@@ -61,7 +61,7 @@ const { modelValue } = toRefs(props);
 const v = ref<string>(modelValue.value ?? '');
 const focused = ref(false);
 const changed = ref(false);
-const inputEl = shallowRef<HTMLTextAreaElement>();
+const inputEl = useTemplateRef('inputEl');
 
 const focus = () => inputEl.value?.focus();
 
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index 55a32664de..50931cc318 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, toRefs } from 'vue';
+import { ref, useTemplateRef, toRefs } from 'vue';
 
 const props = defineProps<{
 	modelValue: string | null;
@@ -39,7 +39,7 @@ const emit = defineEmits<{
 
 const { modelValue } = toRefs(props);
 const v = ref(modelValue.value);
-const inputEl = shallowRef<HTMLElement>();
+const inputEl = useTemplateRef('inputEl');
 
 const onInput = () => {
 	emit('update:modelValue', v.value ?? '');
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 169f3df7a4..39ca39aad7 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import { onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
 import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 
@@ -58,9 +58,9 @@ const props = withDefaults(defineProps<{
 	maxHeight: null,
 });
 
-const rootEl = shallowRef<HTMLElement>();
-const contentEl = shallowRef<HTMLElement>();
-const headerEl = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
+const contentEl = useTemplateRef('contentEl');
+const headerEl = useTemplateRef('headerEl');
 const showBody = ref(props.expanded);
 const ignoreOmit = ref(false);
 const omitted = ref(false);
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index 46da0840fa..e47dba4bae 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
+import { onMounted, onBeforeUnmount, useTemplateRef, ref } from 'vue';
 import MkMenu from './MkMenu.vue';
 import type { MenuItem } from '@/types/menu.js';
 import contains from '@/utility/contains.js';
@@ -34,7 +34,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const rootEl = shallowRef<HTMLDivElement>();
+const rootEl = useTemplateRef('rootEl');
 
 const zIndex = ref<number>(os.claimZIndex('high'));
 
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 4a89fb30ca..bd2e6a9cbc 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, useTemplateRef, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import Cropper from 'cropperjs';
 import tinycolor from 'tinycolor2';
@@ -56,8 +56,8 @@ const props = defineProps<{
 }>();
 
 const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
-const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
-const imgEl = shallowRef<HTMLImageElement>();
+const dialogEl = useTemplateRef('dialogEl');
+const imgEl = useTemplateRef('imgEl');
 let cropper: Cropper | null = null;
 const loading = ref(true);
 
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
index 86d6269c69..54fda6bf7c 100644
--- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -57,14 +57,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
-import { shallowRef } from 'vue';
+import { useTemplateRef } from 'vue';
 import MkLink from '@/components/MkLink.vue';
 import { i18n } from '@/i18n.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 
 const props = defineProps<{
-  emoji: Misskey.entities.EmojiDetailed,
+	emoji: Misskey.entities.EmojiDetailed,
 }>();
 
 const emit = defineEmits<{
@@ -73,7 +73,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialogEl = useTemplateRef('dialogEl');
 
 function cancel() {
 	emit('cancel');
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 312c7ab58f..81d508c161 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, computed } from 'vue';
+import { ref, useTemplateRef, computed } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -117,7 +117,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
+const modal = useTemplateRef('modal');
 
 const inputValue = ref<string | number | null>(props.input?.default ?? null);
 const selectedValue = ref(props.select?.default ?? null);
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 270ef51916..edbdf1cca1 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
+import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from './MkButton.vue';
 import type { MenuItem } from '@/types/menu.js';
@@ -129,8 +129,8 @@ const emit = defineEmits<{
 	(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
 }>();
 
-const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>();
-const fileInput = shallowRef<HTMLInputElement>();
+const loadMoreFiles = useTemplateRef('loadMoreFiles');
+const fileInput = useTemplateRef('fileInput');
 
 const folder = ref<Misskey.entities.DriveFolder | null>(null);
 const files = ref<Misskey.entities.DriveFile[]>([]);
diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveSelectDialog.vue
index f1ecc27123..1b9455e3f3 100644
--- a/packages/frontend/src/components/MkDriveSelectDialog.vue
+++ b/packages/frontend/src/components/MkDriveSelectDialog.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef } from 'vue';
+import { ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import XDrive from '@/components/MkDrive.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -43,7 +43,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 
 const selected = ref<Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]>([]);
 
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index e038b84ba7..a4e9547f90 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
+import { useTemplateRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
 import { url } from '@@/js/config.js';
 import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
 import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js';
@@ -121,7 +121,7 @@ const props = defineProps<{
 }>();
 
 //#region Modalの制御
-const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialogEl = useTemplateRef('dialogEl');
 
 function cancel() {
 	emit('cancel');
@@ -198,9 +198,9 @@ function doCopy() {
 //#endregion
 
 //#region プレビューのリサイズ
-const resizerRootEl = shallowRef<HTMLDivElement>();
+const resizerRootEl = useTemplateRef('resizerRootEl');
 const iframeLoading = ref(true);
-const iframeEl = shallowRef<HTMLIFrameElement>();
+const iframeEl = useTemplateRef('iframeEl');
 const iframeHeight = ref(0);
 const iframeScale = ref(1);
 const iframeStyle = computed(() => {
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index c0883b1342..d4367f6ee8 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, computed, watch, onMounted } from 'vue';
+import { ref, useTemplateRef, computed, watch, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import {
 	emojilist,
@@ -157,8 +157,8 @@ const emit = defineEmits<{
 	(ev: 'esc'): void;
 }>();
 
-const searchEl = shallowRef<HTMLInputElement>();
-const emojisEl = shallowRef<HTMLDivElement>();
+const searchEl = useTemplateRef('searchEl');
+const emojisEl = useTemplateRef('emojisEl');
 
 const {
 	emojiPickerScale,
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 6d7062b41c..4978933f73 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
-import { shallowRef } from 'vue';
+import { useTemplateRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
 import { prefer } from '@/preferences.js';
@@ -64,8 +64,8 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
-const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
+const modal = useTemplateRef('modal');
+const picker = useTemplateRef('picker');
 
 function chosen(emoji: string) {
 	emit('done', emoji);
diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
index 76bb965101..120d5355b2 100644
--- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue
+++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, ref } from 'vue';
+import { useTemplateRef, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
@@ -42,7 +42,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 
 const caption = ref(props.default);
 
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index 554586b2f3..b9888d9b64 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, shallowRef, watch } from 'vue';
+import { onMounted, ref, useTemplateRef, watch } from 'vue';
 import { miLocalStorage } from '@/local-storage.js';
 import { prefer } from '@/preferences.js';
 import { getBgColor } from '@/utility/get-bg-color.js';
@@ -46,7 +46,7 @@ const props = withDefaults(defineProps<{
 	persistKey: null,
 });
 
-const rootEl = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
 const parentBg = ref<string | null>(null);
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 1e2057d28e..b13972f66d 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, ref, shallowRef } from 'vue';
+import { nextTick, onMounted, ref, useTemplateRef } from 'vue';
 import { prefer } from '@/preferences.js';
 import { getBgColor } from '@/utility/get-bg-color.js';
 
@@ -74,7 +74,7 @@ const props = withDefaults(defineProps<{
 	spacerMax: 22,
 });
 
-const rootEl = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
 const bgSame = ref(false);
 const opened = ref(props.defaultOpen);
 const openedAtLeastOnce = ref(props.defaultOpen);
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index df39f34816..4756079e76 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { reactive, shallowRef } from 'vue';
+import { reactive, useTemplateRef } from 'vue';
 import MkInput from './MkInput.vue';
 import MkTextarea from './MkTextarea.vue';
 import MkSwitch from './MkSwitch.vue';
@@ -99,7 +99,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 const values = reactive({});
 
 for (const item in props.form) {
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index 8a84bfd541..28bb936755 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
+import { onMounted, nextTick, watch, useTemplateRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -35,8 +35,8 @@ const props = withDefaults(defineProps<{
 	label: '',
 });
 
-const rootEl = shallowRef<HTMLDivElement | null>(null);
-const chartEl = shallowRef<HTMLCanvasElement | null>(null);
+const rootEl = useTemplateRef('rootEl');
+const chartEl = useTemplateRef('chartEl');
 const now = new Date();
 let chartInstance: Chart | null = null;
 const fetching = ref(true);
diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue
index ad85660fc4..849136eb8f 100644
--- a/packages/frontend/src/components/MkHorizontalSwipe.vue
+++ b/packages/frontend/src/components/MkHorizontalSwipe.vue
@@ -26,12 +26,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 <script lang="ts" setup>
-import { ref, shallowRef, computed, nextTick, watch } from 'vue';
+import { ref, useTemplateRef, computed, nextTick, watch } from 'vue';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
 import { isHorizontalSwipeSwiping as isSwiping } from '@/utility/touch.js';
 import { prefer } from '@/preferences.js';
 
-const rootEl = shallowRef<HTMLDivElement>();
+const rootEl = useTemplateRef('rootEl');
 
 const tabModel = defineModel<string>('tab');
 
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 6585784e42..b86aca19fd 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -83,7 +83,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
 </script>
 
 <script lang="ts" setup>
-import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue';
+import { computed, nextTick, onMounted, onUnmounted, useTemplateRef, watch, ref } from 'vue';
 import { v4 as uuid } from 'uuid';
 import { render } from 'buraha';
 import { prefer } from '@/preferences.js';
@@ -120,9 +120,9 @@ const props = withDefaults(defineProps<{
 });
 
 const viewId = uuid();
-const canvas = shallowRef<HTMLCanvasElement>();
-const root = shallowRef<HTMLDivElement>();
-const img = shallowRef<HTMLImageElement>();
+const canvas = useTemplateRef('canvas');
+const root = useTemplateRef('root');
+const img = useTemplateRef('img');
 const loaded = ref(false);
 const canvasWidth = ref(64);
 const canvasHeight = ref(64);
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index a66e64311a..49cb5cb544 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, useTemplateRef, watch, computed, toRefs } from 'vue';
 import { debounce } from 'throttle-debounce';
 import { useInterval } from '@@/js/use-interval.js';
 import type { InputHTMLAttributes } from 'vue';
@@ -92,9 +92,9 @@ const focused = ref(false);
 const changed = ref(false);
 const invalid = ref(false);
 const filled = computed(() => v.value !== '' && v.value != null);
-const inputEl = shallowRef<HTMLInputElement>();
-const prefixEl = shallowRef<HTMLElement>();
-const suffixEl = shallowRef<HTMLElement>();
+const inputEl = useTemplateRef('inputEl');
+const prefixEl = useTemplateRef('prefixEl');
+const suffixEl = useTemplateRef('suffixEl');
 const height =
 	props.small ? 33 :
 	props.large ? 39 :
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 3113c6fad6..76bec9bc66 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -84,8 +84,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, computed, shallowRef } from 'vue';
+import { onMounted, ref, computed, useTemplateRef } from 'vue';
 import { Chart } from 'chart.js';
+import type { HeatmapSource } from '@/components/MkHeatmap.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkChart from '@/components/MkChart.vue';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
@@ -95,7 +96,6 @@ import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import MkHeatmap from '@/components/MkHeatmap.vue';
-import type { HeatmapSource } from '@/components/MkHeatmap.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
 import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
@@ -109,8 +109,8 @@ const chartLimit = 500;
 const chartSpan = ref<'hour' | 'day'>('hour');
 const chartSrc = ref('active-users');
 const heatmapSrc = ref<HeatmapSource>('active-users');
-const subDoughnutEl = shallowRef<HTMLCanvasElement>();
-const pubDoughnutEl = shallowRef<HTMLCanvasElement>();
+const subDoughnutEl = useTemplateRef('subDoughnutEl');
+const pubDoughnutEl = useTemplateRef('pubDoughnutEl');
 
 const { handler: externalTooltipHandler1 } = useChartTooltip({
 	position: 'middle',
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 9a78eb67f2..3e5a88a170 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from 'vue';
+import { useTemplateRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import { navbarItemDef } from '@/navbar.js';
 import { deviceKind } from '@/utility/device-kind.js';
@@ -48,7 +48,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop
 	deviceKind === 'smartphone' ? 'drawer' :
 	'dialog';
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
+const modal = useTemplateRef('modal');
 
 const menu = prefer.s.menu;
 
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 28860d0cf6..db6deb161a 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -88,7 +88,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
+import { useTemplateRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
 import type { Keymap } from '@/utility/hotkey.js';
@@ -151,8 +151,8 @@ function hasFocus() {
 	return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
 }
 
-const playerEl = shallowRef<HTMLDivElement>();
-const audioEl = shallowRef<HTMLAudioElement>();
+const playerEl = useTemplateRef('playerEl');
+const audioEl = useTemplateRef('audioEl');
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'));
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 8a7a423869..3c066650d9 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, onUnmounted, shallowRef } from 'vue';
+import { computed, onMounted, onUnmounted, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import PhotoSwipeLightbox from 'photoswipe/lightbox';
 import PhotoSwipe from 'photoswipe';
@@ -46,7 +46,7 @@ const props = defineProps<{
 	raw?: boolean;
 }>();
 
-const gallery = shallowRef<HTMLDivElement>();
+const gallery = useTemplateRef('gallery');
 const pswpZIndex = os.claimZIndex('middle');
 document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
 const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 9674cb4461..5475e703a5 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -109,7 +109,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
+import { ref, useTemplateRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
 import type { Keymap } from '@/utility/hotkey.js';
@@ -299,8 +299,8 @@ async function toggleSensitive(file: Misskey.entities.DriveFile) {
 }
 
 // MediaControl: Video State
-const videoEl = shallowRef<HTMLVideoElement>();
-const playerEl = shallowRef<HTMLDivElement>();
+const videoEl = useTemplateRef('videoEl');
+const playerEl = useTemplateRef('playerEl');
 const isHoverring = ref(false);
 const controlsShowing = computed(() => {
 	if (!oncePlayed.value) return true;
diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue
index 086573ba6d..f7cd72b6c6 100644
--- a/packages/frontend/src/components/MkMenu.child.vue
+++ b/packages/frontend/src/components/MkMenu.child.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
+import { nextTick, onMounted, onUnmounted, provide, useTemplateRef, watch } from 'vue';
 import MkMenu from './MkMenu.vue';
 import type { MenuItem } from '@/types/menu.js';
 
@@ -28,7 +28,7 @@ const emit = defineEmits<{
 
 provide('isNestingMenu', true);
 
-const el = shallowRef<HTMLElement>();
+const el = useTemplateRef('el');
 const align = 'left';
 
 const SCROLLBAR_THICKNESS = 16;
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index e3c27c5f6e..5438dac3a8 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -179,7 +179,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts">
-import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
+import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, unref, watch } from 'vue';
 import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 import type { Keymap } from '@/utility/hotkey.js';
 import MkSwitchButton from '@/components/MkSwitch.button.vue';
@@ -212,11 +212,11 @@ const big = isTouchUsing;
 
 const isNestingMenu = inject<boolean>('isNestingMenu', false);
 
-const itemsEl = shallowRef<HTMLElement>();
+const itemsEl = useTemplateRef('itemsEl');
 
 const items2 = ref<InnerMenuItem[]>();
 
-const child = shallowRef<InstanceType<typeof XChild>>();
+const child = useTemplateRef('child');
 
 const keymap = {
 	'up|k|shift+tab': {
@@ -257,7 +257,7 @@ watch(() => props.items, () => {
 });
 
 const childMenu = ref<MenuItem[] | null>();
-const childTarget = shallowRef<HTMLElement | null>();
+const childTarget = useTemplateRef('childTarget');
 
 function closeChild() {
 	childMenu.value = null;
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 8b3086d55e..b5c93df4ed 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue';
+import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, useTemplateRef, computed } from 'vue';
 import type { Keymap } from '@/utility/hotkey.js';
 import * as os from '@/os.js';
 import { isTouchUsing } from '@/utility/touch.js';
@@ -100,8 +100,8 @@ const maxHeight = ref<number>();
 const fixed = ref(false);
 const transformOrigin = ref('center');
 const showing = ref(true);
-const modalRootEl = shallowRef<HTMLElement>();
-const content = shallowRef<HTMLElement>();
+const modalRootEl = useTemplateRef('modalRootEl');
+const content = useTemplateRef('content');
 const zIndex = os.claimZIndex(props.zPriority);
 const useSendAnime = ref(false);
 const type = computed<ModalTypes>(() => {
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index f06cfffee4..19989e375b 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
+import { onMounted, onUnmounted, useTemplateRef, ref } from 'vue';
 import MkModal from './MkModal.vue';
 
 const props = withDefaults(defineProps<{
@@ -47,9 +47,9 @@ const emit = defineEmits<{
 	(event: 'esc'): void;
 }>();
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
-const rootEl = shallowRef<HTMLElement>();
-const headerEl = shallowRef<HTMLElement>();
+const modal = useTemplateRef('modal');
+const rootEl = useTemplateRef('rootEl');
+const headerEl = useTemplateRef('headerEl');
 const bodyWidth = ref(0);
 const bodyHeight = ref(0);
 
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 1f18b173a0..73ff85b150 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -178,7 +178,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, onMounted, ref, shallowRef, watch, provide } from 'vue';
+import { computed, inject, onMounted, ref, useTemplateRef, watch, provide } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { isLink } from '@@/js/is-link.js';
@@ -271,14 +271,14 @@ if (noteViewInterruptors.length > 0) {
 
 const isRenote = Misskey.note.isPureRenote(note.value);
 
-const rootEl = shallowRef<HTMLElement>();
-const menuButton = shallowRef<HTMLElement>();
-const renoteButton = shallowRef<HTMLElement>();
-const renoteTime = shallowRef<HTMLElement>();
-const reactButton = shallowRef<HTMLElement>();
-const clipButton = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
+const menuButton = useTemplateRef('menuButton');
+const renoteButton = useTemplateRef('renoteButton');
+const renoteTime = useTemplateRef('renoteTime');
+const reactButton = useTemplateRef('reactButton');
+const clipButton = useTemplateRef('clipButton');
 const appearNote = computed(() => getAppearNote(note.value));
-const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
+const galleryEl = useTemplateRef('galleryEl');
 const isMyRenote = $i && ($i.id === note.value.userId);
 const showContent = ref(false);
 const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 6af8acb2df..4f74432041 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -211,7 +211,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
+import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { isLink } from '@@/js/is-link.js';
@@ -290,14 +290,14 @@ if (noteViewInterruptors.length > 0) {
 
 const isRenote = Misskey.note.isPureRenote(note.value);
 
-const rootEl = shallowRef<HTMLElement>();
-const menuButton = shallowRef<HTMLElement>();
-const renoteButton = shallowRef<HTMLElement>();
-const renoteTime = shallowRef<HTMLElement>();
-const reactButton = shallowRef<HTMLElement>();
-const clipButton = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
+const menuButton = useTemplateRef('menuButton');
+const renoteButton = useTemplateRef('renoteButton');
+const renoteTime = useTemplateRef('renoteTime');
+const reactButton = useTemplateRef('reactButton');
+const clipButton = useTemplateRef('clipButton');
 const appearNote = computed(() => getAppearNote(note.value));
-const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
+const galleryEl = useTemplateRef('galleryEl');
 const isMyRenote = $i && ($i.id === note.value.userId);
 const showContent = ref(false);
 const isDeleted = ref(false);
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 62d4350400..cee226ee14 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from 'vue';
+import { useTemplateRef } from 'vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkNote from '@/components/MkNote.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
@@ -46,7 +46,7 @@ const props = defineProps<{
 	disableAutoLoad?: boolean;
 }>();
 
-const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
+const pagingComponent = useTemplateRef('pagingComponent');
 
 defineExpose({
 	pagingComponent,
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index a0fb7fea83..d074dceb2f 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -30,13 +30,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef } from 'vue';
-import type { Ref } from 'vue';
+import { ref, useTemplateRef } from 'vue';
+import { notificationTypes } from '@@/js/const.js';
 import MkSwitch from './MkSwitch.vue';
 import MkInfo from './MkInfo.vue';
 import MkButton from './MkButton.vue';
+import type { Ref } from 'vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
-import { notificationTypes } from '@@/js/const.js';
 import { i18n } from '@/i18n.js';
 
 type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>;
@@ -52,7 +52,7 @@ const props = withDefaults(defineProps<{
 	excludeTypes: () => [],
 });
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 
 const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap);
 
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 9f75a75fb6..08fc846327 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
+import { onUnmounted, onDeactivated, onMounted, computed, useTemplateRef, onActivated } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { notificationTypes } from '@@/js/const.js';
 import MkPagination from '@/components/MkPagination.vue';
@@ -41,7 +41,7 @@ const props = defineProps<{
 	excludeTypes?: typeof notificationTypes[number][];
 }>();
 
-const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
+const pagingComponent = useTemplateRef('pagingComponent');
 
 const pagination = computed(() => prefer.r.useGroupedNotifications.value ? {
 	endpoint: 'i/notifications-grouped' as const,
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index a05176e2f4..04276b47fe 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
+import { onMounted, onUnmounted, useTemplateRef, ref } from 'vue';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
@@ -22,7 +22,7 @@ const props = withDefaults(defineProps<{
 	maxHeight: 200,
 });
 
-const content = shallowRef<HTMLElement>();
+const content = useTemplateRef('content');
 const omitted = ref(false);
 const ignoreOmit = ref(false);
 
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 4fbdc51d22..5cb00c5292 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
+import { computed, onMounted, onUnmounted, provide, ref, useTemplateRef } from 'vue';
 import { url } from '@@/js/config.js';
 import type { PageMetadata } from '@/page.js';
 import RouterView from '@/components/global/RouterView.vue';
@@ -57,7 +57,7 @@ const emit = defineEmits<{
 const windowRouter = createRouter(props.initialPath);
 
 const pageMetadata = ref<null | PageMetadata>(null);
-const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
+const windowEl = useTemplateRef('windowEl');
 const history = ref<{ path: string; }[]>([{
 	path: windowRouter.getCurrentFullPath(),
 }]);
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 071dbba5eb..6a1a91a9f4 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts">
-import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
+import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
 import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
@@ -106,7 +106,7 @@ const emit = defineEmits<{
 	(ev: 'status', error: boolean): void;
 }>();
 
-const rootEl = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
 
 // 遡り中かどうか
 const backed = ref(false);
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index 2068ca39a1..2abf8669ed 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, useTemplateRef, ref } from 'vue';
 import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -54,8 +54,8 @@ const emit = defineEmits<{
 	(ev: 'cancelled'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-const passwordInput = shallowRef<InstanceType<typeof MkInput>>();
+const dialog = useTemplateRef('dialog');
+const passwordInput = useTemplateRef('passwordInput');
 const password = ref('');
 const isBackupCode = ref(false);
 const token = ref<string | null>(null);
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index df664e49f7..232cc005e1 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef } from 'vue';
+import { ref, useTemplateRef } from 'vue';
 import MkModal from './MkModal.vue';
 import MkMenu from './MkMenu.vue';
 import type { MenuItem } from '@/types/menu.js';
@@ -28,7 +28,7 @@ const emit = defineEmits<{
 	(ev: 'closing'): void;
 }>();
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
+const modal = useTemplateRef('modal');
 const manualShowing = ref(true);
 const hiding = ref(false);
 
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 6e941f75be..0d37d973f0 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
+import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import insertTextAtCursor from 'insert-text-at-cursor';
@@ -165,11 +165,11 @@ const emit = defineEmits<{
 	(ev: 'fileChangeSensitive', fileId: string, to: boolean): void;
 }>();
 
-const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
-const cwInputEl = shallowRef<HTMLInputElement | null>(null);
-const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null);
-const visibilityButton = shallowRef<HTMLElement>();
-const otherSettingsButton = shallowRef<HTMLElement>();
+const textareaEl = useTemplateRef('textareaEl');
+const cwInputEl = useTemplateRef('cwInputEl');
+const hashtagsInputEl = useTemplateRef('hashtagsInputEl');
+const visibilityButton = useTemplateRef('visibilityButton');
+const otherSettingsButton = useTemplateRef('otherSettingsButton');
 
 const posting = ref(false);
 const posted = ref(false);
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index 4bef5824fe..c467e29df6 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -25,10 +25,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from 'vue';
+import { useTemplateRef } from 'vue';
+import type { PostFormProps } from '@/types/post-form.js';
 import MkModal from '@/components/MkModal.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
-import type { PostFormProps } from '@/types/post-form.js';
 
 const props = withDefaults(defineProps<PostFormProps & {
 	instant?: boolean;
@@ -42,8 +42,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
-const form = shallowRef<InstanceType<typeof MkPostForm>>();
+const modal = useTemplateRef('modal');
 
 function onPosted() {
 	modal.value?.close({
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index af4e2a4fe0..1fbf00d212 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
-import { i18n } from '@/i18n.js';
+import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
 import { getScrollContainer } from '@@/js/scroll.js';
+import { i18n } from '@/i18n.js';
 import { isHorizontalSwipeSwiping } from '@/utility/touch.js';
 
 const SCROLL_STOP = 10;
@@ -43,7 +43,7 @@ const pullDistance = ref(0);
 let supportPointerDesktop = false;
 let startScreenY: number | null = null;
 
-const rootEl = shallowRef<HTMLDivElement>();
+const rootEl = useTemplateRef('rootEl');
 let scrollEl: HTMLElement | null = null;
 
 let disabled = false;
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 8e5be38e03..118dbbe15a 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
 import { isTouchUsing } from '@/utility/touch.js';
 import * as os from '@/os.js';
 
@@ -58,8 +58,8 @@ const emit = defineEmits<{
 	(ev: 'dragEnded', value: number): void;
 }>();
 
-const containerEl = shallowRef<HTMLElement>();
-const thumbEl = shallowRef<HTMLElement>();
+const containerEl = useTemplateRef('containerEl');
+const thumbEl = useTemplateRef('thumbEl');
 
 const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
 const steppedRawValue = computed(() => {
diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue
index 7551524246..453253f0fc 100644
--- a/packages/frontend/src/components/MkReactionIcon.vue
+++ b/packages/frontend/src/components/MkReactionIcon.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, shallowRef } from 'vue';
+import { defineAsyncComponent, useTemplateRef } from 'vue';
 import { useTooltip } from '@/use/use-tooltip.js';
 import * as os from '@/os.js';
 
@@ -20,7 +20,7 @@ const props = defineProps<{
 	withTooltip?: boolean;
 }>();
 
-const elRef = shallowRef();
+const elRef = useTemplateRef('elRef');
 
 if (props.withTooltip) {
 	useTooltip(elRef, (showing) => {
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 062894314d..590b0f6f19 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, onMounted, shallowRef, watch } from 'vue';
+import { computed, inject, onMounted, useTemplateRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { getUnicodeEmoji } from '@@/js/emojilist.js';
 import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
@@ -50,7 +50,7 @@ const emit = defineEmits<{
 	(ev: 'reactionToggled', emoji: string, newCount: number): void;
 }>();
 
-const buttonEl = shallowRef<HTMLElement>();
+const buttonEl = useTemplateRef('buttonEl');
 
 const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
 const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index a962b37e4a..1ab2397337 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, nextTick, shallowRef, ref } from 'vue';
+import { onMounted, nextTick, useTemplateRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
@@ -23,8 +23,8 @@ import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
-const rootEl = shallowRef<HTMLDivElement | null>(null);
-const chartEl = shallowRef<HTMLCanvasElement | null>(null);
+const rootEl = useTemplateRef('rootEl');
+const chartEl = useTemplateRef('chartEl');
 let chartInstance: Chart | null = null;
 const fetching = ref(true);
 
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index 6fa19efed5..60b3f17b14 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef } from 'vue';
+import { onMounted, useTemplateRef } from 'vue';
 import { Chart } from 'chart.js';
 import tinycolor from 'tinycolor2';
 import { store } from '@/store.js';
@@ -20,7 +20,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 
 initChart();
 
-const chartEl = shallowRef<HTMLCanvasElement | null>(null);
+const chartEl = useTemplateRef('chartEl');
 
 const { handler: externalTooltipHandler } = useChartTooltip();
 
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index a970d1ce00..f122da7468 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
-import { shallowRef } from 'vue';
+import { useTemplateRef } from 'vue';
 import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
 import MkSignin from '@/components/MkSignin.vue';
 import MkModal from '@/components/MkModal.vue';
@@ -46,7 +46,7 @@ const emit = defineEmits<{
 	(ev: 'cancelled'): void;
 }>();
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
+const modal = useTemplateRef('modal');
 
 function onClose() {
 	emit('cancelled');
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 6fb9d77837..5c9047dd43 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, ref } from 'vue';
+import { useTemplateRef, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XSignup from '@/components/MkSignupDialog.form.vue';
 import XServerRules from '@/components/MkSignupDialog.rules.vue';
@@ -52,7 +52,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 
 const isAcceptedServerRule = ref(false);
 
diff --git a/packages/frontend/src/components/MkSparkle.vue b/packages/frontend/src/components/MkSparkle.vue
index b3fc67c0df..2400c5ec7f 100644
--- a/packages/frontend/src/components/MkSparkle.vue
+++ b/packages/frontend/src/components/MkSparkle.vue
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
+import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
 
 const particles = ref<{
 	id: string,
@@ -66,7 +66,7 @@ const particles = ref<{
 	dur: number,
 	color: string
 }[]>([]);
-const el = shallowRef<HTMLElement>();
+const el = useTemplateRef('el');
 const width = ref(0);
 const height = ref(0);
 const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202'];
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 2e1a96c326..f819f82923 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -92,15 +92,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
+import { computed, onMounted, ref, useTemplateRef, toRefs } from 'vue';
 import * as Misskey from 'misskey-js';
-import MkInput from '@/components/MkInput.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
 import type {
 	MkSystemWebhookEditorProps,
 	MkSystemWebhookResult,
 	SystemWebhookEventType,
 } from '@/components/MkSystemWebhookEditor.impl.js';
+import MkInput from '@/components/MkInput.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -122,7 +122,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialogEl = useTemplateRef('dialogEl');
 
 const props = defineProps<MkSystemWebhookEditorProps>();
 
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index 87aa046963..8ad9e14015 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, watch, onBeforeUnmount, ref, shallowRef } from 'vue';
+import { onMounted, watch, onBeforeUnmount, ref, useTemplateRef } from 'vue';
 import tinycolor from 'tinycolor2';
 
 const loaded = !!window.TagCanvas;
@@ -24,9 +24,9 @@ const computedStyle = getComputedStyle(document.documentElement);
 const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
 const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
 const available = ref(false);
-const rootEl = shallowRef<HTMLElement | null>(null);
-const canvasEl = shallowRef<HTMLCanvasElement | null>(null);
-const tagsEl = shallowRef<HTMLElement | null>(null);
+const rootEl = useTemplateRef('rootEl');
+const canvasEl = useTemplateRef('canvasEl');
+const tagsEl = useTemplateRef('tagsEl');
 const width = ref(300);
 
 watch(available, () => {
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index c9da226734..14ffdae663 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, useTemplateRef } from 'vue';
 import { debounce } from 'throttle-debounce';
 import type { SuggestionType } from '@/utility/autocomplete.js';
 import MkButton from '@/components/MkButton.vue';
@@ -75,7 +75,7 @@ const focused = ref(false);
 const changed = ref(false);
 const invalid = ref(false);
 const filled = computed(() => v.value !== '' && v.value != null);
-const inputEl = shallowRef<HTMLTextAreaElement>();
+const inputEl = useTemplateRef('inputEl');
 const preview = ref(false);
 let autocompleteWorker: Autocomplete | null = null;
 
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index e8b740eae2..73057e4644 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
+import { computed, watch, onUnmounted, provide, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { BasicTimelineType } from '@/timelines.js';
 import type { Paging } from '@/components/MkPagination.vue';
@@ -67,8 +67,8 @@ type TimelineQueryType = {
 	roleId?: string
 };
 
-const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>();
-const tlComponent = shallowRef<InstanceType<typeof MkNotes>>();
+const prComponent = useTemplateRef('prComponent');
+const tlComponent = useTemplateRef('tlComponent');
 
 let tlNotesCount = 0;
 
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index bed15031cb..b449155edb 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, ref } from 'vue';
+import { useTemplateRef, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkInput from './MkInput.vue';
 import MkSwitch from './MkSwitch.vue';
@@ -77,7 +77,7 @@ const emit = defineEmits<{
 const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin'));
 const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admin') || p.startsWith('write:admin'));
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 const name = ref(props.initialName);
 const permissionSwitches = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
 const permissionSwitchesForAdmin = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index 8031abd563..ddfa6def87 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue';
+import { nextTick, onMounted, onUnmounted, useTemplateRef } from 'vue';
 import * as os from '@/os.js';
 import { calcPopupPosition } from '@/utility/popup-position.js';
 import { prefer } from '@/preferences.js';
@@ -51,7 +51,7 @@ const emit = defineEmits<{
 // タイミングによっては最初から showing = false な場合があり、その場合に closed 扱いにしないと永久にDOMに残ることになる
 if (!props.showing) emit('closed');
 
-const el = shallowRef<HTMLElement>();
+const el = useTemplateRef('el');
 const zIndex = os.claimZIndex('high');
 
 function setPosition() {
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 5908db33c4..3e91baada4 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -148,7 +148,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, watch } from 'vue';
+import { ref, useTemplateRef, watch } from 'vue';
+import { host } from '@@/js/config.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import XNote from '@/components/MkTutorialDialog.Note.vue';
@@ -158,7 +159,6 @@ import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
 import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { host } from '@@/js/config.js';
 import { claimAchievement } from '@/utility/achievements.js';
 import * as os from '@/os.js';
 
@@ -170,7 +170,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const page = ref(props.initialPage ?? 0);
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index 95732d5796..79ab464cb0 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -15,15 +15,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef } from 'vue';
+import { onMounted, useTemplateRef } from 'vue';
+import { version } from '@@/js/config.js';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
-import { version } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { confetti } from '@/utility/confetti.js';
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
+const modal = useTemplateRef('modal');
 
 function whatIsNew() {
 	modal.value?.close();
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 6bf3eb44dc..057af49a36 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, computed, shallowRef } from 'vue';
+import { onMounted, ref, computed, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { host as currentHost, hostname } from '@@/js/config.js';
 import MkInput from '@/components/MkInput.vue';
@@ -94,7 +94,7 @@ const host = ref('');
 const users = ref<Misskey.entities.UserLite[]>([]);
 const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
 const selected = ref<Misskey.entities.UserLite | null>(null);
-const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialogEl = useTemplateRef('dialogEl');
 
 function search() {
 	if (username.value === '' && host.value === '') {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index eb3a69217e..767f5c591a 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -128,7 +128,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue';
+import { ref, useTemplateRef, watch, nextTick, defineAsyncComponent } from 'vue';
+import { host } from '@@/js/config.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
@@ -137,7 +138,6 @@ import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
 import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { host } from '@@/js/config.js';
 import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
 import { store } from '@/store.js';
 import * as os from '@/os.js';
@@ -146,9 +146,8 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-
-// eslint-disable-next-line vue/no-setup-props-reactivity-loss
+const dialog = useTemplateRef('dialog');
+ 
 const page = ref(store.s.accountSetupWizard);
 
 watch(page, () => {
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 650e639c4f..cb402b1a57 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -42,12 +42,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, shallowRef, ref } from 'vue';
+import { nextTick, useTemplateRef, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkModal from '@/components/MkModal.vue';
 import { i18n } from '@/i18n.js';
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
+const modal = useTemplateRef('modal');
 
 const props = withDefaults(defineProps<{
 	currentVisibility: typeof Misskey.noteVisibilities[number];
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index 4168876f08..2c2c515032 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref, nextTick } from 'vue';
+import { onMounted, useTemplateRef, ref, nextTick } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import tinycolor from 'tinycolor2';
@@ -25,7 +25,7 @@ import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
-const chartEl = shallowRef<HTMLCanvasElement | null>(null);
+const chartEl = useTemplateRef('chartEl');
 const now = new Date();
 let chartInstance: Chart | null = null;
 const chartLimit = 30;
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index 34fa6b0723..282da00ee1 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, shallowRef } from 'vue';
+import { watch, useTemplateRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 
-const modal = shallowRef<InstanceType<typeof MkModal>>();
+const modal = useTemplateRef('modal');
 
 const props = defineProps<{
 	success: boolean;
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 4b3c728ee4..98ec448a8e 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
+import { onBeforeUnmount, onMounted, provide, useTemplateRef, ref } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
 import contains from '@/utility/contains.js';
 import * as os from '@/os.js';
@@ -114,7 +114,7 @@ const emit = defineEmits<{
 
 provide('inWindow', true);
 
-const rootEl = shallowRef<HTMLElement | null>();
+const rootEl = useTemplateRef('rootEl');
 const showing = ref(true);
 let beforeClickedAt = 0;
 const maximized = ref(false);
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 3403418991..336160ec17 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -14,7 +14,7 @@ export type MkABehavior = 'window' | 'browser' | null;
 </script>
 
 <script lang="ts" setup>
-import { computed, inject, shallowRef } from 'vue';
+import { computed, inject, useTemplateRef } from 'vue';
 import { url } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<{
 
 const behavior = props.behavior ?? inject<MkABehavior>('linkNavigationBehavior', null);
 
-const el = shallowRef<HTMLElement>();
+const el = useTemplateRef('el');
 
 defineExpose({ $el: el });
 
diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue
index f35932ae77..b352296469 100644
--- a/packages/frontend/src/components/global/MkLazy.vue
+++ b/packages/frontend/src/components/global/MkLazy.vue
@@ -11,9 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue';
+import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, useTemplateRef } from 'vue';
 
-const rootEl = shallowRef<HTMLDivElement>();
+const rootEl = useTemplateRef('rootEl');
 const showing = ref(false);
 
 const observer = new IntersectionObserver(
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 8a5e556293..81adc07f26 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -53,7 +53,7 @@ export type Tab = {
 </script>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
+import { nextTick, onMounted, onUnmounted, useTemplateRef, watch } from 'vue';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
@@ -69,9 +69,9 @@ const emit = defineEmits<{
 	(ev: 'tabClick', key: string);
 }>();
 
-const el = shallowRef<HTMLElement | null>(null);
+const el = useTemplateRef('el');
+const tabHighlightEl = useTemplateRef('tabHighlightEl');
 const tabRefs: Record<string, HTMLElement | null> = {};
-const tabHighlightEl = shallowRef<HTMLElement | null>(null);
 
 function onTabMousedown(tab: Tab, ev: MouseEvent): void {
 	// ユーザビリティの観点からmousedown時にはonClickは呼ばない
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index b353269fef..4321d69253 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
+import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue';
 import tinycolor from 'tinycolor2';
 import { scrollToTop } from '@@/js/scroll.js';
 import XTabs from './MkPageHeader.tabs.vue';
@@ -75,7 +75,7 @@ const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMe
 const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props.hideTitle);
 const thin_ = props.thin || inject('shouldHeaderThin', false);
 
-const el = shallowRef<HTMLElement | undefined>(undefined);
+const el = useTemplateRef('el');
 const bg = ref<string | undefined>(undefined);
 const narrow = ref(false);
 const hasTabs = computed(() => props.tabs.length > 0);
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 7ee3952083..7dda4b2f8a 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -23,9 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted, provide, inject, ref, watch, useTemplateRef } from 'vue';
-import type { Ref } from 'vue';
-
 import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js';
+import type { Ref } from 'vue';
 
 const rootEl = useTemplateRef('rootEl');
 const headerEl = useTemplateRef('headerEl');
diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue
index f7f6f5c140..7c8a5d64d7 100644
--- a/packages/frontend/src/components/grid/MkDataCell.vue
+++ b/packages/frontend/src/components/grid/MkDataCell.vue
@@ -39,10 +39,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 					{{ cell.value }}
 				</div>
 				<div v-else-if="cellType === 'boolean'">
-					<div :class="[$style.bool, {
-						[$style.boolTrue]: cell.value === true,
-						'ti ti-check': cell.value === true,
-					}]"></div>
+					<div
+						:class="[$style.bool, {
+							[$style.boolTrue]: cell.value === true,
+							'ti ti-check': cell.value === true,
+						}]"
+					></div>
 				</div>
 				<div v-else-if="cellType === 'image'">
 					<img
@@ -88,14 +90,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue';
+import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, useTemplateRef, toRefs, watch } from 'vue';
+import type { Size } from '@/components/grid/grid.js';
+import type { CellValue, GridCell } from '@/components/grid/cell.js';
+import type { GridRowSetting } from '@/components/grid/row.js';
 import { GridEventEmitter } from '@/components/grid/grid.js';
 import { useTooltip } from '@/use/use-tooltip.js';
 import * as os from '@/os.js';
 import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
-import type { Size } from '@/components/grid/grid.js';
-import type { CellValue, GridCell } from '@/components/grid/cell.js';
-import type { GridRowSetting } from '@/components/grid/row.js';
 
 const emit = defineEmits<{
 	(ev: 'operation:beginEdit', sender: GridCell): void;
@@ -111,9 +113,9 @@ const props = defineProps<{
 
 const { cell, bus } = toRefs(props);
 
-const rootEl = shallowRef<InstanceType<typeof HTMLTableCellElement>>();
-const contentAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>();
-const inputAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>();
+const rootEl = useTemplateRef('rootEl');
+const contentAreaEl = useTemplateRef('contentAreaEl');
+const inputAreaEl = useTemplateRef('inputAreaEl');
 
 /** 値が編集中かどうか */
 const editing = ref<boolean>(false);
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 36dac2954d..2d0135e6a6 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onBeforeUnmount, ref, shallowRef, computed } from 'vue';
+import { nextTick, onBeforeUnmount, ref, useTemplateRef, computed } from 'vue';
 import { version } from '@@/js/config.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
@@ -403,7 +403,7 @@ const easterEggEmojis = ref<{
 	emoji: string
 }[]>([]);
 const easterEggEngine = ref<{ stop: () => void } | null>(null);
-const containerEl = shallowRef<HTMLElement>();
+const containerEl = useTemplateRef('containerEl');
 
 function iconLoaded() {
 	const emojis = prefer.s.emojiPalettes[0].emojis;
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index f073a4af7e..819ca2c127 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick, inject } from 'vue';
+import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch, nextTick, inject } from 'vue';
 import tinycolor from 'tinycolor2';
 import { scrollToTop } from '@@/js/scroll.js';
 import { popupMenu } from '@/os.js';
@@ -68,9 +68,9 @@ const emit = defineEmits<{
 
 const pageMetadata = inject(DI.pageMetadata);
 
-const el = shallowRef<HTMLElement>(null);
+const el = useTemplateRef('el');
+const tabHighlightEl = useTemplateRef('tabHighlightEl');
 const tabRefs = {};
-const tabHighlightEl = shallowRef<HTMLElement | null>(null);
 const bg = ref<string | null>(null);
 const height = ref(0);
 const hasTabs = computed(() => {
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index 5b049e5bad..a56a24ff7d 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -71,8 +71,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
+import { computed, onMounted, ref, useTemplateRef, toRefs } from 'vue';
 import { entities } from 'misskey-js';
+import type { MkSystemWebhookResult } from '@/components/MkSystemWebhookEditor.impl.js';
 import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
@@ -80,7 +81,6 @@ import MkInput from '@/components/MkInput.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import MkSelect from '@/components/MkSelect.vue';
 import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
-import type { MkSystemWebhookResult } from '@/components/MkSystemWebhookEditor.impl.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkDivider from '@/components/MkDivider.vue';
 import * as os from '@/os.js';
@@ -100,7 +100,7 @@ const props = defineProps<{
 
 const { mode, id } = toRefs(props);
 
-const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialogEl = useTemplateRef('dialogEl');
 
 const loading = ref<number>(0);
 
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index e4119bb62d..08e06ce4b4 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -59,7 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, shallowRef, ref } from 'vue';
+import { computed, useTemplateRef, ref } from 'vue';
 import XHeader from './_header_.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkPagination from '@/components/MkPagination.vue';
@@ -70,7 +70,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { store } from '@/store.js';
 
-const reports = shallowRef<InstanceType<typeof MkPagination>>();
+const reports = useTemplateRef('reports');
 
 const state = ref('unresolved');
 const reporterOrigin = ref('combined');
diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue
index 069ed6e1f3..6e6476b027 100644
--- a/packages/frontend/src/pages/admin/invites.vue
+++ b/packages/frontend/src/pages/admin/invites.vue
@@ -55,8 +55,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, shallowRef } from 'vue';
+import { computed, ref, useTemplateRef } from 'vue';
 import XHeader from './_header_.vue';
+import type { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -66,11 +67,10 @@ import MkSelect from '@/components/MkSelect.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkPagination from '@/components/MkPagination.vue';
-import type { Paging } from '@/components/MkPagination.vue';
 import MkInviteCode from '@/components/MkInviteCode.vue';
 import { definePage } from '@/page.js';
 
-const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
+const pagingComponent = useTemplateRef('pagingComponent');
 
 const type = ref('all');
 const sort = ref('+createdAt');
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index ae7022a1c9..1fb2c4b726 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, shallowRef, ref } from 'vue';
+import { computed, useTemplateRef, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XHeader from './_header_.vue';
 import XModLog from './modlog.ModLog.vue';
@@ -41,7 +41,7 @@ import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 
-const logs = shallowRef<InstanceType<typeof MkPagination>>();
+const logs = useTemplateRef('logs');
 
 const type = ref<string | null>(null);
 const moderatorId = ref('');
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 5c50336b2c..5b7f669f6b 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, useTemplateRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -24,7 +24,7 @@ import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = useTemplateRef('chartEl');
 const now = new Date();
 let chartInstance: Chart = null;
 const chartLimit = 7;
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index 1949201ca0..4c06d94d6d 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, useTemplateRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import isChromatic from 'chromatic';
@@ -34,8 +34,8 @@ import { initChart } from '@/utility/init-chart.js';
 initChart();
 
 const chartLimit = 50;
-const chartEl = shallowRef<HTMLCanvasElement>();
-const chartEl2 = shallowRef<HTMLCanvasElement>();
+const chartEl = useTemplateRef('chartEl');
+const chartEl2 = useTemplateRef('chartEl2');
 const fetching = ref(true);
 
 const { handler: externalTooltipHandler } = useChartTooltip();
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index 424bcdd51f..32dd981ca9 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef } from 'vue';
+import { onMounted, useTemplateRef } from 'vue';
 import { Chart } from 'chart.js';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { initChart } from '@/utility/init-chart.js';
@@ -26,7 +26,7 @@ const props = defineProps<{
 	data: InstanceForPie[];
 }>();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = useTemplateRef('chartEl');
 
 const { handler: externalTooltipHandler } = useChartTooltip({
 	position: 'middle',
diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue
index 34c0945ddb..6fc941a848 100644
--- a/packages/frontend/src/pages/admin/overview.queue.chart.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef } from 'vue';
+import { onMounted, useTemplateRef } from 'vue';
 import { Chart } from 'chart.js';
 import { store } from '@/store.js';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
@@ -22,7 +22,7 @@ const props = defineProps<{
 	type: string;
 }>();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = useTemplateRef('chartEl');
 
 const { handler: externalTooltipHandler } = useChartTooltip();
 
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index de6b254412..cf07cddced 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
+import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import XChart from './overview.queue.chart.vue';
 import type { ApQueueDomain } from '@/pages/admin/queue.vue';
@@ -48,10 +48,10 @@ const activeSincePrevTick = ref(0);
 const active = ref(0);
 const delayed = ref(0);
 const waiting = ref(0);
-const chartProcess = shallowRef<InstanceType<typeof XChart>>();
-const chartActive = shallowRef<InstanceType<typeof XChart>>();
-const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
-const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
+const chartProcess = useTemplateRef('chartProcess');
+const chartActive = useTemplateRef('chartActive');
+const chartDelayed = useTemplateRef('chartDelayed');
+const chartWaiting = useTemplateRef('chartWaiting');
 
 const props = defineProps<{
 	domain: ApQueueDomain;
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index c23662572a..616815a6a6 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { markRaw, onMounted, onBeforeUnmount, nextTick, shallowRef, ref, computed } from 'vue';
+import { markRaw, onMounted, onBeforeUnmount, nextTick, shallowRef, ref, computed, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import XFederation from './overview.federation.vue';
 import XInstances from './overview.instances.vue';
@@ -85,7 +85,7 @@ import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 
-const rootEl = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
 const serverInfo = ref<Misskey.entities.ServerInfoResponse | null>(null);
 const topSubInstancesForPie = ref<InstanceForPie[] | null>(null);
 const topPubInstancesForPie = ref<InstanceForPie[] | null>(null);
diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue
index 9c7a83b1fb..5dd2887024 100644
--- a/packages/frontend/src/pages/admin/queue.chart.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef } from 'vue';
+import { onMounted, useTemplateRef } from 'vue';
 import { Chart } from 'chart.js';
 import { store } from '@/store.js';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
@@ -22,7 +22,7 @@ const props = defineProps<{
 	type: string;
 }>();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = useTemplateRef('chartEl');
 
 const { handler: externalTooltipHandler } = useChartTooltip();
 
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 607a974d20..1ba02d6e0e 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
+import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import XChart from './queue.chart.chart.vue';
 import type { ApQueueDomain } from '@/pages/admin/queue.vue';
@@ -65,10 +65,10 @@ const active = ref(0);
 const delayed = ref(0);
 const waiting = ref(0);
 const jobs = ref<Misskey.Endpoints[`admin/queue/${ApQueueDomain}-delayed`]['res']>([]);
-const chartProcess = shallowRef<InstanceType<typeof XChart>>();
-const chartActive = shallowRef<InstanceType<typeof XChart>>();
-const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
-const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
+const chartProcess = useTemplateRef('chartProcess');
+const chartActive = useTemplateRef('chartActive');
+const chartDelayed = useTemplateRef('chartDelayed');
+const chartWaiting = useTemplateRef('chartWaiting');
 
 const props = defineProps<{
 	domain: ApQueueDomain;
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index 1998144c05..a44951a947 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, shallowRef, ref, watchEffect } from 'vue';
+import { computed, useTemplateRef, ref, watchEffect } from 'vue';
 import XHeader from './_header_.vue';
 import { defaultMemoryStorage } from '@/memory-storage';
 import MkButton from '@/components/MkButton.vue';
@@ -82,7 +82,7 @@ type SearchQuery = {
 	hostname?: string;
 };
 
-const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
+const paginationComponent = useTemplateRef('paginationComponent');
 const storedQuery = JSON.parse(defaultMemoryStorage.getItem('admin-users-query') ?? '{}') as SearchQuery;
 
 const sort = ref(storedQuery.sort ?? '+createdAt');
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index f0587a5ca0..5f98ed798f 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref, shallowRef } from 'vue';
+import { computed, watch, ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
-import MkTimeline from '@/components/MkTimeline.vue';
 import { scroll } from '@@/js/scroll.js';
+import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { definePage } from '@/page.js';
@@ -42,8 +42,8 @@ const props = defineProps<{
 
 const antenna = ref<Misskey.entities.Antenna | null>(null);
 const queue = ref(0);
-const rootEl = shallowRef<HTMLElement>();
-const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
+const rootEl = useTemplateRef('rootEl');
+const tlEl = useTemplateRef('tlEl');
 
 function queueUpdated(q) {
 	queue.value = q;
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 46e7a3d05e..c714ccb003 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
+import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkPagination from '@/components/MkPagination.vue';
@@ -86,7 +86,7 @@ import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 
-const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
+const emojisPaginationComponent = useTemplateRef('emojisPaginationComponent');
 
 const tab = ref('local');
 const query = ref<string | null>(null);
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index e30e381ce1..baae888a94 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -191,7 +191,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch, useTemplateRef } from 'vue';
 import * as Matter from 'matter-js';
 import * as Misskey from 'misskey-js';
 import { DropAndFusionGame } from 'misskey-bubble-game';
@@ -567,8 +567,8 @@ let game = new DropAndFusionGame({
 });
 attachGameEvents();
 
-const containerEl = shallowRef<HTMLElement>();
-const canvasEl = shallowRef<HTMLCanvasElement>();
+const containerEl = useTemplateRef('containerEl');
+const canvasEl = useTemplateRef('canvasEl');
 const dropperX = ref(0);
 const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null);
 const stock = shallowRef<{ id: string; mono: Mono }[]>([]);
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index c112045a21..c0618b9fce 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, ref, shallowRef, computed } from 'vue';
+import { watch, ref, useTemplateRef, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkUserList from '@/components/MkUserList.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
@@ -77,7 +77,7 @@ const props = defineProps<{
 }>();
 
 const origin = ref('local');
-const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>();
+const tagsEl = useTemplateRef('tagsEl');
 const tagsLocal = ref<Misskey.entities.Hashtag[]>([]);
 const tagsRemote = ref<Misskey.entities.Hashtag[]>([]);
 
diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue
index d24ebe6aae..b76f6a6033 100644
--- a/packages/frontend/src/pages/explore.vue
+++ b/packages/frontend/src/pages/explore.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref, shallowRef } from 'vue';
+import { computed, watch, ref, useTemplateRef } from 'vue';
 import XFeatured from './explore.featured.vue';
 import XUsers from './explore.users.vue';
 import XRoles from './explore.roles.vue';
@@ -38,7 +38,7 @@ const props = withDefaults(defineProps<{
 });
 
 const tab = ref(props.initialTab);
-const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>();
+const tagsEl = useTemplateRef('tagsEl');
 
 watch(() => props.tag, () => {
 	if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index f44d2364af..1ea501c34c 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
-import { shallowRef, computed, ref } from 'vue';
+import { useTemplateRef, computed, ref } from 'vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -57,7 +57,7 @@ import { infoImageUrl } from '@/instance.js';
 import { $i } from '@/i.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
-const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
+const paginationComponent = useTemplateRef('paginationComponent');
 
 const pagination = computed<Paging>(() => tab.value === 'list' ? {
 	endpoint: 'following/requests/list',
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index a6f0dfcf4b..9181e7f2ef 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, shallowRef } from 'vue';
+import { computed, ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
@@ -47,7 +47,7 @@ import { definePage } from '@/page.js';
 import { serverErrorImageUrl, instance } from '@/instance.js';
 import { $i } from '@/i.js';
 
-const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
+const pagingComponent = useTemplateRef('pagingComponent');
 const currentInviteLimit = ref<null | number>(null);
 const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number;
 const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number;
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index 247720d9ac..d354221ca3 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, ref, shallowRef, computed } from 'vue';
+import { watch, ref, useTemplateRef, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -46,7 +46,7 @@ const tab = ref('my');
 
 const favorites = ref<Misskey.entities.Clip[] | null>(null);
 
-const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
+const pagingComponent = useTemplateRef('pagingComponent');
 
 watch(tab, async () => {
 	favorites.value = await misskeyApi('clips/my-favorites');
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index a8b4da2a06..4a980ce472 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -15,8 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-
-import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
+import { watch, ref, useTemplateRef, onMounted, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import XContainer from '../page-editor.container.vue';
 import { i18n } from '@/i18n.js';
@@ -33,7 +32,7 @@ const emit = defineEmits<{
 let autocomplete: Autocomplete;
 
 const text = ref(props.modelValue.text ?? '');
-const inputEl = shallowRef<HTMLTextAreaElement | null>(null);
+const inputEl = useTemplateRef('inputEl');
 
 watch(text, () => {
 	emit('update:modelValue', {
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index c88f7fbe15..2a07a9a21c 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -106,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, ref } from 'vue';
+import { useTemplateRef, ref } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
@@ -132,7 +132,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 const page = ref(0);
 const token = ref<string | number | null>(null);
 const backupCodes = ref<string[]>();
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 335ecedfa8..0f02d95d71 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, ref, computed } from 'vue';
+import { useTemplateRef, ref, computed } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
@@ -82,7 +82,7 @@ const emit = defineEmits<{
 	(ev: 'detach'): void;
 }>();
 
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = useTemplateRef('dialog');
 const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
 const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
 const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 89dc9581c2..c315e9289a 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { computed, onActivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import { computed, onActivated, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
 import type { PageMetadata } from '@/page.js';
 import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
 import { i18n } from '@/i18n.js';
@@ -56,7 +56,7 @@ const indexInfo = {
 	hideHeader: true,
 };
 const INFO = ref<PageMetadata>(indexInfo);
-const el = shallowRef<HTMLElement | null>(null);
+const el = useTemplateRef('el');
 const childInfo = ref<null | PageMetadata>(null);
 
 const router = useRouter();
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index f059050621..530b63b701 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, computed } from 'vue';
+import { useTemplateRef, computed } from 'vue';
 import { notificationTypes } from '@@/js/const.js';
 import XNotificationConfig from './notifications.notification-config.vue';
 import type { NotificationConfig } from './notifications.notification-config.vue';
@@ -88,7 +88,7 @@ const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequest
 
 const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login', 'createToken'] satisfies (typeof notificationTypes[number])[] as string[];
 
-const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
+const allowButton = useTemplateRef('allowButton');
 const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
 const sendReadMessage = computed(() => pushRegistrationInServer.value?.sendReadMessage || false);
 const userLists = await misskeyApi('users/lists/list');
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 68f4b7a26d..38497e7c1b 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
+import { computed, watch, provide, useTemplateRef, ref, onMounted, onActivated } from 'vue';
 import { scroll } from '@@/js/scroll.js';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
 import type { MenuItem } from '@/types/menu.js';
@@ -59,8 +59,8 @@ import { prefer } from '@/preferences.js';
 
 provide('shouldOmitHeaderTitle', true);
 
-const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
-const rootEl = shallowRef<HTMLElement>();
+const tlComponent = useTemplateRef('tlComponent');
+const rootEl = useTemplateRef('rootEl');
 
 type TimelinePageSrc = BasicTimelineType | `list:${string}`;
 
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index e4857c7d30..be84fd6efe 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref, shallowRef } from 'vue';
+import { computed, watch, ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
-import MkTimeline from '@/components/MkTimeline.vue';
 import { scroll } from '@@/js/scroll.js';
+import MkTimeline from '@/components/MkTimeline.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
@@ -41,8 +41,8 @@ const props = defineProps<{
 
 const list = ref<Misskey.entities.UserList | null>(null);
 const queue = ref(0);
-const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
-const rootEl = shallowRef<HTMLElement>();
+const tlEl = useTemplateRef('tlEl');
+const rootEl = useTemplateRef('rootEl');
 
 watch(() => props.listId, async () => {
 	list.value = await misskeyApi('users/lists/show', {
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index 3b5e9617d5..f5d2002669 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -14,11 +14,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, useTemplateRef, ref } from 'vue';
 import { Chart } from 'chart.js';
-import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
+import type { ChartDataset } from 'chart.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
@@ -33,8 +33,8 @@ const props = defineProps<{
 	user: Misskey.entities.User;
 }>();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
-const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
+const chartEl = useTemplateRef('chartEl');
+const legendEl = useTemplateRef('legendEl');
 const now = new Date();
 let chartInstance: Chart = null;
 const chartLimit = 30;
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index af8e4d43a6..01c62810d4 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -14,11 +14,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, useTemplateRef, ref } from 'vue';
 import { Chart } from 'chart.js';
-import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
+import type { ChartDataset } from 'chart.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
@@ -33,8 +33,8 @@ const props = defineProps<{
 	user: Misskey.entities.User;
 }>();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
-const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
+const chartEl = useTemplateRef('chartEl');
+const legendEl = useTemplateRef('legendEl');
 const now = new Date();
 let chartInstance: Chart = null;
 const chartLimit = 50;
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index 32e4e78d21..ed12b1b5c7 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -14,11 +14,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, useTemplateRef, ref } from 'vue';
 import { Chart } from 'chart.js';
-import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
+import type { ChartDataset } from 'chart.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
@@ -33,8 +33,8 @@ const props = defineProps<{
 	user: Misskey.entities.User;
 }>();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
-const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
+const chartEl = useTemplateRef('chartEl');
+const legendEl = useTemplateRef('legendEl');
 const now = new Date();
 let chartInstance: Chart = null;
 const chartLimit = 30;
diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue
index 8fb84fd58f..680fe08c14 100644
--- a/packages/frontend/src/pages/welcome.timeline.note.vue
+++ b/packages/frontend/src/pages/welcome.timeline.note.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, onUpdated, onMounted } from 'vue';
+import { ref, useTemplateRef, onUpdated, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
 import MkMediaList from '@/components/MkMediaList.vue';
@@ -45,7 +45,7 @@ defineProps<{
 	note: Misskey.entities.Note;
 }>();
 
-const noteTextEl = shallowRef<HTMLDivElement>();
+const noteTextEl = useTemplateRef('noteTextEl');
 const shouldCollapse = ref(false);
 const showContent = ref(false);
 
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 180b2e5f77..6108a8a698 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -21,15 +21,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
-import { onUpdated, ref, shallowRef } from 'vue';
+import { onUpdated, ref, useTemplateRef } from 'vue';
+import { getScrollContainer } from '@@/js/scroll.js';
 import XNote from '@/pages/welcome.timeline.note.vue';
 import { misskeyApiGet } from '@/utility/misskey-api.js';
-import { getScrollContainer } from '@@/js/scroll.js';
 
 const notes = ref<Misskey.entities.Note[]>([]);
 const isScrolling = ref(false);
 const scrollState = ref<null | 'intro' | 'loop'>(null);
-const notesMainContainerEl = shallowRef<HTMLElement>();
+const notesMainContainerEl = useTemplateRef('notesMainContainerEl');
 
 misskeyApiGet('notes/featured').then(_notes => {
 	notes.value = _notes;
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 8200b6ad38..43d24da28b 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, computed, watch, ref, shallowRef } from 'vue';
+import { defineAsyncComponent, computed, watch, ref, useTemplateRef } from 'vue';
 import { openInstanceMenu } from './_common_/common.js';
 // import { host } from '@@/js/config.js';
 import * as os from '@/os.js';
@@ -76,7 +76,7 @@ const otherNavItemIndicated = computed<boolean>(() => {
 	}
 	return false;
 });
-const el = shallowRef<HTMLElement>();
+const el = useTemplateRef('el');
 // let accounts = $ref([]);
 // let connection = $ref(null);
 const iconOnly = ref(false);
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 2ef06726f9..c17e78bb03 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue';
+import { defineAsyncComponent, onMounted, provide, ref, computed, useTemplateRef } from 'vue';
 import { instanceName } from '@@/js/config.js';
 import { isLink } from '@@/js/is-link.js';
 import XSidebar from './classic.sidebar.vue';
@@ -77,7 +77,7 @@ const fullView = ref(false);
 const globalHeaderHeight = ref(0);
 const wallpaper = miLocalStorage.getItem('wallpaper') != null;
 const showMenuOnTop = computed(() => store.s.menuDisplay === 'top');
-const live2d = shallowRef<HTMLIFrameElement>();
+const live2d = useTemplateRef('live2d');
 const widgetsLeft = ref<HTMLElement>();
 const widgetsRight = ref<HTMLElement>();
 
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 4e9ec7c586..4c33d7cb27 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -92,7 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue';
+import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue';
 import { v4 as uuid } from 'uuid';
 import XCommon from './_common_/common.vue';
 import XSidebar from '@/ui/_common_/navbar.vue';
@@ -169,7 +169,7 @@ function showSettings() {
 	os.pageWindow('/settings/deck');
 }
 
-const columnsEl = shallowRef<HTMLElement>();
+const columnsEl = useTemplateRef('columnsEl');
 
 const addColumn = async (ev) => {
 	const { canceled, result: column } = await os.select({
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index ae282ba324..194b56c842 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
+import { onMounted, ref, useTemplateRef, watch, defineAsyncComponent } from 'vue';
 import XColumn from './column.vue';
 import type { entities as MisskeyEntities } from 'misskey-js';
 import type { Column } from '@/deck.js';
@@ -34,7 +34,7 @@ const props = defineProps<{
 	isStacked: boolean;
 }>();
 
-const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
+const timeline = useTemplateRef('timeline');
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
 const antennaName = ref<string | null>(null);
 
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index 964b42874e..c2644da707 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, shallowRef, watch } from 'vue';
+import { onMounted, ref, shallowRef, watch, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import XColumn from './column.vue';
 import type { Column } from '@/deck.js';
@@ -40,7 +40,7 @@ const props = defineProps<{
 	isStacked: boolean;
 }>();
 
-const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
+const timeline = useTemplateRef('timeline');
 const channel = shallowRef<Misskey.entities.Channel>();
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
 
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 3358b4e52b..4841a78323 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } from 'vue';
+import { onBeforeUnmount, onMounted, provide, watch, useTemplateRef, ref, computed } from 'vue';
 import type { Column } from '@/deck.js';
 import type { MenuItem } from '@/types/menu.js';
 import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from '@/deck.js';
@@ -68,7 +68,7 @@ const emit = defineEmits<{
 	(ev: 'headerWheel', ctx: WheelEvent): void;
 }>();
 
-const body = shallowRef<HTMLDivElement | null>();
+const body = useTemplateRef('body');
 
 const dragging = ref(false);
 watch(dragging, v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 2b4e86b8a2..a8f17feb23 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, shallowRef, ref, onMounted } from 'vue';
+import { watch, useTemplateRef, ref, onMounted } from 'vue';
 import XColumn from './column.vue';
 import type { entities as MisskeyEntities } from 'misskey-js';
 import type { Column } from '@/deck.js';
@@ -34,7 +34,7 @@ const props = defineProps<{
 	isStacked: boolean;
 }>();
 
-const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
+const timeline = useTemplateRef('timeline');
 const withRenotes = ref(props.column.withRenotes ?? true);
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
 const listName = ref<string | null>(null);
diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue
index 0a2c0e9952..8378dddfef 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, shallowRef } from 'vue';
+import { defineAsyncComponent, useTemplateRef } from 'vue';
 import XColumn from './column.vue';
-import { updateColumn } from '@/deck.js';
 import type { Column } from '@/deck.js';
+import { updateColumn } from '@/deck.js';
 import XNotifications from '@/components/MkNotifications.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
@@ -25,7 +25,7 @@ const props = defineProps<{
 	isStacked: boolean;
 }>();
 
-const notificationsComponent = shallowRef<InstanceType<typeof XNotifications>>();
+const notificationsComponent = useTemplateRef('notificationsComponent');
 
 function func() {
 	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 5d07440017..468b3e49e0 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, ref, shallowRef, watch } from 'vue';
+import { onMounted, ref, useTemplateRef, watch } from 'vue';
 import XColumn from './column.vue';
 import type { Column } from '@/deck.js';
 import type { MenuItem } from '@/types/menu.js';
@@ -32,7 +32,7 @@ const props = defineProps<{
 	isStacked: boolean;
 }>();
 
-const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
+const timeline = useTemplateRef('timeline');
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
 const roleName = ref<string | null>(null);
 
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 2dec7bf5aa..6759135654 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, watch, ref, shallowRef, computed } from 'vue';
+import { onMounted, watch, ref, useTemplateRef, computed } from 'vue';
 import XColumn from './column.vue';
 import type { Column } from '@/deck.js';
 import type { MenuItem } from '@/types/menu.js';
@@ -50,7 +50,7 @@ const props = defineProps<{
 	isStacked: boolean;
 }>();
 
-const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
+const timeline = useTemplateRef('timeline');
 
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
 const withRenotes = ref(props.column.withRenotes ?? true);
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 133360972b..d85745bde6 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -95,7 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef } from 'vue';
+import { defineAsyncComponent, provide, onMounted, computed, ref, watch, useTemplateRef } from 'vue';
 import { instanceName } from '@@/js/config.js';
 import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js';
 import { isLink } from '@@/js/is-link.js';
@@ -135,7 +135,7 @@ window.addEventListener('resize', () => {
 
 const pageMetadata = ref<null | PageMetadata>(null);
 const widgetsShowing = ref(false);
-const navFooter = shallowRef<HTMLElement>();
+const navFooter = useTemplateRef('navFooter');
 
 provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {
diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue
index 29e21ee6c3..2bc7facc88 100644
--- a/packages/frontend/src/widgets/WidgetAichan.vue
+++ b/packages/frontend/src/widgets/WidgetAichan.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, shallowRef } from 'vue';
+import { onMounted, onUnmounted, useTemplateRef } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
 import type { GetFormResultType } from '@/utility/form.js';
@@ -35,7 +35,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const live2d = shallowRef<HTMLIFrameElement>();
+const live2d = useTemplateRef('live2d');
 
 const touched = () => {
 	//if (this.live2d) this.live2d.changeExpression('gurugurume');
diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
index c6724127fa..0c9f98f9e3 100644
--- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkContainer :naked="widgetProps.transparent" :showHeader="false" class="mkw-instance-cloud">
 	<div class="">
-		<MkTagCloud v-if="activeInstances">
+		<MkTagCloud v-if="activeInstances" ref="cloud">
 			<li v-for="instance in activeInstances" :key="instance.id">
 				<a @click.prevent="onInstanceClick(instance)">
 					<img style="width: 32px;" :src="getInstanceIcon(instance)">
@@ -18,8 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from 'vue';
+import { shallowRef, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import type { GetFormResultType } from '@/utility/form.js';
@@ -27,7 +28,6 @@ import MkContainer from '@/components/MkContainer.vue';
 import MkTagCloud from '@/components/MkTagCloud.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const name = 'instanceCloud';
@@ -50,7 +50,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const cloud = shallowRef<InstanceType<typeof MkTagCloud> | null>();
+const cloud = useTemplateRef('cloud');
 const activeInstances = shallowRef<Misskey.entities.FederationInstance[] | null>(null);
 
 function onInstanceClick(i) {
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 2cbf3a8ef6..2ccbb7a28f 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -17,14 +17,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, shallowRef } from 'vue';
+import { onMounted, ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'slideshow';
@@ -54,8 +54,8 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
 
 const images = ref<Misskey.entities.DriveFile[]>([]);
 const fetching = ref(true);
-const slideA = shallowRef<HTMLElement>();
-const slideB = shallowRef<HTMLElement>();
+const slideA = useTemplateRef('slideA');
+const slideB = useTemplateRef('slideB');
 
 const change = () => {
 	if (images.value.length === 0) return;
-- 
cgit v1.2.3-freya

