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/widgets/WidgetAiscript.vue | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

(limited to 'packages/frontend/src/widgets/WidgetAiscript.vue')

diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index cf7e9c5a3e..80573d2dc4 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import { Interpreter, Parser, utils } from '@syuilo/aiscript';
-import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import { GetFormResultType } from '@/scripts/form.js';
+import { useWidgetPropsManager } from './widget.js';
+import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
+import type { GetFormResultType } from '@/scripts/form.js';
 import * as os from '@/os.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
-- 
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/widgets/WidgetAiscript.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 8c9ec5827fa2040c8d705d2a01329da593d19fa3 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 22:12:23 +0900
Subject: enhance(frontend): improve accounts management

---
 packages/frontend/src/account.ts                   | 390 ---------------------
 packages/frontend/src/accounts.ts                  | 341 ++++++++++++++++++
 packages/frontend/src/aiscript/api.ts              |   2 +-
 packages/frontend/src/boot/common.ts               |  10 +-
 packages/frontend/src/boot/main-boot.ts            |  24 +-
 .../src/components/MkAnnouncementDialog.vue        |   5 +-
 packages/frontend/src/components/MkAuthConfirm.vue |   9 +-
 packages/frontend/src/components/MkClipPreview.vue |   2 +-
 .../frontend/src/components/MkCropperDialog.vue    |   2 +-
 packages/frontend/src/components/MkDrive.file.vue  |   2 +-
 packages/frontend/src/components/MkEmojiPicker.vue |   2 +-
 .../frontend/src/components/MkFollowButton.vue     |   2 +-
 .../frontend/src/components/MkInstanceStats.vue    |   2 +-
 packages/frontend/src/components/MkMediaAudio.vue  |   2 +-
 packages/frontend/src/components/MkMediaImage.vue  |   2 +-
 packages/frontend/src/components/MkMediaVideo.vue  |   2 +-
 packages/frontend/src/components/MkMention.vue     |   2 +-
 packages/frontend/src/components/MkNote.vue        |   2 +-
 .../frontend/src/components/MkNoteDetailed.vue     |   2 +-
 packages/frontend/src/components/MkNoteSub.vue     |   2 +-
 .../frontend/src/components/MkNotification.vue     |   2 +-
 .../frontend/src/components/MkPasswordDialog.vue   |   2 +-
 packages/frontend/src/components/MkPostForm.vue    |   5 +-
 packages/frontend/src/components/MkPreview.vue     |   2 +-
 .../components/MkPushNotificationAllowButton.vue   |   3 +-
 .../src/components/MkReactionsViewer.reaction.vue  |   2 +-
 packages/frontend/src/components/MkSignin.vue      |   5 +-
 .../src/components/MkSignupDialog.form.vue         |   4 +-
 packages/frontend/src/components/MkTimeline.vue    |   2 +-
 .../src/components/MkTokenGenerateWindow.vue       |   2 +-
 .../src/components/MkTutorialDialog.Note.vue       |   2 +-
 .../src/components/MkTutorialDialog.Sensitive.vue  |   2 +-
 packages/frontend/src/components/MkUserInfo.vue    |   2 +-
 packages/frontend/src/components/MkUserPopup.vue   |   2 +-
 .../frontend/src/components/MkUserSelectDialog.vue |   2 +-
 .../src/components/MkUserSetupDialog.Profile.vue   |   2 +-
 packages/frontend/src/components/global/MkAd.vue   |   2 +-
 .../src/components/global/MkCustomEmoji.vue        |   2 +-
 .../src/components/global/MkPageHeader.vue         |   3 +-
 packages/frontend/src/i.ts                         |  34 ++
 packages/frontend/src/local-storage.ts             |   1 -
 packages/frontend/src/navbar.ts                    |   2 +-
 packages/frontend/src/pages/about-misskey.vue      |   2 +-
 packages/frontend/src/pages/about.emojis.vue       |   2 +-
 packages/frontend/src/pages/achievements.vue       |   2 +-
 packages/frontend/src/pages/admin-file.vue         |   2 +-
 packages/frontend/src/pages/admin-user.vue         |   2 +-
 packages/frontend/src/pages/announcement.vue       |   5 +-
 packages/frontend/src/pages/announcements.vue      |   5 +-
 packages/frontend/src/pages/auth.vue               |   3 +-
 .../src/pages/avatar-decoration-edit-dialog.vue    |   2 +-
 packages/frontend/src/pages/avatar-decorations.vue |   2 +-
 packages/frontend/src/pages/channel.vue            |   2 +-
 packages/frontend/src/pages/clip.vue               |   2 +-
 .../frontend/src/pages/drop-and-fusion.game.vue    |   2 +-
 packages/frontend/src/pages/emojis.emoji.vue       |   2 +-
 packages/frontend/src/pages/flash/flash.vue        |   2 +-
 packages/frontend/src/pages/follow-requests.vue    |   2 +-
 packages/frontend/src/pages/gallery/post.vue       |   2 +-
 packages/frontend/src/pages/instance-info.vue      |   2 +-
 packages/frontend/src/pages/invite.vue             |   2 +-
 packages/frontend/src/pages/my-lists/index.vue     |   2 +-
 packages/frontend/src/pages/my-lists/list.vue      |   2 +-
 packages/frontend/src/pages/note.vue               |   2 +-
 .../frontend/src/pages/page-editor/page-editor.vue |   2 +-
 packages/frontend/src/pages/page.vue               |   2 +-
 packages/frontend/src/pages/reversi/game.board.vue |   2 +-
 .../frontend/src/pages/reversi/game.setting.vue    |   2 +-
 packages/frontend/src/pages/reversi/game.vue       |   2 +-
 packages/frontend/src/pages/reversi/index.vue      |   2 +-
 packages/frontend/src/pages/scratchpad.vue         |   2 +-
 packages/frontend/src/pages/search.note.vue        |   2 +-
 .../frontend/src/pages/settings/2fa.qrdialog.vue   |   2 +-
 packages/frontend/src/pages/settings/2fa.vue       |   5 +-
 .../frontend/src/pages/settings/account-data.vue   |   2 +-
 packages/frontend/src/pages/settings/accounts.vue  | 103 ++----
 .../settings/avatar-decoration.decoration.vue      |   2 +-
 .../pages/settings/avatar-decoration.dialog.vue    |   2 +-
 .../src/pages/settings/avatar-decoration.vue       |   2 +-
 packages/frontend/src/pages/settings/drive.vue     |   2 +-
 packages/frontend/src/pages/settings/email.vue     |   2 +-
 packages/frontend/src/pages/settings/index.vue     |   3 +-
 packages/frontend/src/pages/settings/migration.vue |   2 +-
 .../pages/settings/mute-block.instance-mute.vue    |   2 +-
 .../frontend/src/pages/settings/mute-block.vue     |   2 +-
 .../frontend/src/pages/settings/notifications.vue  |   2 +-
 packages/frontend/src/pages/settings/other.vue     |   3 +-
 packages/frontend/src/pages/settings/privacy.vue   |   2 +-
 packages/frontend/src/pages/settings/profile.vue   |   2 +-
 packages/frontend/src/pages/signup-complete.vue    |   2 +-
 packages/frontend/src/pages/tag.vue                |   2 +-
 packages/frontend/src/pages/theme-editor.vue       |   2 +-
 packages/frontend/src/pages/timeline.vue           |   2 +-
 packages/frontend/src/pages/user/achievements.vue  |   2 +-
 packages/frontend/src/pages/user/home.vue          |   2 +-
 packages/frontend/src/pages/user/index.vue         |   2 +-
 packages/frontend/src/pages/welcome.setup.vue      |   2 +-
 packages/frontend/src/pizzax.ts                    |   2 +-
 packages/frontend/src/preferences.ts               |   2 +-
 packages/frontend/src/preferences/def.ts           |   4 +
 packages/frontend/src/preferences/manager.ts       |   2 +-
 packages/frontend/src/preferences/utility.ts       |   2 +-
 packages/frontend/src/router/definition.ts         |   2 +-
 packages/frontend/src/signout.ts                   |  54 +++
 packages/frontend/src/store.ts                     |   4 +
 packages/frontend/src/stream.ts                    |   2 +-
 packages/frontend/src/theme-store.ts               |   2 +-
 packages/frontend/src/timelines.ts                 |   2 +-
 .../frontend/src/ui/_common_/PreferenceRestore.vue |   2 +-
 .../frontend/src/ui/_common_/announcements.vue     |   2 +-
 packages/frontend/src/ui/_common_/common.ts        |   2 +-
 packages/frontend/src/ui/_common_/common.vue       |   2 +-
 .../frontend/src/ui/_common_/navbar-for-mobile.vue |   3 +-
 packages/frontend/src/ui/_common_/navbar.vue       |   3 +-
 packages/frontend/src/ui/_common_/sw-inject.ts     |   3 +-
 packages/frontend/src/ui/classic.header.vue        |   4 +-
 packages/frontend/src/ui/classic.sidebar.vue       |   3 +-
 packages/frontend/src/ui/deck.vue                  |   2 +-
 packages/frontend/src/ui/universal.vue             |   2 +-
 packages/frontend/src/use/use-note-capture.ts      |   2 +-
 packages/frontend/src/utility/achievements.ts      |   2 +-
 .../src/utility/autogen/settings-search-index.ts   |   7 +
 packages/frontend/src/utility/check-permissions.ts |   2 +-
 packages/frontend/src/utility/get-note-menu.ts     |   2 +-
 packages/frontend/src/utility/get-user-menu.ts     |   2 +-
 packages/frontend/src/utility/isFfVisibleForMe.ts  |   2 +-
 packages/frontend/src/utility/misskey-api.ts       |   2 +-
 packages/frontend/src/utility/please-login.ts      |   2 +-
 packages/frontend/src/utility/show-moved-dialog.ts |   2 +-
 packages/frontend/src/utility/upload.ts            |   2 +-
 packages/frontend/src/widgets/WidgetActivity.vue   |   2 +-
 packages/frontend/src/widgets/WidgetAiscript.vue   |   2 +-
 .../frontend/src/widgets/WidgetAiscriptApp.vue     |   2 +-
 .../src/widgets/WidgetBirthdayFollowings.vue       |   2 +-
 packages/frontend/src/widgets/WidgetButton.vue     |   2 +-
 packages/frontend/src/widgets/WidgetProfile.vue    |   2 +-
 packages/frontend/test/aiscript/api.test.ts        |   2 +-
 137 files changed, 640 insertions(+), 622 deletions(-)
 delete mode 100644 packages/frontend/src/account.ts
 create mode 100644 packages/frontend/src/accounts.ts
 create mode 100644 packages/frontend/src/i.ts
 create mode 100644 packages/frontend/src/signout.ts

(limited to 'packages/frontend/src/widgets/WidgetAiscript.vue')

diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
deleted file mode 100644
index c90d4da5ec..0000000000
--- a/packages/frontend/src/account.ts
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { defineAsyncComponent, reactive, ref } from 'vue';
-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 '@/utility/show-suspended-dialog.js';
-import { i18n } from '@/i18n.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { del, get, set } from '@/utility/idb-proxy.js';
-import { waiting, popup, popupMenu, success, alert } from '@/os.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
-import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
-
-// TODO: 他のタブと永続化されたstateを同期
-// TODO: accountsはpreferences管理にする(tokenは別管理)
-
-type Account = Misskey.entities.MeDetailed & { token: string };
-
-const accountData = miLocalStorage.getItem('account');
-
-// TODO: 外部からはreadonlyに
-export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
-
-export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
-export const iAmAdmin = $i != null && $i.isAdmin;
-
-export function signinRequired() {
-	if ($i == null) throw new Error('signin required');
-	return $i;
-}
-
-export let notesCount = $i == null ? 0 : $i.notesCount;
-export function incNotesCount() {
-	notesCount++;
-}
-
-export async function signout() {
-	if (!$i) return;
-
-	defaultMemoryStorage.clear();
-
-	waiting();
-	document.cookie.split(';').forEach((cookie) => {
-		const cookieName = cookie.split('=')[0].trim();
-		if (cookieName === 'token') {
-			document.cookie = `${cookieName}=; max-age=0; path=/`;
-		}
-	});
-	miLocalStorage.removeItem('account');
-	await removeAccount($i.id);
-	const accounts = await getAccounts();
-
-	//#region Remove service worker registration
-	try {
-		if (navigator.serviceWorker.controller) {
-			const registration = await navigator.serviceWorker.ready;
-			const push = await registration.pushManager.getSubscription();
-			if (push) {
-				await window.fetch(`${apiUrl}/sw/unregister`, {
-					method: 'POST',
-					body: JSON.stringify({
-						i: $i.token,
-						endpoint: push.endpoint,
-					}),
-					headers: {
-						'Content-Type': 'application/json',
-					},
-				});
-			}
-		}
-
-		if (accounts.length === 0) {
-			await navigator.serviceWorker.getRegistrations()
-				.then(registrations => {
-					return Promise.all(registrations.map(registration => registration.unregister()));
-				});
-		}
-	} catch (err) {}
-	//#endregion
-
-	if (accounts.length > 0) login(accounts[0].token);
-	else unisonReload('/');
-}
-
-export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
-	return (await get('accounts')) || [];
-}
-
-export async function addAccount(id: Account['id'], token: Account['token']) {
-	const accounts = await getAccounts();
-	if (!accounts.some(x => x.id === id)) {
-		await set('accounts', accounts.concat([{ id, token }]));
-	}
-}
-
-export async function removeAccount(idOrToken: Account['id']) {
-	const accounts = await getAccounts();
-	const i = accounts.findIndex(x => x.id === idOrToken || x.token === idOrToken);
-	if (i !== -1) accounts.splice(i, 1);
-
-	if (accounts.length > 0) {
-		await set('accounts', accounts);
-	} else {
-		await del('accounts');
-	}
-}
-
-function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> {
-	document.cookie = 'token=; path=/; max-age=0';
-	document.cookie = `token=${token}; path=/queue; max-age=86400; SameSite=Strict; Secure`; // bull dashboardの認証とかで使う
-
-	return new Promise((done, fail) => {
-		window.fetch(`${apiUrl}/i`, {
-			method: 'POST',
-			body: JSON.stringify({
-				i: token,
-			}),
-			headers: {
-				'Content-Type': 'application/json',
-			},
-		})
-			.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
-				if (res.status >= 500 && res.status < 600) {
-					// サーバーエラー(5xx)の場合をrejectとする
-					// （認証エラーなど4xxはresolve）
-					return fail2(res);
-				}
-				res.json().then(done2, fail2);
-			}))
-			.then(async res => {
-				if ('error' in res) {
-					if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
-						// SUSPENDED
-						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
-							await showSuspendedDialog();
-						}
-					} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
-						// USER_IS_DELETED
-						// アカウントが削除されている
-						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
-							await alert({
-								type: 'error',
-								title: i18n.ts.accountDeleted,
-								text: i18n.ts.accountDeletedDescription,
-							});
-						}
-					} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
-						// AUTHENTICATION_FAILED
-						// トークンが無効化されていたりアカウントが削除されたりしている
-						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
-							await alert({
-								type: 'error',
-								title: i18n.ts.tokenRevoked,
-								text: i18n.ts.tokenRevokedDescription,
-							});
-						}
-					} else {
-						await alert({
-							type: 'error',
-							title: i18n.ts.failedToFetchAccountInformation,
-							text: JSON.stringify(res.error),
-						});
-					}
-
-					// rejectかつ理由がtrueの場合、削除対象であることを示す
-					fail(true);
-				} else {
-					(res as Account).token = token;
-					done(res as Account);
-				}
-			})
-			.catch(fail);
-	});
-}
-
-export function updateAccount(accountData: Account) {
-	if (!$i) return;
-	for (const key of Object.keys($i)) {
-		delete $i[key];
-	}
-	for (const [key, value] of Object.entries(accountData)) {
-		$i[key] = value;
-	}
-	miLocalStorage.setItem('account', JSON.stringify($i));
-}
-
-export function updateAccountPartial(accountData: Partial<Account>) {
-	if (!$i) return;
-	for (const [key, value] of Object.entries(accountData)) {
-		$i[key] = value;
-	}
-	miLocalStorage.setItem('account', JSON.stringify($i));
-}
-
-export async function refreshAccount() {
-	if (!$i) return;
-	return fetchAccount($i.token, $i.id)
-		.then(updateAccount, reason => {
-			if (reason === true) return signout();
-			return;
-		});
-}
-
-export async function login(token: Account['token'], redirect?: string) {
-	const showing = ref(true);
-	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
-		success: false,
-		showing: showing,
-	}, {
-		closed: () => dispose(),
-	});
-	if (_DEV_) console.log('logging as token ', token);
-	const me = await fetchAccount(token, undefined, true)
-		.catch(reason => {
-			if (reason === true) {
-				// 削除対象の場合
-				removeAccount(token);
-			}
-
-			showing.value = false;
-			throw reason;
-		});
-	miLocalStorage.setItem('account', JSON.stringify(me));
-	await addAccount(me.id, token);
-
-	if (redirect) {
-		// 他のタブは再読み込みするだけ
-		reloadChannel.postMessage(null);
-		// このページはredirectで指定された先に移動
-		location.href = redirect;
-		return;
-	}
-
-	unisonReload();
-}
-
-export async function openAccountMenu(opts: {
-	includeCurrentAccount?: boolean;
-	withExtraOperation: boolean;
-	active?: Misskey.entities.UserDetailed['id'];
-	onChoose?: (account: Misskey.entities.UserDetailed) => void;
-}, ev: MouseEvent) {
-	if (!$i) return;
-
-	async function switchAccount(account: Misskey.entities.UserDetailed) {
-		const storedAccounts = await getAccounts();
-		const found = storedAccounts.find(x => x.id === account.id);
-		if (found == null) return;
-		switchAccountWithToken(found.token);
-	}
-
-	function switchAccountWithToken(token: string) {
-		login(token);
-	}
-
-	const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
-	const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) });
-
-	function createItem(account: Misskey.entities.UserDetailed) {
-		return {
-			type: 'user' as const,
-			user: account,
-			active: opts.active != null ? opts.active === account.id : false,
-			action: () => {
-				if (opts.onChoose) {
-					opts.onChoose(account);
-				} else {
-					switchAccount(account);
-				}
-			},
-		};
-	}
-
-	const accountItemPromises = storedAccounts.map(a => new Promise<ReturnType<typeof createItem> | MenuButton>(res => {
-		accountsPromise.then(accounts => {
-			const account = accounts.find(x => x.id === a.id);
-			if (account == null) return res({
-				type: 'button' as const,
-				text: a.id,
-				action: () => {
-					switchAccountWithToken(a.token);
-				},
-			});
-
-			res(createItem(account));
-		});
-	}));
-
-	const menuItems: MenuItem[] = [];
-
-	if (opts.withExtraOperation) {
-		menuItems.push({
-			type: 'link',
-			text: i18n.ts.profile,
-			to: `/@${$i.username}`,
-			avatar: $i,
-		}, {
-			type: 'divider',
-		});
-
-		if (opts.includeCurrentAccount) {
-			menuItems.push(createItem($i));
-		}
-
-		menuItems.push(...accountItemPromises);
-
-		menuItems.push({
-			type: 'parent',
-			icon: 'ti ti-plus',
-			text: i18n.ts.addAccount,
-			children: [{
-				text: i18n.ts.existingAccount,
-				action: () => {
-					getAccountWithSigninDialog().then(res => {
-						if (res != null) {
-							success();
-						}
-					});
-				},
-			}, {
-				text: i18n.ts.createAccount,
-				action: () => {
-					getAccountWithSignupDialog().then(res => {
-						if (res != null) {
-							switchAccountWithToken(res.token);
-						}
-					});
-				},
-			}],
-		}, {
-			type: 'link',
-			icon: 'ti ti-users',
-			text: i18n.ts.manageAccounts,
-			to: '/settings/accounts',
-		});
-	} else {
-		if (opts.includeCurrentAccount) {
-			menuItems.push(createItem($i));
-		}
-
-		menuItems.push(...accountItemPromises);
-	}
-
-	popupMenu(menuItems, ev.currentTarget ?? ev.target, {
-		align: 'left',
-	});
-}
-
-export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
-	return new Promise((resolve) => {
-		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
-			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
-				await addAccount(res.id, res.i);
-				resolve({ id: res.id, token: res.i });
-			},
-			cancelled: () => {
-				resolve(null);
-			},
-			closed: () => {
-				dispose();
-			},
-		});
-	});
-}
-
-export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
-	return new Promise((resolve) => {
-		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
-			done: async (res: Misskey.entities.SignupResponse) => {
-				await addAccount(res.id, res.token);
-				resolve({ id: res.id, token: res.token });
-			},
-			cancelled: () => {
-				resolve(null);
-			},
-			closed: () => {
-				dispose();
-			},
-		});
-	});
-}
-
-if (_DEV_) {
-	(window as any).$i = $i;
-}
diff --git a/packages/frontend/src/accounts.ts b/packages/frontend/src/accounts.ts
new file mode 100644
index 0000000000..2382a8ec32
--- /dev/null
+++ b/packages/frontend/src/accounts.ts
@@ -0,0 +1,341 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { apiUrl, host } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
+import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
+import { i18n } from '@/i18n.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { waiting, popup, popupMenu, success, alert } from '@/os.js';
+import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
+import { prefer } from '@/preferences.js';
+import { store } from '@/store.js';
+import { $i } from '@/i.js';
+import { signout } from '@/signout.js';
+
+// TODO: 他のタブと永続化されたstateを同期
+
+type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
+
+export async function getAccounts(): Promise<{
+	host: string;
+	user: Misskey.entities.User;
+	token: string | null;
+}[]> {
+	const tokens = store.s.accountTokens;
+	const accounts = prefer.s.accounts;
+	return accounts.map(([host, user]) => ({
+		host,
+		user,
+		token: tokens[host + '/' + user.id] ?? null,
+	}));
+}
+
+async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) {
+	if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
+		store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
+		prefer.commit('accounts', [...prefer.s.accounts, [host, user]]);
+	}
+}
+
+export async function removeAccount(host: string, id: AccountWithToken['id']) {
+	const tokens = JSON.parse(JSON.stringify(store.s.accountTokens));
+	delete tokens[host + '/' + id];
+	store.set('accountTokens', tokens);
+	prefer.commit('accounts', prefer.s.accounts.filter(x => x[0] !== host || x[1].id !== id));
+}
+
+const isAccountDeleted = Symbol('isAccountDeleted');
+
+function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Misskey.entities.MeDetailed> {
+	return new Promise((done, fail) => {
+		window.fetch(`${apiUrl}/i`, {
+			method: 'POST',
+			body: JSON.stringify({
+				i: token,
+			}),
+			headers: {
+				'Content-Type': 'application/json',
+			},
+		})
+			.then(res => new Promise<Misskey.entities.MeDetailed | { error: Record<string, any> }>((done2, fail2) => {
+				if (res.status >= 500 && res.status < 600) {
+					// サーバーエラー(5xx)の場合をrejectとする
+					// （認証エラーなど4xxはresolve）
+					return fail2(res);
+				}
+				res.json().then(done2, fail2);
+			}))
+			.then(async res => {
+				if ('error' in res) {
+					if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
+						// SUSPENDED
+						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
+							await showSuspendedDialog();
+						}
+					} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
+						// USER_IS_DELETED
+						// アカウントが削除されている
+						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
+							await alert({
+								type: 'error',
+								title: i18n.ts.accountDeleted,
+								text: i18n.ts.accountDeletedDescription,
+							});
+						}
+					} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
+						// AUTHENTICATION_FAILED
+						// トークンが無効化されていたりアカウントが削除されたりしている
+						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
+							await alert({
+								type: 'error',
+								title: i18n.ts.tokenRevoked,
+								text: i18n.ts.tokenRevokedDescription,
+							});
+						}
+					} else {
+						await alert({
+							type: 'error',
+							title: i18n.ts.failedToFetchAccountInformation,
+							text: JSON.stringify(res.error),
+						});
+					}
+
+					fail(isAccountDeleted);
+				} else {
+					done(res);
+				}
+			})
+			.catch(fail);
+	});
+}
+
+export function updateCurrentAccount(accountData: Misskey.entities.MeDetailed) {
+	if (!$i) return;
+	const token = $i.token;
+	for (const key of Object.keys($i)) {
+		delete $i[key];
+	}
+	for (const [key, value] of Object.entries(accountData)) {
+		$i[key] = value;
+	}
+	prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
+		// TODO: $iのホストも比較したいけど通常null
+		if (user.id === $i.id) {
+			return [host, $i];
+		} else {
+			return [host, user];
+		}
+	}));
+	$i.token = token;
+	miLocalStorage.setItem('account', JSON.stringify($i));
+}
+
+export function updateCurrentAccountPartial(accountData: Partial<Misskey.entities.MeDetailed>) {
+	if (!$i) return;
+	for (const [key, value] of Object.entries(accountData)) {
+		$i[key] = value;
+	}
+	prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
+		// TODO: $iのホストも比較したいけど通常null
+		if (user.id === $i.id) {
+			const newUser = JSON.parse(JSON.stringify($i));
+			for (const [key, value] of Object.entries(accountData)) {
+				newUser[key] = value;
+			}
+			return [host, newUser];
+		}
+		return [host, user];
+	}));
+	miLocalStorage.setItem('account', JSON.stringify($i));
+}
+
+export async function refreshCurrentAccount() {
+	if (!$i) return;
+	return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
+		if (reason === isAccountDeleted) {
+			removeAccount(host, $i.id);
+			if (Object.keys(store.s.accountTokens).length > 0) {
+				login(Object.values(store.s.accountTokens)[0]);
+			} else {
+				signout();
+			}
+		}
+	});
+}
+
+export async function login(token: AccountWithToken['token'], redirect?: string) {
+	const showing = ref(true);
+	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
+		success: false,
+		showing: showing,
+	}, {
+		closed: () => dispose(),
+	});
+
+	const me = await fetchAccount(token, undefined, true).catch(reason => {
+		showing.value = false;
+		throw reason;
+	});
+
+	miLocalStorage.setItem('account', JSON.stringify({
+		...me,
+		token,
+	}));
+
+	await addAccount(host, me, token);
+
+	if (redirect) {
+		// 他のタブは再読み込みするだけ
+		reloadChannel.postMessage(null);
+		// このページはredirectで指定された先に移動
+		location.href = redirect;
+		return;
+	}
+
+	unisonReload();
+}
+
+export async function switchAccount(host: string, id: string) {
+	const token = store.s.accountTokens[host + '/' + id];
+	if (token) {
+		login(token);
+	} else {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
+				store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
+				login(res.i);
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+	}
+}
+
+export async function openAccountMenu(opts: {
+	includeCurrentAccount?: boolean;
+	withExtraOperation: boolean;
+	active?: Misskey.entities.User['id'];
+	onChoose?: (account: Misskey.entities.User) => void;
+}, ev: MouseEvent) {
+	if (!$i) return;
+
+	function createItem(host: string, account: Misskey.entities.User): MenuItem {
+		return {
+			type: 'user' as const,
+			user: account,
+			active: opts.active != null ? opts.active === account.id : false,
+			action: async () => {
+				if (opts.onChoose) {
+					opts.onChoose(account);
+				} else {
+					switchAccount(host, account.id);
+				}
+			},
+		};
+	}
+
+	const menuItems: MenuItem[] = [];
+
+	// TODO: $iのホストも比較したいけど通常null
+	const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.user.id !== $i.id))).map(a => createItem(a.host, a.user));
+
+	if (opts.withExtraOperation) {
+		menuItems.push({
+			type: 'link',
+			text: i18n.ts.profile,
+			to: `/@${$i.username}`,
+			avatar: $i,
+		}, {
+			type: 'divider',
+		});
+
+		if (opts.includeCurrentAccount) {
+			menuItems.push(createItem(host, $i));
+		}
+
+		menuItems.push(...accountItems);
+
+		menuItems.push({
+			type: 'parent',
+			icon: 'ti ti-plus',
+			text: i18n.ts.addAccount,
+			children: [{
+				text: i18n.ts.existingAccount,
+				action: () => {
+					getAccountWithSigninDialog().then(res => {
+						if (res != null) {
+							success();
+						}
+					});
+				},
+			}, {
+				text: i18n.ts.createAccount,
+				action: () => {
+					getAccountWithSignupDialog().then(res => {
+						if (res != null) {
+							switchAccount(host, res.id);
+						}
+					});
+				},
+			}],
+		}, {
+			type: 'link',
+			icon: 'ti ti-users',
+			text: i18n.ts.manageAccounts,
+			to: '/settings/accounts',
+		});
+	} else {
+		if (opts.includeCurrentAccount) {
+			menuItems.push(createItem(host, $i));
+		}
+
+		menuItems.push(...accountItems);
+	}
+
+	popupMenu(menuItems, ev.currentTarget ?? ev.target, {
+		align: 'left',
+	});
+}
+
+export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
+	return new Promise((resolve) => {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
+				const user = await fetchAccount(res.i, res.id, true);
+				await addAccount(host, user, res.i);
+				resolve({ id: res.id, token: res.i });
+			},
+			cancelled: () => {
+				resolve(null);
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+	});
+}
+
+export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
+	return new Promise((resolve) => {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
+			done: async (res: Misskey.entities.SignupResponse) => {
+				const user = JSON.parse(JSON.stringify(res));
+				delete user.token;
+				await addAccount(host, user, res.token);
+				resolve({ id: res.id, token: res.token });
+			},
+			cancelled: () => {
+				resolve(null);
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+	});
+}
diff --git a/packages/frontend/src/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts
index 3acc1127c9..e7e396023d 100644
--- a/packages/frontend/src/aiscript/api.ts
+++ b/packages/frontend/src/aiscript/api.ts
@@ -9,7 +9,7 @@ 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 { $i } from '@/i.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { customEmojis } from '@/custom-emojis.js';
 
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 122aa50ac0..73c4256c4b 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -15,7 +15,7 @@ import components from '@/components/index.js';
 import { applyTheme } from '@/theme.js';
 import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
 import { updateI18n, i18n } from '@/i18n.js';
-import { $i, refreshAccount, login } from '@/account.js';
+import { refreshCurrentAccount, login } from '@/accounts.js';
 import { store } from '@/store.js';
 import { fetchInstance, instance } from '@/instance.js';
 import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
@@ -29,6 +29,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
 import { setupRouter } from '@/router/main.js';
 import { createMainRouter } from '@/router/definition.js';
 import { prefer } from '@/preferences.js';
+import { $i } from '@/i.js';
 
 export async function common(createVue: () => App<Element>) {
 	console.info(`Misskey v${version}`);
@@ -38,11 +39,6 @@ export async function common(createVue: () => App<Element>) {
 
 		console.info(`vue ${vueVersion}`);
 
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		(window as any).$i = $i;
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		(window as any).$store = store;
-
 		window.addEventListener('error', event => {
 			console.error(event);
 			/*
@@ -244,7 +240,7 @@ export async function common(createVue: () => App<Element>) {
 			console.log('account cache found. refreshing...');
 		}
 
-		refreshAccount();
+		refreshCurrentAccount();
 	}
 	//#endregion
 
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index be72eeb9e1..64e3a236e8 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -16,7 +16,7 @@ import { i18n } from '@/i18n.js';
 import { alert, confirm, popup, post, toast } from '@/os.js';
 import { useStream } from '@/stream.js';
 import * as sound from '@/utility/sound.js';
-import { $i, signout, updateAccountPartial } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 import { ColdDeviceStorage, store } from '@/store.js';
 import { reactionPicker } from '@/utility/reaction-picker.js';
@@ -32,6 +32,8 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { launchPlugins } from '@/plugin.js';
 import { unisonReload } from '@/utility/unison-reload.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
+import { signout } from '@/signout.js';
 
 export async function mainBoot() {
 	const { isClientUpdated, lastVersion } = await common(() => {
@@ -480,11 +482,11 @@ export async function mainBoot() {
 
 		// 自分の情報が更新されたとき
 		main.on('meUpdated', i => {
-			updateAccountPartial(i);
+			updateCurrentAccountPartial(i);
 		});
 
 		main.on('readAllNotifications', () => {
-			updateAccountPartial({
+			updateCurrentAccountPartial({
 				hasUnreadNotification: false,
 				unreadNotificationsCount: 0,
 			});
@@ -492,39 +494,39 @@ export async function mainBoot() {
 
 		main.on('unreadNotification', () => {
 			const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
-			updateAccountPartial({
+			updateCurrentAccountPartial({
 				hasUnreadNotification: true,
 				unreadNotificationsCount,
 			});
 		});
 
 		main.on('unreadMention', () => {
-			updateAccountPartial({ hasUnreadMentions: true });
+			updateCurrentAccountPartial({ hasUnreadMentions: true });
 		});
 
 		main.on('readAllUnreadMentions', () => {
-			updateAccountPartial({ hasUnreadMentions: false });
+			updateCurrentAccountPartial({ hasUnreadMentions: false });
 		});
 
 		main.on('unreadSpecifiedNote', () => {
-			updateAccountPartial({ hasUnreadSpecifiedNotes: true });
+			updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: true });
 		});
 
 		main.on('readAllUnreadSpecifiedNotes', () => {
-			updateAccountPartial({ hasUnreadSpecifiedNotes: false });
+			updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: false });
 		});
 
 		main.on('readAllAntennas', () => {
-			updateAccountPartial({ hasUnreadAntenna: false });
+			updateCurrentAccountPartial({ hasUnreadAntenna: false });
 		});
 
 		main.on('unreadAntenna', () => {
-			updateAccountPartial({ hasUnreadAntenna: true });
+			updateCurrentAccountPartial({ hasUnreadAntenna: true });
 			sound.playMisskeySfx('antenna');
 		});
 
 		main.on('readAllAnnouncements', () => {
-			updateAccountPartial({ hasUnreadAnnouncement: false });
+			updateCurrentAccountPartial({ hasUnreadAnnouncement: false });
 		});
 
 		// 個人宛てお知らせが発行されたとき
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 41fd2564d8..582bb137bc 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -29,7 +29,8 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { $i, updateAccountPartial } from '@/account.js';
+import { $i } from '@/i.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
 
 const props = withDefaults(defineProps<{
 	announcement: Misskey.entities.Announcement;
@@ -51,7 +52,7 @@ async function ok() {
 
 	modal.value?.close();
 	misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
-	updateAccountPartial({
+	updateCurrentAccountPartial({
 		unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
 	});
 }
diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue
index 090c31044e..00bf8e68d9 100644
--- a/packages/frontend/src/components/MkAuthConfirm.vue
+++ b/packages/frontend/src/components/MkAuthConfirm.vue
@@ -117,10 +117,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script setup lang="ts">
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
-
 import MkButton from '@/components/MkButton.vue';
-
-import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
+import { $i } from '@/i.js';
+import { getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { getProxiedImageUrl } from '@/utility/media-proxy.js';
@@ -158,7 +157,7 @@ async function init() {
 
 	const accounts = await getAccounts();
 
-	const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id));
+	const accountIdsToFetch = accounts.map(a => a.user.id).filter(id => !users.value.has(id));
 
 	if (accountIdsToFetch.length > 0) {
 		const usersRes = await misskeyApi('users/show', {
@@ -170,7 +169,7 @@ async function init() {
 
 			users.value.set(user.id, {
 				...user,
-				token: accounts.find(a => a.id === user.id)!.token,
+				token: accounts.find(a => a.user.id === user.id)!.token,
 			});
 		}
 	}
diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue
index 5b09ec90dd..2154c08ab3 100644
--- a/packages/frontend/src/components/MkClipPreview.vue
+++ b/packages/frontend/src/components/MkClipPreview.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { computed } from 'vue';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import number from '@/filters/number.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 3c41d597de..4a89fb30ca 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -38,7 +38,7 @@ import tinycolor from 'tinycolor2';
 import { apiUrl } from '@@/js/config.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index 733d50728e..f02a767186 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -44,7 +44,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import bytes from '@/filters/bytes.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
 import { deviceKind } from '@/utility/device-kind.js';
 import { useRouter } from '@/router/supplier.js';
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 384682277e..c0883b1342 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -136,7 +136,7 @@ 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 { $i } from '@/i.js';
 import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index a063854520..b62494fa20 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -44,7 +44,7 @@ import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import { claimAchievement } from '@/utility/achievements.js';
 import { pleaseLogin } from '@/utility/please-login.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 70777bb89a..3113c6fad6 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -89,7 +89,7 @@ import { Chart } from 'chart.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkChart from '@/components/MkChart.vue';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import * as os from '@/os.js';
 import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 33d4d269e7..096c51bbd6 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -98,7 +98,7 @@ import * as os from '@/os.js';
 import bytes from '@/filters/bytes.js';
 import { hms } from '@/filters/hms.js';
 import MkMediaRange from '@/components/MkMediaRange.vue';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 6029b1e0b6..20ac1a917e 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -60,7 +60,7 @@ import bytes from '@/filters/bytes.js';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 74c1aefc3a..403ec61736 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -121,7 +121,7 @@ import * as os from '@/os.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 { $i, iAmModerator } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index 419407955f..f2cf33eb65 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -18,7 +18,7 @@ import { toUnicode } from 'punycode.js';
 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 { $i } from '@/i.js';
 import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index aa352d5163..a22ad346bf 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -208,7 +208,7 @@ import * as sound from '@/utility/sound.js';
 import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { reactionPicker } from '@/utility/reaction-picker.js';
 import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
 import { useNoteCapture } from '@/use/use-note-capture.js';
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index b3f99b702a..dd8d3567b2 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -238,7 +238,7 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import * as sound from '@/utility/sound.js';
 import { reactionPicker } from '@/utility/reaction-picker.js';
 import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
 import { useNoteCapture } from '@/use/use-note-capture.js';
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index bb7347cd26..4fd1c210cb 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -48,7 +48,7 @@ import MkCwButton from '@/components/MkCwButton.vue';
 import { notePage } from '@/filters/note.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { userPage } from '@/filters/user.js';
 import { checkWordMute } from '@/utility/check-word-mute.js';
 
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 5d096cf92d..b2380a5e0e 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -169,7 +169,7 @@ import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { infoImageUrl } from '@/instance.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index 4d1787d420..24750012d0 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 5e379d08b7..c6958eea77 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -127,7 +127,8 @@ import { store } 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 { signinRequired, notesCount, incNotesCount } from '@/i.js';
+import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
 import { uploadFile } from '@/utility/upload.js';
 import { deepClone } from '@/utility/clone.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
@@ -837,7 +838,7 @@ async function post(ev?: MouseEvent) {
 
 	if (postAccount.value) {
 		const storedAccounts = await getAccounts();
-		token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token;
+		token = storedAccounts.find(x => x.user.id === postAccount.value?.id)?.token;
 	}
 
 	posting.value = true;
diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue
index 6efd99d14b..d8dfbd1655 100644
--- a/packages/frontend/src/components/MkPreview.vue
+++ b/packages/frontend/src/components/MkPreview.vue
@@ -43,7 +43,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkRadio from '@/components/MkRadio.vue';
 import * as os from '@/os.js';
 import * as config from '@@/js/config.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const text = ref('');
 const flag = ref(true);
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index 780f8bc6d0..9c37eb5e72 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -42,12 +42,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script setup lang="ts">
 import { ref } from 'vue';
-import { $i, getAccounts } from '@/account.js';
+import { $i } from '@/i.js';
 import MkButton from '@/components/MkButton.vue';
 import { instance } from '@/instance.js';
 import { apiWithDialog, promiseDialog } from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
+import { getAccounts } from '@/accounts.js';
 
 defineProps<{
 	primary?: boolean;
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index d079e68cde..12a066c710 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -27,7 +27,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import * as os from '@/os.js';
 import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { useTooltip } from '@/use/use-tooltip.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkReactionEffect from '@/components/MkReactionEffect.vue';
 import { claimAchievement } from '@/utility/achievements.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 2dcc87f425..b0fbe3c490 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -67,20 +67,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
 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 '@/utility/please-login.js';
+import type { PwResponse } from '@/components/MkSignin.password.vue';
 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';
 
 import XInput from '@/components/MkSignin.input.vue';
 import XPassword from '@/components/MkSignin.password.vue';
-import type { PwResponse } from '@/components/MkSignin.password.vue';
 import XTotp from '@/components/MkSignin.totp.vue';
 import XPasskey from '@/components/MkSignin.passkey.vue';
+import { login } from '@/accounts.js';
 
 const emit = defineEmits<{
 	(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 151cd60fb2..78b6722c1e 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -85,13 +85,13 @@ import * as Misskey from 'misskey-js';
 import * as config from '@@/js/config.js';
 import MkButton from './MkButton.vue';
 import MkInput from './MkInput.vue';
-import MkCaptcha from '@/components/MkCaptcha.vue';
 import type { Captcha } from '@/components/MkCaptcha.vue';
+import MkCaptcha from '@/components/MkCaptcha.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { login } from '@/account.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
+import { login } from '@/accounts.js';
 
 const props = withDefaults(defineProps<{
 	autoSet?: boolean;
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 62ae9a048a..e8b740eae2 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -25,7 +25,7 @@ import MkNotes from '@/components/MkNotes.vue';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import { useStream } from '@/stream.js';
 import * as sound from '@/utility/sound.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 31ecb15ab8..bed15031cb 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -55,7 +55,7 @@ import MkButton from './MkButton.vue';
 import MkInfo from './MkInfo.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
-import { iAmAdmin } from '@/account.js';
+import { iAmAdmin } from '@/i.js';
 
 const props = withDefaults(defineProps<{
 	title?: string | null;
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index b26a01737e..59e1b096ae 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
 import { ref, reactive } from 'vue';
 import { i18n } from '@/i18n.js';
 import { globalEvents } from '@/events.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkNote from '@/components/MkNote.vue';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index f7b60fbc45..8ae6c1ceaa 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -31,7 +31,7 @@ import MkPostForm from '@/components/MkPostForm.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkNote from '@/components/MkNote.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const emit = defineEmits<{
 	(ev: 'succeeded'): void;
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index eb189b446b..cff531b2ca 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -39,7 +39,7 @@ import MkFollowButton from '@/components/MkFollowButton.vue';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
 import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 8a89b253e3..7e8b1200d5 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -65,7 +65,7 @@ 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 { $i } from '@/i.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
 import { getStaticImageUrl } from '@/utility/media-proxy.js';
 
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index e5c6df267b..6bf3eb44dc 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -70,7 +70,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 
 const emit = defineEmits<{
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 14acfd3f89..7a5e5772a4 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -39,7 +39,7 @@ import FormSlot from '@/components/form/slot.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { chooseFileFromPc } from '@/utility/select-file.js';
 import * as os from '@/os.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index c196519c15..b55069ca25 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -47,7 +47,7 @@ import { instance } from '@/instance.js';
 import MkButton from '@/components/MkButton.vue';
 import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 type Ad = (typeof instance)['ads'][number];
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 9981092dff..20a07e9c28 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -35,7 +35,7 @@ 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';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 728e37cf51..69bbd88cb6 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -50,7 +50,8 @@ import type { PageHeaderItem } from '@/types/page-header.js';
 import type { PageMetadata } from '@/page.js';
 import { globalEvents } from '@/events.js';
 import { injectReactiveMetadata } from '@/page.js';
-import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const props = withDefaults(defineProps<{
 	overridePageMetadata?: PageMetadata;
diff --git a/packages/frontend/src/i.ts b/packages/frontend/src/i.ts
new file mode 100644
index 0000000000..aa84c6aa61
--- /dev/null
+++ b/packages/frontend/src/i.ts
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { reactive } from 'vue';
+import * as Misskey from 'misskey-js';
+import { miLocalStorage } from '@/local-storage.js';
+
+// TODO: 他のタブと永続化されたstateを同期
+
+type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
+
+const accountData = miLocalStorage.getItem('account');
+
+// TODO: 外部からはreadonlyに
+export const $i = accountData ? reactive(JSON.parse(accountData) as AccountWithToken) : null;
+
+export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
+export const iAmAdmin = $i != null && $i.isAdmin;
+
+export function signinRequired() {
+	if ($i == null) throw new Error('signin required');
+	return $i;
+}
+
+export let notesCount = $i == null ? 0 : $i.notesCount;
+export function incNotesCount() {
+	notesCount++;
+}
+
+if (_DEV_) {
+	(window as any).$i = $i;
+}
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 3977edb91b..099339fbee 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -9,7 +9,6 @@ export type Keys = (
 	'instance' |
 	'instanceCachedAt' |
 	'account' |
-	'accounts' |
 	'latestDonationInfoShownAt' |
 	'neverShowDonationInfo' |
 	'neverShowLocalOnlyInfo' |
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 837f333c9a..c0a6a370fc 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -5,7 +5,7 @@
 
 import { computed, reactive } from 'vue';
 import { clearCache } from './utility/clear-cache.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
 import { lookup } from '@/utility/lookup.js';
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 5395429d7f..36dac2954d 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -146,7 +146,7 @@ import { instance } from '@/instance.js';
 import * as os from '@/os.js';
 import { definePage } from '@/page.js';
 import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const patronsWithIcon = [{
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index d7d526f3ba..b166dfd940 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const customEmojiTags = getCustomEmojiTags();
 const q = ref('');
diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue
index 53ce75f9bf..ca2443cc5b 100644
--- a/packages/frontend/src/pages/achievements.vue
+++ b/packages/frontend/src/pages/achievements.vue
@@ -17,7 +17,7 @@ import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 import MkAchievements from '@/components/MkAchievements.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.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 0af28e94fa..9274b90892 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -86,7 +86,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { iAmAdmin, iAmModerator } from '@/account.js';
+import { iAmAdmin, iAmModerator } from '@/i.js';
 
 const tab = ref('overview');
 const file = ref<Misskey.entities.DriveFile | null>(null);
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 2efae999ce..ff1d5e64d3 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -233,7 +233,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { acct } from '@/filters/user.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
-import { iAmAdmin, $i, iAmModerator } from '@/account.js';
+import { iAmAdmin, $i, iAmModerator } from '@/i.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 import MkPagination from '@/components/MkPagination.vue';
 
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index 977bbe0b47..6562610b12 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -55,8 +55,9 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i, updateAccountPartial } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
 
 const props = defineProps<{
 	announcementId: string;
@@ -90,7 +91,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
 	target.isRead = true;
 	await misskeyApi('i/read-announcement', { announcementId: target.id });
 	if ($i) {
-		updateAccountPartial({
+		updateCurrentAccountPartial({
 			unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
 		});
 	}
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 13f28d9b35..2387ac728b 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -56,7 +56,8 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i, updateAccountPartial } from '@/account.js';
+import { $i } from '@/i.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
 
 const paginationCurrent = {
 	endpoint: 'announcements' as const,
@@ -94,7 +95,7 @@ async function read(target) {
 		return a;
 	});
 	misskeyApi('i/read-announcement', { announcementId: target.id });
-	updateAccountPartial({
+	updateCurrentAccountPartial({
 		unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
 	});
 }
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index 8b0fde4a25..e4699379f0 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -47,9 +47,10 @@ import * as Misskey from 'misskey-js';
 import XForm from './auth.form.vue';
 import MkSignin from '@/components/MkSignin.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i, login } from '@/account.js';
+import { $i } from '@/i.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
+import { login } from '@/accounts.js';
 
 const props = defineProps<{
 	token: string;
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
index a3c5a36614..884429dfeb 100644
--- a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -73,7 +73,7 @@ import { i18n } from '@/i18n.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index eb1015b19e..b84b9efc1a 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index a774aa6e44..c5951f0e13 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -82,7 +82,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { deviceKind } from '@/utility/device-kind.js';
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 9765ebf216..590a506a55 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -36,7 +36,7 @@ import * as Misskey from 'misskey-js';
 import { url } from '@@/js/config.js';
 import type { MenuItem } from '@/types/menu.js';
 import MkNotes from '@/components/MkNotes.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index f760aca8ae..364006e9ad 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -208,7 +208,7 @@ import { claimAchievement } from '@/utility/achievements.js';
 import { store } from '@/store.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import * as sound from '@/utility/sound.js';
 import MkRange from '@/components/MkRange.vue';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index 35a240b9ba..bedb0b64f9 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -22,7 +22,7 @@ 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';
+import { $i } from '@/i.js';
 
 const props = defineProps<{
   emoji: Misskey.entities.EmojiSimple;
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 6bce6689d4..08ac913958 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -80,7 +80,7 @@ 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 { $i } from '@/i.js';
 import { isSupportShare } from '@/utility/navigator.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { pleaseLogin } from '@/utility/please-login.js';
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index bd48b882d2..7e496f522d 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -54,7 +54,7 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { infoImageUrl } from '@/instance.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
 const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 56ddb820cf..eb01aadbcc 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -77,7 +77,7 @@ import MkFollowButton from '@/components/MkFollowButton.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { prefer } from '@/preferences.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { isSupportShare } from '@/utility/navigator.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index eddeb4aba9..c4aed8d6df 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -148,7 +148,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import number from '@/filters/number.js';
-import { iAmModerator, iAmAdmin } from '@/account.js';
+import { iAmModerator, iAmAdmin } from '@/i.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 8369927d85..352e1d9386 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -45,7 +45,7 @@ import type { Paging } from '@/components/MkPagination.vue';
 import MkInviteCode from '@/components/MkInviteCode.vue';
 import { definePage } from '@/page.js';
 import { serverErrorImageUrl, instance } from '@/instance.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
 const currentInviteLimit = ref<null | number>(null);
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 0bc9b3f3c2..cc701cb16b 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -37,7 +37,7 @@ import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { userListsCache } from '@/cache.js';
 import { infoImageUrl } from '@/instance.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index fdee890cfd..6481c0da0c 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -66,7 +66,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInput from '@/components/MkInput.vue';
 import { userListsCache } from '@/cache.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import MkPagination from '@/components/MkPagination.vue';
 import { mainRouter } from '@/router/main.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 6f53cba806..fb83993fee 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -65,7 +65,7 @@ import { prefer } from '@/preferences.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';
+import { $i } from '@/i.js';
 
 // contextは非ログイン状態の情報しかないためログイン時は利用できない
 const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index ed701ed3c0..e2f6084252 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -75,7 +75,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.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 1c288442b5..00c664d2a0 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -115,7 +115,7 @@ import MkPagePreview from '@/components/MkPagePreview.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { deepClone } from '@/utility/clone.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { isSupportShare } from '@/utility/navigator.js';
 import { instance } from '@/instance.js';
 import { getStaticImageUrl } from '@/utility/media-proxy.js';
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 71dd220cfe..ef9cc242c6 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -150,7 +150,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import { deepClone } from '@/utility/clone.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 03b75f89ae..2715b70b95 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -114,7 +114,7 @@ import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as Reversi from 'misskey-reversi';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { deepClone } from '@/utility/clone.js';
 import MkButton from '@/components/MkButton.vue';
 import MkRadios from '@/components/MkRadios.vue';
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index 053ec2aa08..662df00d9b 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -17,7 +17,7 @@ import GameBoard from './game.board.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { definePage } from '@/page.js';
 import { useStream } from '@/stream.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
 import { url } from '@@/js/config.js';
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index ff2e7e922f..d66ff8db05 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -113,7 +113,7 @@ import { useStream } from '@/stream.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkPagination from '@/components/MkPagination.vue';
 import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index ed5cd23b23..b0d3b5bbd2 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -66,7 +66,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { registerAsUiLib } from '@/aiscript/ui.js';
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index bfe56e793b..4801e9bc27 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref, shallowRef, toRef } from 'vue';
 import type * as Misskey from 'misskey-js';
 import type { Paging } from '@/components/MkPagination.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { host as localHost } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 9093ffd7a9..41a2535813 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -117,7 +117,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkLink from '@/components/MkLink.vue';
 import { confetti } from '@/utility/confetti.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 806599e801..20d1b0fe0f 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -92,8 +92,9 @@ import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkLink from '@/components/MkLink.vue';
 import * as os from '@/os.js';
-import { signinRequired, updateAccountPartial } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { i18n } from '@/i18n.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
 
 const $i = signinRequired();
 
@@ -131,7 +132,7 @@ async function unregisterTOTP(): Promise<void> {
 		password: auth.result.password,
 		token: auth.result.token,
 	}).then(res => {
-		updateAccountPartial({
+		updateCurrentAccountPartial({
 			twoFactorEnabled: false,
 		});
 	}).catch(error => {
diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue
index ed5fe48821..14bea577a3 100644
--- a/packages/frontend/src/pages/settings/account-data.vue
+++ b/packages/frontend/src/pages/settings/account-data.vue
@@ -167,7 +167,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 2cf65be2d0..749ae5147f 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -4,80 +4,51 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div class="">
-	<FormSuspense :p="init">
-		<div class="_gaps">
-			<div class="_buttons">
-				<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
-				<MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton>
-			</div>
-
-			<template v-for="[id, user] in accounts">
-				<MkUserCardMini v-if="user != null" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/>
-				<button v-else v-panel class="_button" :class="$style.unknownUser" @click="menu(id, $event)">
-					<div :class="$style.unknownUserAvatarMock"><i class="ti ti-user-question"></i></div>
-					<div>
-						<div :class="$style.unknownUserTitle">{{ i18n.ts.unknown }}</div>
-						<div :class="$style.unknownUserSub">ID: <span class="_monospace">{{ id }}</span></div>
-					</div>
-				</button>
-			</template>
+<SearchMarker path="/settings/accounts" :label="i18n.ts.accounts" :keywords="['accounts']" icon="ti ti-users">
+	<div class="_gaps">
+		<div class="_buttons">
+			<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
+			<!--<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i></MkButton>-->
 		</div>
-	</FormSuspense>
-</div>
+
+		<MkUserCardMini v-for="x in accounts" :key="x[0] + x[1].id" :user="x[1]" :class="$style.user" @click.prevent="menu(x[0], x[1], $event)"/>
+	</div>
+</SearchMarker>
 </template>
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
-import FormSuspense from '@/components/form/suspense.vue';
+import type { MenuItem } from '@/types/menu.js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
+import { $i } from '@/i.js';
+import { switchAccount, removeAccount, login, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import type { MenuItem } from '@/types/menu.js';
-
-const storedAccounts = ref<{ id: string, token: string }[] | null>(null);
-const accounts = ref(new Map<string, Misskey.entities.UserDetailed | null>());
+import { prefer } from '@/preferences.js';
 
-const init = async () => {
-	getAccounts().then(accounts => {
-		storedAccounts.value = accounts.filter(x => x.id !== $i!.id);
+const accounts = prefer.r.accounts;
 
-		return misskeyApi('users/show', {
-			userIds: storedAccounts.value.map(x => x.id),
-		});
-	}).then(response => {
-		if (storedAccounts.value == null) return;
-		accounts.value = new Map(storedAccounts.value.map(x => [x.id, response.find((y: Misskey.entities.UserDetailed) => y.id === x.id) ?? null]));
-	});
-};
+function refreshAllAccounts() {
+	// TODO
+}
 
-function menu(account: Misskey.entities.UserDetailed | string, ev: MouseEvent) {
+function menu(host: string, account: Misskey.entities.UserDetailed, ev: MouseEvent) {
 	let menu: MenuItem[];
 
-	if (typeof account === 'string') {
-		menu = [{
-			text: i18n.ts.logout,
-			icon: 'ti ti-trash',
-			danger: true,
-			action: () => removeAccount(account),
-		}];
-	} else {
-		menu = [{
-			text: i18n.ts.switch,
-			icon: 'ti ti-switch-horizontal',
-			action: () => switchAccount(account.id),
-		}, {
-			text: i18n.ts.logout,
-			icon: 'ti ti-trash',
-			danger: true,
-			action: () => removeAccount(account.id),
-		}];
-	}
+	menu = [{
+		text: i18n.ts.switch,
+		icon: 'ti ti-switch-horizontal',
+		action: () => switchAccount(host, account.id),
+	}, {
+		text: i18n.ts.logout,
+		icon: 'ti ti-trash',
+		danger: true,
+		action: () => removeAccount(host, account.id),
+	}];
 
 	os.popupMenu(menu, ev.currentTarget ?? ev.target);
 }
@@ -92,16 +63,10 @@ function addAccount(ev: MouseEvent) {
 	}], ev.currentTarget ?? ev.target);
 }
 
-async function removeAccount(id: string) {
-	await _removeAccount(id);
-	accounts.value.delete(id);
-}
-
 function addExistingAccount() {
 	getAccountWithSigninDialog().then((res) => {
 		if (res != null) {
 			os.success();
-			init();
 		}
 	});
 }
@@ -109,21 +74,11 @@ function addExistingAccount() {
 function createAccount() {
 	getAccountWithSignupDialog().then((res) => {
 		if (res != null) {
-			switchAccountWithToken(res.token);
+			login(res.token);
 		}
 	});
 }
 
-async function switchAccount(id: string) {
-	const fetchedAccounts = await getAccounts();
-	const token = fetchedAccounts.find(x => x.id === id)!.token;
-	switchAccountWithToken(token);
-}
-
-function switchAccountWithToken(token: string) {
-	login(token);
-}
-
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index 3c9914b4e2..6b3bb1b513 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed } from 'vue';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 40542ad5b2..e4803eda2e 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -51,7 +51,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import { i18n } from '@/i18n.js';
 import MkRange from '@/components/MkRange.vue';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index ba25eee175..91549e5240 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -54,7 +54,7 @@ import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import MkInfo from '@/components/MkInfo.vue';
 import { definePage } from '@/page.js';
 
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 34941d5af0..0b25ee5e37 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -104,7 +104,7 @@ import bytes from '@/filters/bytes.js';
 import MkChart from '@/components/MkChart.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { prefer } from '@/preferences.js';
 import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index 0cbda44882..10e2a000d4 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -67,7 +67,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkDisableSection from '@/components/MkDisableSection.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 0579b6d14a..ba5957f06a 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -38,7 +38,7 @@ 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 { $i } from '@/i.js';
 import { clearCache } from '@/utility/clear-cache.js';
 import { instance } from '@/instance.js';
 import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
@@ -47,6 +47,7 @@ import { useRouter } from '@/router/supplier.js';
 import { searchIndexes } from '@/utility/autogen/settings-search-index.js';
 import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
 import { store } from '@/store.js';
+import { signout } from '@/signout.js';
 
 const SETTING_INDEX = searchIndexes; // TODO: lazy load
 
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index 60386da545..260e390b51 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -68,7 +68,7 @@ import MkUserInfo from '@/components/MkUserInfo.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.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 52e1937663..1c942e715a 100644
--- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
@@ -19,7 +19,7 @@ import { ref, watch } from 'vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index a5ab7caf99..ce762af071 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -188,7 +188,7 @@ import { definePage } from '@/page.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import * as os from '@/os.js';
 import { instance, infoImageUrl } from '@/instance.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 49910cdf4a..785fcdfbce 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -75,7 +75,7 @@ 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 { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 62b0f5c941..27fb743cb2 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -127,13 +127,14 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { signout, signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { reloadAsk } from '@/utility/reload-ask.js';
 import FormSection from '@/components/form/section.vue';
 import { prefer } from '@/preferences.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
+import { signout } from '@/signout.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index edc750c295..b73f781b9c 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -185,7 +185,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { definePage } from '@/page.js';
 import FormSlot from '@/components/form/slot.vue';
 import { formatDateTimeString } from '@/utility/format-time-string.js';
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 1d85ba7834..b12ba9fe93 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -164,7 +164,7 @@ import FormSlot from '@/components/form/slot.vue';
 import { selectFile } from '@/utility/select-file.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { langmap } from '@/utility/langmap.js';
 import { definePage } from '@/page.js';
 import { claimAchievement } from '@/utility/achievements.js';
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index c3f1b4b18e..e37f05f1be 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -28,10 +28,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 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 '@/utility/misskey-api.js';
+import { login } from '@/accounts.js';
 
 const submitting = ref(false);
 
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 1af69d82db..868c64d06d 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -25,7 +25,7 @@ import MkNotes from '@/components/MkNotes.vue';
 import MkButton from '@/components/MkButton.vue';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { store } from '@/store.js';
 import * as os from '@/os.js';
 import { genEmbedCode } from '@/utility/get-embed-code.js';
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 5ee42cc1ec..45d8c96ab7 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -86,7 +86,7 @@ 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 { $i } from '@/i.js';
 import { applyTheme } from '@/theme.js';
 import * as os from '@/os.js';
 import { store } from '@/store.js';
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 0ac6979b05..68f4b7a26d 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -48,7 +48,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { definePage } from '@/page.js';
 import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
 import { deviceKind } from '@/utility/device-kind.js';
diff --git a/packages/frontend/src/pages/user/achievements.vue b/packages/frontend/src/pages/user/achievements.vue
index b78ac2dc17..8f13e959e1 100644
--- a/packages/frontend/src/pages/user/achievements.vue
+++ b/packages/frontend/src/pages/user/achievements.vue
@@ -14,7 +14,7 @@ import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkAchievements from '@/components/MkAchievements.vue';
 import { claimAchievement } from '@/utility/achievements.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const props = defineProps<{
 	user: Misskey.entities.User;
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 6450f1e077..149481f99b 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -176,7 +176,7 @@ 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 { $i, iAmModerator } from '@/i.js';
 import { dateString } from '@/filters/date.js';
 import { confetti } from '@/utility/confetti.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index b5127de390..16413a55cc 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -38,7 +38,7 @@ import { acct as getAcct } from '@/filters/user.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { serverContext, assertServerContext } from '@/server-context.js';
 
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 939ca934e8..d9e3ca9966 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -45,9 +45,9 @@ import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import MkAnimBg from '@/components/MkAnimBg.vue';
+import { login } from '@/accounts.js';
 
 const username = ref('');
 const password = ref('');
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index f55b1e93cf..3ebf2ab4e4 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -8,7 +8,7 @@
 import { onUnmounted, ref, watch } from 'vue';
 import { BroadcastChannel } from 'broadcast-channel';
 import type { Ref } from 'vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { get, set } from '@/utility/idb-proxy.js';
 import { store } from '@/store.js';
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index 8785acd7d1..f9e6ab2a75 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -9,7 +9,7 @@ import { cloudBackup } from '@/preferences/utility.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { isSameScope, PreferencesManager } from '@/preferences/manager.js';
 import { store } from '@/store.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 
 const TAB_ID = uuid();
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index eb3d6eeac4..e460359acd 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -32,6 +32,10 @@ export type SoundStore = {
 // NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
 
 export const PREF_DEF = {
+	accounts: {
+		default: [] as [host: string, user: Misskey.entities.User][],
+	},
+
 	pinnedUserLists: {
 		accountDependent: true,
 		default: [] as Misskey.entities.UserList[],
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index b053cadacb..fad0226b6e 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -9,7 +9,7 @@ import { host, version } from '@@/js/config.js';
 import { PREF_DEF } from './def.js';
 import type { Ref, WritableComputedRef } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.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/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
index c37dbcf96b..bf3dfa157f 100644
--- a/packages/frontend/src/preferences/utility.ts
+++ b/packages/frontend/src/preferences/utility.ts
@@ -12,7 +12,7 @@ import { miLocalStorage } from '@/local-storage.js';
 import { prefer } from '@/preferences.js';
 import * as os from '@/os.js';
 import { store } from '@/store.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { unisonReload } from '@/utility/unison-reload.js';
 
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 73920766d7..9a81032cc8 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -7,7 +7,7 @@ import { defineAsyncComponent } from 'vue';
 import type { AsyncComponentLoader } from 'vue';
 import type { IRouter, RouteDef } from '@/nirax.js';
 import { Router } from '@/nirax.js';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import MkLoading from '@/pages/_loading_.vue';
 import MkError from '@/pages/_error_.vue';
 
diff --git a/packages/frontend/src/signout.ts b/packages/frontend/src/signout.ts
new file mode 100644
index 0000000000..8e90552546
--- /dev/null
+++ b/packages/frontend/src/signout.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent, ref } from 'vue';
+import { apiUrl, host } from '@@/js/config.js';
+import { defaultMemoryStorage } from '@/memory-storage';
+import { i18n } from '@/i18n.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { waiting, popup, popupMenu, success, alert } from '@/os.js';
+import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
+import { prefer } from '@/preferences.js';
+import { store } from '@/store.js';
+import { $i } from '@/i.js';
+
+export async function signout() {
+	if (!$i) return;
+
+	defaultMemoryStorage.clear();
+
+	waiting();
+	miLocalStorage.removeItem('account');
+
+	// TODO: preferencesも削除
+
+	//#region Remove service worker registration
+	try {
+		if (navigator.serviceWorker.controller) {
+			const registration = await navigator.serviceWorker.ready;
+			const push = await registration.pushManager.getSubscription();
+			if (push) {
+				await window.fetch(`${apiUrl}/sw/unregister`, {
+					method: 'POST',
+					body: JSON.stringify({
+						i: $i.token,
+						endpoint: push.endpoint,
+					}),
+					headers: {
+						'Content-Type': 'application/json',
+					},
+				});
+			}
+		}
+
+		await navigator.serviceWorker.getRegistrations()
+			.then(registrations => {
+				return Promise.all(registrations.map(registration => registration.unregister()));
+			});
+	} catch (err) {}
+	//#endregion
+
+	unisonReload('/');
+}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 9a61e63d0e..ffbd8d10ef 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -104,6 +104,10 @@ export const store = markRaw(new Storage('base', {
 		where: 'deviceAccount',
 		default: {} as Record<string, string>, // plugin id, token
 	},
+	accountTokens: {
+		where: 'device',
+		default: {} as Record<string, string>, // host/userId, token
+	},
 
 	enablePreferencesAutoCloudBackup: {
 		where: 'device',
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index e194e96a7f..c97d7d4071 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -5,7 +5,7 @@
 
 import * as Misskey from 'misskey-js';
 import { markRaw } from 'vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { wsOrigin } from '@@/js/config.js';
 // TODO: No WebsocketモードでStreamMockが使えそう
 //import { StreamMock } from '@/utility/stream-mock.js';
diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts
index 5d09ec27f9..2ae5d8730e 100644
--- a/packages/frontend/src/theme-store.ts
+++ b/packages/frontend/src/theme-store.ts
@@ -5,7 +5,7 @@
 
 import type { Theme } from '@/theme.js';
 import { getBuiltinThemes } from '@/theme.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 export function getThemes(): Theme[] {
diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts
index 94eda3545e..a39ccd481d 100644
--- a/packages/frontend/src/timelines.ts
+++ b/packages/frontend/src/timelines.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 
 export const basicTimelineTypes = [
diff --git a/packages/frontend/src/ui/_common_/PreferenceRestore.vue b/packages/frontend/src/ui/_common_/PreferenceRestore.vue
index c70b82cd0e..5fd9f5e44b 100644
--- a/packages/frontend/src/ui/_common_/PreferenceRestore.vue
+++ b/packages/frontend/src/ui/_common_/PreferenceRestore.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { hideRestoreBackupSuggestion, restoreFromCloudBackup } from '@/preferences/utility.js';
 
diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue
index d153dc8726..f9af8e1ee7 100644
--- a/packages/frontend/src/ui/_common_/announcements.vue
+++ b/packages/frontend/src/ui/_common_/announcements.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index 8e5ba8927a..819e1fa42f 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -9,7 +9,7 @@ import * as os from '@/os.js';
 import { instance } from '@/instance.js';
 import { host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 function toolsMenuItems(): MenuItem[] {
 	return [{
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index e218cd8c62..a39a4ee86b 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -53,7 +53,7 @@ import { popups } from '@/os.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 { $i } from '@/i.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 698e9d8d47..2fbc9ab4b3 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -53,10 +53,11 @@ import { computed, defineAsyncComponent, toRef } from 'vue';
 import { openInstanceMenu } from './common.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const menu = toRef(prefer.s, 'menu');
 const otherMenuItemIndicated = computed(() => {
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 234972e76d..1810ec1743 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -93,13 +93,14 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue';
 import { openInstanceMenu } from './common.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-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 '@/utility/get-dom-node-or-null.js';
 import { useRouter } from '@/router/supplier.js';
 import { prefer } from '@/preferences.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts
index df392c6532..ae61e497b5 100644
--- a/packages/frontend/src/ui/_common_/sw-inject.ts
+++ b/packages/frontend/src/ui/_common_/sw-inject.ts
@@ -5,10 +5,11 @@
 
 import { post } from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i, login } from '@/account.js';
+import { $i } from '@/i.js';
 import { getAccountFromId } from '@/utility/get-account-from-id.js';
 import { deepClone } from '@/utility/clone.js';
 import { mainRouter } from '@/router/main.js';
+import { login } from '@/accounts.js';
 
 export function swInject() {
 	navigator.serviceWorker.addEventListener('message', async ev => {
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index 39b40754ff..1e8a342977 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -51,12 +51,12 @@ import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
 import { openInstanceMenu } from './_common_/common.js';
 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 { store } from '@/store.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const WINDOW_THRESHOLD = 1400;
 
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 259aad7401..096ea0d4cf 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -54,7 +54,6 @@ import { openInstanceMenu } from './_common_/common.js';
 // import { host } from '@@/js/config.js';
 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 '@/utility/sticky-sidebar.js';
 // import { mainRouter } from '@/router.js';
@@ -63,6 +62,8 @@ import { store } from '@/store.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const WINDOW_THRESHOLD = 1400;
 
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 337b0dac94..a5db4031e2 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -100,7 +100,7 @@ import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { deviceKind } from '@/utility/device-kind.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index cb93b20c24..29248b2720 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -108,7 +108,7 @@ import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 import { deviceKind } from '@/utility/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/use/use-note-capture.ts b/packages/frontend/src/use/use-note-capture.ts
index 0bc10e90e4..0de2dbb3c5 100644
--- a/packages/frontend/src/use/use-note-capture.ts
+++ b/packages/frontend/src/use/use-note-capture.ts
@@ -7,7 +7,7 @@ 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';
+import { $i } from '@/i.js';
 
 export function useNoteCapture(props: {
 	rootEl: ShallowRef<HTMLElement | undefined>;
diff --git a/packages/frontend/src/utility/achievements.ts b/packages/frontend/src/utility/achievements.ts
index 3025a985ba..f6ab587ae1 100644
--- a/packages/frontend/src/utility/achievements.ts
+++ b/packages/frontend/src/utility/achievements.ts
@@ -4,7 +4,7 @@
  */
 
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 export const ACHIEVEMENT_TYPES = [
 	'notes1',
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index ebc67eb58d..e768d8a5ae 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -814,6 +814,13 @@ export const searchIndexes: SearchIndexItem[] = [
 		path: '/settings/avatar-decoration',
 		icon: 'ti ti-sparkles',
 	},
+	{
+		id: 'zK6posor9',
+		label: i18n.ts.accounts,
+		keywords: ['accounts'],
+		path: '/settings/accounts',
+		icon: 'ti ti-users',
+	},
 	{
 		id: '330Q4mf8E',
 		children: [
diff --git a/packages/frontend/src/utility/check-permissions.ts b/packages/frontend/src/utility/check-permissions.ts
index ed86529d5b..2de8fd2cd1 100644
--- a/packages/frontend/src/utility/check-permissions.ts
+++ b/packages/frontend/src/utility/check-permissions.ts
@@ -4,7 +4,7 @@
  */
 
 import { instance } from '@/instance.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 export const notesSearchAvailable = (
 	// FIXME: instance.policies would be null in Vitest
diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts
index 74eb326c4c..d2026de0b6 100644
--- a/packages/frontend/src/utility/get-note-menu.ts
+++ b/packages/frontend/src/utility/get-note-menu.ts
@@ -9,7 +9,7 @@ import { url } from '@@/js/config.js';
 import { claimAchievement } from './achievements.js';
 import type { Ref, ShallowRef } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts
index 1b9b0eac95..b89c7537e2 100644
--- a/packages/frontend/src/utility/get-user-menu.ts
+++ b/packages/frontend/src/utility/get-user-menu.ts
@@ -13,7 +13,7 @@ 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 { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { notesSearchAvailable, canSearchNonLocalNotes } from '@/utility/check-permissions.js';
 import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
 import { mainRouter } from '@/router/main.js';
diff --git a/packages/frontend/src/utility/isFfVisibleForMe.ts b/packages/frontend/src/utility/isFfVisibleForMe.ts
index e28e5725bc..48ef1c4e49 100644
--- a/packages/frontend/src/utility/isFfVisibleForMe.ts
+++ b/packages/frontend/src/utility/isFfVisibleForMe.ts
@@ -4,7 +4,7 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
 	if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
diff --git a/packages/frontend/src/utility/misskey-api.ts b/packages/frontend/src/utility/misskey-api.ts
index dc07ad477b..72ba54ade3 100644
--- a/packages/frontend/src/utility/misskey-api.ts
+++ b/packages/frontend/src/utility/misskey-api.ts
@@ -6,7 +6,7 @@
 import * as Misskey from 'misskey-js';
 import { ref } from 'vue';
 import { apiUrl } from '@@/js/config.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 export const pendingApiRequestsCount = ref(0);
 
 export type Endpoint = keyof Misskey.Endpoints;
diff --git a/packages/frontend/src/utility/please-login.ts b/packages/frontend/src/utility/please-login.ts
index a8a330eb6d..9253105f48 100644
--- a/packages/frontend/src/utility/please-login.ts
+++ b/packages/frontend/src/utility/please-login.ts
@@ -4,7 +4,7 @@
  */
 
 import { defineAsyncComponent } from 'vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { popup } from '@/os.js';
diff --git a/packages/frontend/src/utility/show-moved-dialog.ts b/packages/frontend/src/utility/show-moved-dialog.ts
index 35b3ef79d8..db21b028cd 100644
--- a/packages/frontend/src/utility/show-moved-dialog.ts
+++ b/packages/frontend/src/utility/show-moved-dialog.ts
@@ -4,7 +4,7 @@
  */
 
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 
 export function showMovedDialog() {
diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts
index d105a318a7..eb3cbd3dfa 100644
--- a/packages/frontend/src/utility/upload.ts
+++ b/packages/frontend/src/utility/upload.ts
@@ -9,7 +9,7 @@ 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 { $i } from '@/i.js';
 import { alert } from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue
index d911e71ab2..db03d1406c 100644
--- a/packages/frontend/src/widgets/WidgetActivity.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.vue
@@ -28,7 +28,7 @@ import XChart from './WidgetActivity.chart.vue';
 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 { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'activity';
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index b49041158f..c46fd81466 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -27,7 +27,7 @@ import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'aiscript';
diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
index fb9dea1847..429b0e0ffb 100644
--- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue
+++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
@@ -21,7 +21,7 @@ import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps
 import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkAsUi from '@/components/MkAsUi.vue';
 import MkContainer from '@/components/MkContainer.vue';
 import { registerAsUiLib } from '@/aiscript/ui.js';
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index 8c7507ef44..be11a26917 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -33,7 +33,7 @@ import MkContainer from '@/components/MkContainer.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const name = i18n.ts._widgets.birthdayFollowings;
 
diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue
index 3f0f9eb9fd..4afe735a22 100644
--- a/packages/frontend/src/widgets/WidgetButton.vue
+++ b/packages/frontend/src/widgets/WidgetButton.vue
@@ -18,7 +18,7 @@ import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps
 import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkButton from '@/components/MkButton.vue';
 
 const name = 'button';
diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue
index c86d1c9653..3fe8378a39 100644
--- a/packages/frontend/src/widgets/WidgetProfile.vue
+++ b/packages/frontend/src/widgets/WidgetProfile.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import type { GetFormResultType } from '@/utility/form.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { userPage } from '@/filters/user.js';
 
 const name = 'profile';
diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts
index a569c0fa51..36838af163 100644
--- a/packages/frontend/test/aiscript/api.test.ts
+++ b/packages/frontend/test/aiscript/api.test.ts
@@ -33,7 +33,7 @@ async function exe(script: string): Promise<values.Value[]> {
 	return outputs;
 }
 
-let $iMock = vi.hoisted<Partial<typeof import('@/account.js').$i> | null >(
+let $iMock = vi.hoisted<Partial<typeof import('@/i.js').$i> | null >(
 	() => null
 );
 
-- 
cgit v1.2.3-freya

