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

* wip

* wip

* wip

* wip

* revert unnecessary changes

* wip

* refactor(frontend): enforce verbatimModuleSyntax

* fix

* refactor(frontend-shared): enforce verbatimModuleSyntax

* wip

* refactor(frontend-embed): enforce verbatimModuleSyntax

* enforce consistent-type-imports

* fix lint config

* attemt to fix ci

* fix lint

* fix

* fix

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

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

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 5a6ada474a..470837ace5 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -31,7 +31,7 @@ import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { notificationTypes } from '@@/js/const.js';
+import type { notificationTypes } from '@@/js/const.js';
 import { infoImageUrl } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
-- 
cgit v1.2.3-freya


From d30ddd4c2ebcacc0d0b49c74e8dfe05b5422ba2e Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 9 Mar 2025 12:34:08 +0900
Subject: Refine preferences (#15597)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* wip

* wip

* test

* wip rollup pluginでsearchIndexの情報生成

* wip

* SPDX

* wip: markerIdを自動付与

* rollupでビルド時・devモード時に毎回uuidを生成するように

* 開発サーバーでだけ必要な挙動は開発サーバーのみで

* 条件が逆

* wip: childrenの生成

* update comment

* update comment

* rename auto generated file

* hashをパスと行数から決定

* Update privacy.vue

* Update privacy.vue

* wip

* Update general.vue

* Update general.vue

* wip

* wip

* Update SearchMarker.vue

* wip

* Update profile.vue

* Update mute-block.vue

* Update mute-block.vue

* Update general.vue

* Update general.vue

* childrenがduplicate key errorを吐く問題をいったん解決

* マーカーの形を成形

* loggerを置きかえ

* とりあえず省略記法に対応

* Refactor and Format codes

* wip

* Update settings-search-index.ts

* wip

* wip

* とりあえず不確定要因の仮置きidを削除

* hashの生成を正規化（絶対パスになっていたのを緩和）

* pathの入力を省略可能に

* adminでもパス生成できるように

* Update settings-search-index.ts

* Update privacy.vue

* wip

* build searchIndex

* wip

* build

* Update general.vue

* build

* Update sounds.vue

* build

* build

* Update sounds.vue

* 🎨

* 🎨

* Update privacy.vue

* Update privacy.vue

* Update security.vue

* create-search-indexを多少改善

* build

* Update 2fa.vue

* wip

* 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義

* キャッシュはdevServerでなくても更新

* Revert "wip"

This reverts commit 41bffd3a13f55618bf939dc1c9acb2a77ead4054.

* inlining

* wip

* Update theme.vue

* 🎨

* wip normalize

* Update theme.vue

* キャッシュのパス変換

* build

* wip

* wip

* Update SearchMarker.vue

* i18n.ts['key'] の形式が取り出せない問題のFix

* build

* 仮でpath入れ

* 必ず絶対パスが使われるように

* wip

* 🎨

* storybookビルド時はcreateSearchIndexをしない

* inliningの構造化

* format code

* Update index.vue

* wip

* wip

* 🎨

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* clean up

* wip

* wip

* wip

* Update rollup-plugin-unwind-css-module-class-name.test.ts

* Update navbar.vue

* clean up

* wip

* wip

* wip

* wip

* wip

* Update preferences-backups.vue

* Update common.ts

* Update preferences.ts

* wip

* wip

* wip

* wip

* Update MkPreferenceContainer.vue

* Update MkPreferenceContainer.vue

* Update MkPreferenceContainer.vue

* enhance: 検索で上下矢印を使用することで検索結果を移動できるように

* Update main-boot.ts

* refactor

* wip

* Update sounds.vue

* fix(frontend): PageWindowでSearchMarkerが動作するように

* enhance(frontend): SearchMarkerの点滅を一定時間で止める

* wip

* lint fix

* fix: 子要素監視が抜けていたのを修正

* アニメーションの回数はCSSで制御するように

* refactor

* enhance(frontend): 検索インデックス作成時のログを削減

* revert

* fix

* fix

* Update preferences.ts

* Update preferences.ts

* wip

* Update preferences.ts

* wip

* 🎨

* wip

* Update MkPreferenceContainer.vue

* wip

* Update preferences.ts

* wip

* Update preferences.ts

* Update preferences.ts

* wip

* wip

* Update preferences.ts

* wip

* wip

* Update preferences.ts

* Update CHANGELOG.md

* Update preferences.ts

* Update deck-store.ts

* deckStoreをdefaultStoreに統合

* wip

* defaultStore -> store

* Update profile.ts

* wip

* refactor

* wip: plugin

* plugin

* plugin

* plugin

* Update plugin.ts

* wip

* Update plugin.vue

* Update preferences.ts

* Update main-boot.ts

* wip

* fix test

* Update plugin.vue

* Update plugin.vue

* Update utility.ts

* wip

* wip

* Update utility.ts

* wip

* wip

* clean up

* Update utility.ts

---------

Co-authored-by: tai-cha <dev@taichan.site>
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
---
 CHANGELOG.md                                       |   3 +-
 locales/index.d.ts                                 |  80 ++++
 locales/ja-JP.yml                                  |  23 +
 packages/frontend/.storybook/preview.ts            |  10 +-
 ...lup-plugin-unwind-css-module-class-name.test.ts |  20 +-
 packages/frontend/src/boot/common.ts               |  60 +--
 packages/frontend/src/boot/main-boot.ts            | 128 +++++-
 .../frontend/src/components/MkAutocomplete.vue     |  13 +-
 packages/frontend/src/components/MkCaptcha.vue     |   4 +-
 packages/frontend/src/components/MkChart.vue       |   4 +-
 packages/frontend/src/components/MkCode.core.vue   |   4 +-
 packages/frontend/src/components/MkCode.vue        |   6 +-
 packages/frontend/src/components/MkContainer.vue   |  10 +-
 packages/frontend/src/components/MkContextMenu.vue |  10 +-
 .../frontend/src/components/MkCropperDialog.vue    |   8 +-
 .../src/components/MkDateSeparatedList.vue         |   6 +-
 .../frontend/src/components/MkDrive.folder.vue     |  12 +-
 packages/frontend/src/components/MkDrive.vue       |   8 +-
 packages/frontend/src/components/MkEmojiPicker.vue |  22 +-
 .../src/components/MkEmojiPickerDialog.vue         |   8 +-
 .../frontend/src/components/MkFoldableSection.vue  |  10 +-
 packages/frontend/src/components/MkFolder.vue      |  10 +-
 .../frontend/src/components/MkFollowButton.vue     |   9 +-
 .../src/components/MkGalleryPostPreview.vue        |   4 +-
 packages/frontend/src/components/MkHeatmap.vue     |   4 +-
 .../frontend/src/components/MkHorizontalSwipe.vue  |  11 +-
 .../frontend/src/components/MkImgWithBlurhash.vue  |  16 +-
 packages/frontend/src/components/MkLaunchPad.vue   |   4 +-
 packages/frontend/src/components/MkMediaAudio.vue  |  18 +-
 packages/frontend/src/components/MkMediaBanner.vue |   4 +-
 packages/frontend/src/components/MkMediaImage.vue  |  20 +-
 packages/frontend/src/components/MkMediaList.vue   |  12 +-
 packages/frontend/src/components/MkMediaVideo.vue  |  18 +-
 packages/frontend/src/components/MkMention.vue     |   4 +-
 packages/frontend/src/components/MkModal.vue       |   8 +-
 packages/frontend/src/components/MkNote.vue        |  29 +-
 .../frontend/src/components/MkNoteDetailed.vue     |  23 +-
 packages/frontend/src/components/MkNoteHeader.vue  |   1 -
 .../frontend/src/components/MkNoteMediaGrid.vue    |  18 +-
 .../frontend/src/components/MkNotifications.vue    |   8 +-
 packages/frontend/src/components/MkPagination.vue  |  17 +-
 packages/frontend/src/components/MkPostForm.vue    |  44 +-
 .../frontend/src/components/MkPostFormAttaches.vue |   4 +-
 .../src/components/MkPreferenceContainer.vue       |  94 +++++
 .../src/components/MkReactionsViewer.reaction.vue  |  10 +-
 .../frontend/src/components/MkReactionsViewer.vue  |  12 +-
 .../frontend/src/components/MkRetentionHeatmap.vue |   4 +-
 .../src/components/MkRetentionLineChart.vue        |   4 +-
 packages/frontend/src/components/MkTimeline.vue    |   8 +-
 packages/frontend/src/components/MkToast.vue       |  10 +-
 packages/frontend/src/components/MkTooltip.vue     |  10 +-
 packages/frontend/src/components/MkUrlPreview.vue  |   7 +-
 .../frontend/src/components/MkUrlPreviewPopup.vue  |   4 +-
 packages/frontend/src/components/MkUserInfo.vue    |   4 +-
 packages/frontend/src/components/MkUserPopup.vue   |  12 +-
 .../frontend/src/components/MkUserSelectDialog.vue |   8 +-
 .../frontend/src/components/MkUserSetupDialog.vue  |  12 +-
 .../MkVisitorDashboard.ActiveUsersChart.vue        |   4 +-
 packages/frontend/src/components/MkWindow.vue      |  10 +-
 .../frontend/src/components/MkYouTubePlayer.vue    |   6 +-
 packages/frontend/src/components/global/MkAd.vue   |  11 +-
 .../frontend/src/components/global/MkAvatar.vue    |  13 +-
 .../src/components/global/MkCustomEmoji.vue        |   4 +-
 .../frontend/src/components/global/MkEmoji.vue     |   8 +-
 .../frontend/src/components/global/MkError.vue     |   4 +-
 .../src/components/global/MkFooterSpacer.vue       |   4 +-
 packages/frontend/src/components/global/MkMfm.ts   |  20 +-
 .../src/components/global/MkPageHeader.tabs.vue    |  22 +-
 .../frontend/src/components/global/RouterView.vue  |   4 +-
 .../src/components/global/SearchMarker.vue         |   2 +-
 packages/frontend/src/deck.ts                      | 306 ++++++++++++++
 packages/frontend/src/directives/click-anime.ts    |   4 +-
 packages/frontend/src/directives/ripple.ts         |   4 +-
 packages/frontend/src/local-storage.ts             |   7 +-
 packages/frontend/src/os.ts                        |  10 +-
 packages/frontend/src/pages/_error_.vue            |   6 +-
 packages/frontend/src/pages/about-misskey.vue      |   4 +-
 packages/frontend/src/pages/admin/abuses.vue       |   6 +-
 .../admin/custom-emojis-manager.local.register.vue |  16 +-
 .../src/pages/admin/overview.active-users.vue      |   4 +-
 .../src/pages/admin/overview.ap-requests.vue       |   4 +-
 .../src/pages/admin/overview.instances.vue         |   6 +-
 .../src/pages/admin/overview.moderators.vue        |   6 +-
 .../src/pages/admin/overview.queue.chart.vue       |   4 +-
 .../frontend/src/pages/admin/overview.stats.vue    |   4 +-
 .../frontend/src/pages/admin/overview.users.vue    |   6 +-
 .../frontend/src/pages/admin/queue.chart.chart.vue |   4 +-
 packages/frontend/src/pages/announcement.vue       |  10 +-
 packages/frontend/src/pages/channel.vue            |   8 +-
 .../frontend/src/pages/drop-and-fusion.game.vue    |  28 +-
 packages/frontend/src/pages/flash/flash.vue        |   6 +-
 packages/frontend/src/pages/gallery/post.vue       |   8 +-
 packages/frontend/src/pages/install-extensions.vue |   8 +-
 packages/frontend/src/pages/my-lists/list.vue      |   4 +-
 packages/frontend/src/pages/note.vue               |   4 +-
 packages/frontend/src/pages/page.vue               |  15 +-
 .../frontend/src/pages/settings/accessibility.vue  |  79 ++--
 .../frontend/src/pages/settings/appearance.vue     | 231 +++++-----
 packages/frontend/src/pages/settings/deck.vue      |  10 +-
 packages/frontend/src/pages/settings/drive.vue     |  37 +-
 .../frontend/src/pages/settings/emoji-picker.vue   |  94 +++--
 .../frontend/src/pages/settings/import-export.vue  |   4 +-
 packages/frontend/src/pages/settings/index.vue     |  20 +-
 .../frontend/src/pages/settings/mute-block.vue     |   4 +-
 packages/frontend/src/pages/settings/navbar.vue    |  14 +-
 packages/frontend/src/pages/settings/other.vue     |  13 +-
 .../frontend/src/pages/settings/plugin.install.vue |   2 +-
 packages/frontend/src/pages/settings/plugin.vue    | 164 ++++----
 .../src/pages/settings/preferences-backups.vue     | 465 ---------------------
 .../frontend/src/pages/settings/preferences.vue    | 275 +++++++-----
 packages/frontend/src/pages/settings/privacy.vue   |  65 +--
 packages/frontend/src/pages/settings/profile.vue   |   4 +-
 packages/frontend/src/pages/settings/sounds.vue    |  50 ++-
 .../src/pages/settings/statusbar.statusbar.vue     |  12 +-
 packages/frontend/src/pages/settings/statusbar.vue |   8 +-
 .../frontend/src/pages/settings/theme.install.vue  |   2 +-
 packages/frontend/src/pages/settings/theme.vue     |  32 +-
 packages/frontend/src/pages/tag.vue                |  10 +-
 packages/frontend/src/pages/theme-editor.vue       |  15 +-
 packages/frontend/src/pages/timeline.vue           |  45 +-
 .../frontend/src/pages/user/activity.following.vue |   4 +-
 .../frontend/src/pages/user/activity.notes.vue     |   4 +-
 packages/frontend/src/pages/user/activity.pv.vue   |   4 +-
 packages/frontend/src/pages/user/home.vue          |   4 +-
 packages/frontend/src/pizzax.ts                    |   4 +-
 packages/frontend/src/plugin.ts                    | 223 ++++++++--
 packages/frontend/src/preferences.ts               |  81 ++++
 packages/frontend/src/preferences/def.ts           | 308 ++++++++++++++
 packages/frontend/src/preferences/profile.ts       | 236 +++++++++++
 packages/frontend/src/preferences/store.ts         |  92 ++++
 packages/frontend/src/preferences/utility.ts       | 222 ++++++++++
 packages/frontend/src/router/definition.ts         |   4 -
 .../src/scripts/autogen/settings-search-index.ts   | 141 ++++---
 packages/frontend/src/scripts/code-highlighter.ts  |  32 +-
 packages/frontend/src/scripts/emoji-picker.ts      |   4 +-
 .../frontend/src/scripts/get-drive-file-menu.ts    |   6 +-
 packages/frontend/src/scripts/get-note-menu.ts     |  15 +-
 packages/frontend/src/scripts/get-user-menu.ts     |  13 +-
 packages/frontend/src/scripts/init-chart.ts        |   4 +-
 packages/frontend/src/scripts/install-plugin.ts    | 131 ------
 packages/frontend/src/scripts/install-theme.ts     |  38 --
 packages/frontend/src/scripts/reaction-picker.ts   |   4 +-
 packages/frontend/src/scripts/select-file.ts       |  10 +-
 packages/frontend/src/scripts/sound.ts             |  23 +-
 packages/frontend/src/scripts/theme.ts             |  32 ++
 packages/frontend/src/scripts/upload.ts            |   8 +-
 packages/frontend/src/store.ts                     | 300 ++++++-------
 packages/frontend/src/theme-store.ts               |  33 +-
 .../frontend/src/ui/_common_/PreferenceRestore.vue |  73 ++++
 packages/frontend/src/ui/_common_/common.vue       |  24 +-
 .../frontend/src/ui/_common_/navbar-for-mobile.vue |   4 +-
 packages/frontend/src/ui/_common_/navbar.vue       |  11 +-
 packages/frontend/src/ui/_common_/statusbars.vue   |   4 +-
 .../frontend/src/ui/_common_/stream-indicator.vue  |   4 +-
 packages/frontend/src/ui/classic.header.vue        |   7 +-
 packages/frontend/src/ui/classic.sidebar.vue       |   9 +-
 packages/frontend/src/ui/classic.vue               |  27 +-
 packages/frontend/src/ui/deck.vue                  |  54 +--
 packages/frontend/src/ui/deck/antenna-column.vue   |  10 +-
 packages/frontend/src/ui/deck/channel-column.vue   |   8 +-
 packages/frontend/src/ui/deck/column.vue           |   6 +-
 packages/frontend/src/ui/deck/deck-store.ts        | 318 +-------------
 packages/frontend/src/ui/deck/direct-column.vue    |   2 +-
 packages/frontend/src/ui/deck/list-column.vue      |  10 +-
 packages/frontend/src/ui/deck/main-column.vue      |  12 +-
 packages/frontend/src/ui/deck/mentions-column.vue  |   2 +-
 .../frontend/src/ui/deck/notifications-column.vue  |   4 +-
 .../frontend/src/ui/deck/role-timeline-column.vue  |   8 +-
 packages/frontend/src/ui/deck/tl-column.vue        |   7 +-
 .../frontend/src/ui/deck/tl-note-notification.ts   |   2 +-
 packages/frontend/src/ui/deck/widgets-column.vue   |   4 +-
 packages/frontend/src/ui/universal.vue             |  44 +-
 packages/frontend/src/ui/universal.widgets.vue     |  26 +-
 packages/frontend/src/ui/visitor.vue               |  11 +-
 packages/frontend/src/widgets/WidgetFederation.vue |   6 +-
 packages/frontend/src/widgets/WidgetJobQueue.vue   |   4 +-
 packages/frontend/src/widgets/WidgetMemo.vue       |   8 +-
 packages/frontend/src/widgets/WidgetPhotos.vue     |   4 +-
 packages/frontend/src/widgets/WidgetTrends.vue     |   6 +-
 packages/frontend/test/emoji.test.ts               |   6 +-
 packages/frontend/test/init.ts                     |  10 +-
 181 files changed, 3441 insertions(+), 2467 deletions(-)
 create mode 100644 packages/frontend/src/components/MkPreferenceContainer.vue
 create mode 100644 packages/frontend/src/deck.ts
 delete mode 100644 packages/frontend/src/pages/settings/preferences-backups.vue
 create mode 100644 packages/frontend/src/preferences.ts
 create mode 100644 packages/frontend/src/preferences/def.ts
 create mode 100644 packages/frontend/src/preferences/profile.ts
 create mode 100644 packages/frontend/src/preferences/store.ts
 create mode 100644 packages/frontend/src/preferences/utility.ts
 delete mode 100644 packages/frontend/src/scripts/install-plugin.ts
 delete mode 100644 packages/frontend/src/scripts/install-theme.ts
 create mode 100644 packages/frontend/src/ui/_common_/PreferenceRestore.vue

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77c3c8b519..b651a03ab0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@
 -
 
 ### Client
--
+- Feat: 設定の管理が強化されました
+  - 自動でバックアップされるように
 
 ### Server
 -
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 6810d204cb..344c3ccf9d 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5278,6 +5278,86 @@ export interface Locale extends ILocale {
      * アクセシビリティ
      */
     "accessibility": string;
+    /**
+     * 設定のプロファイル
+     */
+    "preferencesProfile": string;
+    /**
+     * 設定IDをコピー
+     */
+    "copyPreferenceId": string;
+    /**
+     * 初期値に戻す
+     */
+    "resetToDefaultValue": string;
+    /**
+     * アカウントで上書き
+     */
+    "overrideByAccount": string;
+    /**
+     * 無題
+     */
+    "untitled": string;
+    /**
+     * 名前はありません
+     */
+    "noName": string;
+    /**
+     * スキップ
+     */
+    "skip": string;
+    /**
+     * 復元
+     */
+    "restore": string;
+    "_preferencesProfile": {
+        /**
+         * プロファイル名
+         */
+        "profileName": string;
+        /**
+         * このデバイスを識別する名前を設定してください。
+         */
+        "profileNameDescription": string;
+        /**
+         * 例: 「メインPC」、「スマホ」など
+         */
+        "profileNameDescription2": string;
+    };
+    "_preferencesBackup": {
+        /**
+         * 自動バックアップ
+         */
+        "autoBackup": string;
+        /**
+         * バックアップから復元
+         */
+        "restoreFromBackup": string;
+        /**
+         * バックアップが見つかりませんでした
+         */
+        "noBackupsFoundTitle": string;
+        /**
+         * 自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。
+         */
+        "noBackupsFoundDescription": string;
+        /**
+         * 復元するバックアップを選択してください
+         */
+        "selectBackupToRestore": string;
+        /**
+         * 自動バックアップを有効にするにはプロファイル名の設定が必要です。
+         */
+        "youNeedToNameYourProfileToEnableAutoBackup": string;
+        /**
+         * このデバイスで設定の自動バックアップは有効になっていません。
+         */
+        "autoPreferencesBackupIsNotEnabledForThisDevice": string;
+        /**
+         * 設定のバックアップが見つかりました
+         */
+        "backupFound": string;
+    };
     "_accountSettings": {
         /**
          * コンテンツの表示にログインを必須にする
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 7a5d2f795e..301b68e21d 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1315,6 +1315,29 @@ markAsSensitiveConfirm: "このメディアをセンシティブとして設定
 unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか？"
 preferences: "環境設定"
 accessibility: "アクセシビリティ"
+preferencesProfile: "設定のプロファイル"
+copyPreferenceId: "設定IDをコピー"
+resetToDefaultValue: "初期値に戻す"
+overrideByAccount: "アカウントで上書き"
+untitled: "無題"
+noName: "名前はありません"
+skip: "スキップ"
+restore: "復元"
+
+_preferencesProfile:
+  profileName: "プロファイル名"
+  profileNameDescription: "このデバイスを識別する名前を設定してください。"
+  profileNameDescription2: "例: 「メインPC」、「スマホ」など"
+
+_preferencesBackup:
+  autoBackup: "自動バックアップ"
+  restoreFromBackup: "バックアップから復元"
+  noBackupsFoundTitle: "バックアップが見つかりませんでした"
+  noBackupsFoundDescription: "自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。"
+  selectBackupToRestore: "復元するバックアップを選択してください"
+  youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。"
+  autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。"
+  backupFound: "設定のバックアップが見つかりました"
 
 _accountSettings:
   requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index d000a28232..00639be642 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -68,9 +68,9 @@ queueMicrotask(() => {
 		import('../src/directives'),
 		import('../src/widgets'),
 		import('../src/scripts/theme'),
-		import('../src/store'),
+		import('../src/preferences'),
 		import('../src/os'),
-	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => {
+	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { prefer }, os]) => {
 		setup((app) => {
 			moduleInitialized = true;
 			if (app[appInitialized]) {
@@ -83,7 +83,7 @@ queueMicrotask(() => {
 			widgets(app);
 			misskeyOS = os;
 			if (isChromatic()) {
-				defaultStore.set('animation', false);
+				prefer.set('animation', false);
 			}
 		});
 	});
@@ -104,9 +104,9 @@ const preview = {
 							}
 						}).catch(() => {})
 					: Promise.resolve();
-				const resetDefaultStorePromise = import('../src/store').then(({ defaultStore }) => {
+				const resetDefaultStorePromise = import('../src/store').then(({ store }) => {
 					// @ts-expect-error
-					defaultStore.init();
+					store.init();
 				}).catch(() => {});
 				Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
 					initLocalStorage();
diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
index 5d8cf05fff..884ae3afac 100644
--- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
+++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
@@ -58,7 +58,7 @@ describe(normalizeClass.name, () => {
 
 it('Composition API (standard)', () => {
 	const ast = parse(`
-import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
+import { c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
 import { M as MkContainer } from './MkContainer-!~{03M}~.js';
 import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
 import './photoswipe-!~{003}~.js';
@@ -74,7 +74,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
     let fetching = ref(true);
     let images = ref([]);
     function thumbnail(image) {
-      return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
+      return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
     }
     onMounted(() => {
       const image = [
@@ -173,7 +173,7 @@ export { index_photos as default };
 `.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
 	unwindCssModuleClassName(ast);
 	expect(generate(ast)).toBe(`
-import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
+import {c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
 import {M as MkContainer} from './MkContainer-!~{03M}~.js';
 import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
 import './photoswipe-!~{003}~.js';
@@ -190,7 +190,7 @@ const index_photos = defineComponent({
     let fetching = ref(true);
     let images = ref([]);
     function thumbnail(image) {
-      return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
+      return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
     }
     onMounted(() => {
       const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
@@ -268,7 +268,7 @@ export {index_photos as default};
 it('Composition API (with `useCssModule()`)', () => {
 	const ast = parse(`
 import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
-import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
+import { d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
 
 function isDebuggerEnabled(id) {
   try {
@@ -393,7 +393,7 @@ const _sfc_main = defineComponent({
       el.style.left = "";
     }
     return () => h(
-      defaultStore.state.animation ? TransitionGroup : "div",
+      prefer.s.animation ? TransitionGroup : "div",
       {
         class: {
           [$style["date-separated-list"]]: true,
@@ -402,7 +402,7 @@ const _sfc_main = defineComponent({
           [$style["direction-down"]]: props.direction === "down",
           [$style["direction-up"]]: props.direction === "up"
         },
-        ...defaultStore.state.animation ? {
+        ...prefer.s.animation ? {
           name: "list",
           tag: "div",
           onBeforeLeave,
@@ -441,7 +441,7 @@ export { MkDateSeparatedList as M };
 	unwindCssModuleClassName(ast);
 	expect(generate(ast)).toBe(`
 import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js';
-import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
+import {d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
 function isDebuggerEnabled(id) {
   try {
     return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
@@ -555,7 +555,7 @@ const _sfc_main = defineComponent({
       el.style.top = "";
       el.style.left = "";
     }
-    return () => h(defaultStore.state.animation ? TransitionGroup : "div", {
+    return () => h(prefer.s.animation ? TransitionGroup : "div", {
       class: {
         [$style["date-separated-list"]]: true,
         [$style["date-separated-list-nogap"]]: props.noGap,
@@ -563,7 +563,7 @@ const _sfc_main = defineComponent({
         [$style["direction-down"]]: props.direction === "down",
         [$style["direction-up"]]: props.direction === "up"
       },
-      ...defaultStore.state.animation ? {
+      ...prefer.s.animation ? {
         name: "list",
         tag: "div",
         onBeforeLeave,
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index d09b98efe0..7b35d12a80 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -6,6 +6,8 @@
 import { computed, watch, version as vueVersion } from 'vue';
 import { compareVersions } from 'compare-versions';
 import { version, lang, updateLocale, locale } from '@@/js/config.js';
+import defaultLightTheme from '@@/themes/l-light.json5';
+import defaultDarkTheme from '@@/themes/d-green-lime.json5';
 import type { App } from 'vue';
 import widgets from '@/widgets/index.js';
 import directives from '@/directives/index.js';
@@ -14,7 +16,7 @@ import { applyTheme } from '@/scripts/theme.js';
 import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
 import { updateI18n, i18n } from '@/i18n.js';
 import { $i, refreshAccount, login } from '@/account.js';
-import { defaultStore, ColdDeviceStorage } from '@/store.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';
@@ -26,6 +28,7 @@ import { miLocalStorage } from '@/local-storage.js';
 import { fetchCustomEmojis } from '@/custom-emojis.js';
 import { setupRouter } from '@/router/main.js';
 import { createMainRouter } from '@/router/definition.js';
+import { prefer } from '@/preferences.js';
 
 export async function common(createVue: () => App<Element>) {
 	console.info(`Misskey v${version}`);
@@ -38,7 +41,7 @@ export async function common(createVue: () => App<Element>) {
 		// 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 = defaultStore;
+		(window as any).$store = store;
 
 		window.addEventListener('error', event => {
 			console.error(event);
@@ -123,7 +126,7 @@ export async function common(createVue: () => App<Element>) {
 	html.setAttribute('lang', lang);
 	//#endregion
 
-	await defaultStore.ready;
+	await store.ready;
 	await deckStore.ready;
 
 	const fetchInstanceMetaPromise = fetchInstance();
@@ -151,56 +154,63 @@ export async function common(createVue: () => App<Element>) {
 	//#endregion
 
 	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
-	watch(defaultStore.reactiveState.darkMode, (darkMode) => {
-		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
+	watch(store.reactiveState.darkMode, (darkMode) => {
+		applyTheme(darkMode
+			? (prefer.s.darkTheme ?? defaultDarkTheme)
+			: (prefer.s.lightTheme ?? defaultLightTheme),
+		);
 	}, { immediate: miLocalStorage.getItem('theme') == null });
 
-	document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light';
+	document.documentElement.dataset.colorScheme = store.state.darkMode ? 'dark' : 'light';
 
-	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
-	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
+	const darkTheme = prefer.model('darkTheme');
+	const lightTheme = prefer.model('lightTheme');
 
 	watch(darkTheme, (theme) => {
-		if (defaultStore.state.darkMode) {
-			applyTheme(theme);
+		if (store.state.darkMode) {
+			applyTheme(theme ?? defaultDarkTheme);
 		}
 	});
 
 	watch(lightTheme, (theme) => {
-		if (!defaultStore.state.darkMode) {
-			applyTheme(theme);
+		if (!store.state.darkMode) {
+			applyTheme(theme ?? defaultLightTheme);
 		}
 	});
 
 	//#region Sync dark mode
-	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-		defaultStore.set('darkMode', isDeviceDarkmode());
+	if (prefer.s.syncDeviceDarkMode) {
+		store.set('darkMode', isDeviceDarkmode());
 	}
 
 	window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
-		if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-			defaultStore.set('darkMode', mql.matches);
+		if (prefer.s.syncDeviceDarkMode) {
+			store.set('darkMode', mql.matches);
 		}
 	});
 	//#endregion
 
+	if (prefer.s.darkTheme && store.state.darkMode) {
+		if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
+	} else if (prefer.s.lightTheme && !store.state.darkMode) {
+		if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
+	}
+
 	fetchInstanceMetaPromise.then(() => {
-		if (defaultStore.state.themeInitial) {
-			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
-			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
-			defaultStore.set('themeInitial', false);
-		}
+		// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
+		if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.set('lightTheme', JSON.parse(instance.defaultLightTheme));
+		if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
 	});
 
-	watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
+	watch(store.reactiveState.overridedDeviceKind, (kind) => {
 		updateDeviceKind(kind);
 	}, { immediate: true });
 
-	watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
+	watch(prefer.r.useBlurEffectForModal, v => {
 		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
 	}, { immediate: true });
 
-	watch(defaultStore.reactiveState.useBlurEffect, v => {
+	watch(prefer.r.useBlurEffect, v => {
 		if (v) {
 			document.documentElement.style.removeProperty('--MI-blur');
 		} else {
@@ -214,7 +224,7 @@ export async function common(createVue: () => App<Element>) {
 			navigator.wakeLock.request('screen');
 		}
 	});
-	if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) {
+	if (prefer.s.keepScreenOn && 'wakeLock' in navigator) {
 		navigator.wakeLock.request('screen')
 			.then(onVisibilityChange)
 			.catch(() => {
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 3a43c6794b..0ebe55ed1d 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -5,26 +5,29 @@
 
 import { createApp, defineAsyncComponent, markRaw } from 'vue';
 import { ui } from '@@/js/config.js';
-import { common } from './common.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 { 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 { $i, signout, updateAccountPartial } from '@/account.js';
 import { instance } from '@/instance.js';
-import { ColdDeviceStorage, defaultStore } from '@/store.js';
+import { ColdDeviceStorage, store } from '@/store.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
 import { initializeSw } from '@/scripts/initialize-sw.js';
-import { deckStore } from '@/ui/deck/deck-store.js';
 import { emojiPicker } from '@/scripts/emoji-picker.js';
 import { mainRouter } from '@/router/main.js';
 import { makeHotkey } from '@/scripts/hotkey.js';
-import type { Keymap } from '@/scripts/hotkey.js';
 import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
+import { prefer } from '@/preferences.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { deckStore } from '@/ui/deck/deck-store.js';
+import { launchPlugin } from '@/plugin.js';
 
 export async function mainBoot() {
 	const { isClientUpdated } = await common(() => {
@@ -34,7 +37,7 @@ export async function mainBoot() {
 		if (!$i) uiStyle = 'visitor';
 
 		if (searchParams.has('zen')) uiStyle = 'zen';
-		if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen';
+		if (uiStyle === 'deck' && prefer.s['deck.useSimpleUiForNonRootPages'] && location.pathname !== '/') uiStyle = 'zen';
 
 		if (searchParams.has('ui')) uiStyle = searchParams.get('ui');
 
@@ -73,9 +76,9 @@ export async function mainBoot() {
 
 	let reloadDialogShowing = false;
 	stream.on('_disconnected_', async () => {
-		if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
+		if (prefer.s.serverDisconnectedBehavior === 'reload') {
 			location.reload();
-		} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
+		} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
 			if (reloadDialogShowing) return;
 			reloadDialogShowing = true;
 			const { canceled } = await confirm({
@@ -102,18 +105,14 @@ export async function mainBoot() {
 		removeCustomEmojis(emojiData.emojis);
 	});
 
-	for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
-		import('@/plugin.js').then(async ({ install }) => {
-			// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
-			await new Promise(r => setTimeout(r, 0));
-			install(plugin);
-		});
+	for (const plugin of prefer.s.plugins.filter(p => p.active)) {
+		launchPlugin(plugin);
 	}
 
 	try {
-		if (defaultStore.state.enableSeasonalScreenEffect) {
+		if (prefer.s.enableSeasonalScreenEffect) {
 			const month = new Date().getMonth() + 1;
-			if (defaultStore.state.hemisphere === 'S') {
+			if (prefer.s.hemisphere === 'S') {
 				// ▼南半球
 				if (month === 7 || month === 8) {
 					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
@@ -138,8 +137,99 @@ export async function mainBoot() {
 	}
 
 	if ($i) {
-		defaultStore.loaded.then(() => {
-			if (defaultStore.state.accountSetupWizard !== -1) {
+		store.loaded.then(async () => {
+			// prefereces migration
+			// TODO: そのうち消す
+			if (store.state.menu.length > 0) {
+				const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
+				if (themes.length > 0) {
+					prefer.set('themes', themes);
+				}
+				const plugins = ColdDeviceStorage.get('plugins');
+				prefer.set('plugins', plugins.map(p => ({
+					...p,
+					installId: (p as any).id,
+					id: undefined,
+				})));
+				prefer.set('lightTheme', ColdDeviceStorage.get('lightTheme'));
+				prefer.set('darkTheme', ColdDeviceStorage.get('darkTheme'));
+				prefer.set('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
+				prefer.set('keepCw', store.state.keepCw);
+				prefer.set('collapseRenotes', store.state.collapseRenotes);
+				prefer.set('rememberNoteVisibility', store.state.rememberNoteVisibility);
+				prefer.set('uploadFolder', store.state.uploadFolder);
+				prefer.set('keepOriginalUploading', store.state.keepOriginalUploading);
+				prefer.set('menu', store.state.menu);
+				prefer.set('statusbars', store.state.statusbars);
+				prefer.set('pinnedUserLists', store.state.pinnedUserLists);
+				prefer.set('serverDisconnectedBehavior', store.state.serverDisconnectedBehavior);
+				prefer.set('nsfw', store.state.nsfw);
+				prefer.set('highlightSensitiveMedia', store.state.highlightSensitiveMedia);
+				prefer.set('animation', store.state.animation);
+				prefer.set('animatedMfm', store.state.animatedMfm);
+				prefer.set('advancedMfm', store.state.advancedMfm);
+				prefer.set('showReactionsCount', store.state.showReactionsCount);
+				prefer.set('enableQuickAddMfmFunction', store.state.enableQuickAddMfmFunction);
+				prefer.set('loadRawImages', store.state.loadRawImages);
+				prefer.set('imageNewTab', store.state.imageNewTab);
+				prefer.set('disableShowingAnimatedImages', store.state.disableShowingAnimatedImages);
+				prefer.set('emojiStyle', store.state.emojiStyle);
+				prefer.set('menuStyle', store.state.menuStyle);
+				prefer.set('useBlurEffectForModal', store.state.useBlurEffectForModal);
+				prefer.set('useBlurEffect', store.state.useBlurEffect);
+				prefer.set('showFixedPostForm', store.state.showFixedPostForm);
+				prefer.set('showFixedPostFormInChannel', store.state.showFixedPostFormInChannel);
+				prefer.set('enableInfiniteScroll', store.state.enableInfiniteScroll);
+				prefer.set('useReactionPickerForContextMenu', store.state.useReactionPickerForContextMenu);
+				prefer.set('showGapBetweenNotesInTimeline', store.state.showGapBetweenNotesInTimeline);
+				prefer.set('instanceTicker', store.state.instanceTicker);
+				prefer.set('emojiPickerScale', store.state.emojiPickerScale);
+				prefer.set('emojiPickerWidth', store.state.emojiPickerWidth);
+				prefer.set('emojiPickerHeight', store.state.emojiPickerHeight);
+				prefer.set('emojiPickerStyle', store.state.emojiPickerStyle);
+				prefer.set('reportError', store.state.reportError);
+				prefer.set('squareAvatars', store.state.squareAvatars);
+				prefer.set('showAvatarDecorations', store.state.showAvatarDecorations);
+				prefer.set('numberOfPageCache', store.state.numberOfPageCache);
+				prefer.set('showNoteActionsOnlyHover', store.state.showNoteActionsOnlyHover);
+				prefer.set('showClipButtonInNoteFooter', store.state.showClipButtonInNoteFooter);
+				prefer.set('reactionsDisplaySize', store.state.reactionsDisplaySize);
+				prefer.set('limitWidthOfReaction', store.state.limitWidthOfReaction);
+				prefer.set('forceShowAds', store.state.forceShowAds);
+				prefer.set('aiChanMode', store.state.aiChanMode);
+				prefer.set('devMode', store.state.devMode);
+				prefer.set('mediaListWithOneImageAppearance', store.state.mediaListWithOneImageAppearance);
+				prefer.set('notificationPosition', store.state.notificationPosition);
+				prefer.set('notificationStackAxis', store.state.notificationStackAxis);
+				prefer.set('enableCondensedLine', store.state.enableCondensedLine);
+				prefer.set('keepScreenOn', store.state.keepScreenOn);
+				prefer.set('disableStreamingTimeline', store.state.disableStreamingTimeline);
+				prefer.set('useGroupedNotifications', store.state.useGroupedNotifications);
+				prefer.set('dataSaver', store.state.dataSaver);
+				prefer.set('enableSeasonalScreenEffect', store.state.enableSeasonalScreenEffect);
+				prefer.set('enableHorizontalSwipe', store.state.enableHorizontalSwipe);
+				prefer.set('useNativeUiForVideoAudioPlayer', store.state.useNativeUIForVideoAudioPlayer);
+				prefer.set('keepOriginalFilename', store.state.keepOriginalFilename);
+				prefer.set('alwaysConfirmFollow', store.state.alwaysConfirmFollow);
+				prefer.set('confirmWhenRevealingSensitiveMedia', store.state.confirmWhenRevealingSensitiveMedia);
+				prefer.set('contextMenu', store.state.contextMenu);
+				prefer.set('skipNoteRender', store.state.skipNoteRender);
+				prefer.set('showSoftWordMutedWord', store.state.showSoftWordMutedWord);
+				prefer.set('confirmOnReact', store.state.confirmOnReact);
+				prefer.set('sound.masterVolume', store.state.sound_masterVolume);
+				prefer.set('sound.notUseSound', store.state.sound_notUseSound);
+				prefer.set('sound.useSoundOnlyWhenActive', store.state.sound_useSoundOnlyWhenActive);
+				prefer.set('sound.on.note', store.state.sound_note as any);
+				prefer.set('sound.on.noteMy', store.state.sound_noteMy as any);
+				prefer.set('sound.on.notification', store.state.sound_notification as any);
+				prefer.set('sound.on.reaction', store.state.sound_reaction as any);
+				store.set('deck.profile', deckStore.state.profile);
+				store.set('deck.columns', deckStore.state.columns);
+				store.set('deck.layout', deckStore.state.layout);
+				store.set('menu', []);
+			}
+
+			if (store.state.accountSetupWizard !== -1) {
 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
 					closed: () => dispose(),
 				});
@@ -154,7 +244,7 @@ export async function mainBoot() {
 			});
 		}
 
-		function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) {
+		function onAnnouncementCreated(ev: { announcement: Misskey.entities.Announcement }) {
 			const announcement = ev.announcement;
 			if (announcement.display === 'dialog') {
 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
@@ -412,7 +502,7 @@ export async function mainBoot() {
 			post();
 		},
 		'd': () => {
-			defaultStore.set('darkMode', !defaultStore.state.darkMode);
+			store.set('darkMode', !store.state.darkMode);
 		},
 		's': () => {
 			mainRouter.push('/search');
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 33495c8af6..1a68353331 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -54,17 +54,18 @@ import contains from '@/scripts/contains.js';
 import { acct } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.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 { prefer } from '@/preferences.js';
 
 const lib = emojilist.filter(x => x.category !== 'flags');
 
 const emojiDb = computed(() => {
 	//#region Unicode Emoji
-	const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
+	const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
 	const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
 		emoji: x.char,
@@ -72,7 +73,7 @@ const emojiDb = computed(() => {
 		url: char2path(x.char),
 	}));
 
-	for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+	for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
 		for (const [emoji, keywords] of Object.entries(index)) {
 			for (const k of keywords) {
 				unicodeEmojiDB.push({
@@ -154,10 +155,10 @@ function complete(type: string, value: any) {
 	emit('done', { type, value });
 	emit('closed');
 	if (type === 'emoji') {
-		let recents = defaultStore.state.recentlyUsedEmojis;
+		let recents = store.state.recentlyUsedEmojis;
 		recents = recents.filter((emoji: any) => emoji !== value);
 		recents.unshift(value);
-		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 	}
 }
 
@@ -237,7 +238,7 @@ function exec() {
 	} else if (props.type === 'emoji') {
 		if (!props.q || props.q === '') {
 			// 最近使った絵文字をサジェスト
-			emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
+			emojis.value = store.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
 			return;
 		}
 
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index 134f8226d4..05b8264a83 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 // APIs provided by Captcha services
 // see: https://docs.hcaptcha.com/configuration/#javascript-api
@@ -154,7 +154,7 @@ async function requestRender() {
 
 		captchaWidgetId.value = captcha.value.render(elem, {
 			sitekey: props.sitekey,
-			theme: defaultStore.state.darkMode ? 'dark' : 'light',
+			theme: store.state.darkMode ? 'dark' : 'light',
 			callback: callback,
 			'expired-callback': () => callback(undefined),
 			'error-callback': () => callback(undefined),
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index d05f4921f6..c8685f8618 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -54,7 +54,7 @@ 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 { defaultStore } from '@/store.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';
@@ -161,7 +161,7 @@ const render = () => {
 		chartInstance.destroy();
 	}
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
 
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index 0d7a67eaec..a9001b376a 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -13,7 +13,7 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 const props = defineProps<{
 	code: string;
@@ -22,7 +22,7 @@ const props = defineProps<{
 }>();
 
 const highlighter = await getHighlighter();
-const darkMode = defaultStore.reactiveState.darkMode;
+const darkMode = store.reactiveState.darkMode;
 const codeLang = ref<BundledLanguage | 'aiscript'>('js');
 
 const [lightThemeName, darkThemeName] = await Promise.all([
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index cb82bfd98b..06e4d32878 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</button>
 	<Suspense>
 		<template #fallback>
-			<MkLoading />
+			<MkLoading/>
 		</template>
 		<XCode v-if="show && lang" :code="code" :lang="lang"/>
 		<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
@@ -28,9 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent, ref } from 'vue';
 import * as os from '@/os.js';
 import MkLoading from '@/components/global/MkLoading.vue';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	code: string;
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{
 	forceShow: false,
 });
 
-const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
+const show = ref(props.forceShow === true ? true : !prefer.s.dataSaver.code);
 
 const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
 
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 6a278250fa..169f3df7a4 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</header>
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
 		@enter="enter"
 		@afterEnter="afterEnter"
 		@leave="leave"
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index f51fefa0c0..ed9e5127b0 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <Transition
 	appear
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
 >
 	<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
 		<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
@@ -22,7 +22,7 @@ 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 { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import * as os from '@/os.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 0186cfc2c0..9f9c5b32e2 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -35,13 +35,13 @@ import { onMounted, shallowRef, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import Cropper from 'cropperjs';
 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 { defaultStore } from '@/store.js';
-import { apiUrl } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { prefer } from '@/preferences.js';
 
 const emit = defineEmits<{
 	(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
@@ -81,8 +81,8 @@ const ok = async () => {
 			formData.append('i', $i!.token);
 			if (props.uploadFolder) {
 				formData.append('folderId', props.uploadFolder);
-			} else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
-				formData.append('folderId', defaultStore.state.uploadFolder);
+			} else if (props.uploadFolder !== null && prefer.s.uploadFolder) {
+				formData.append('folderId', prefer.s.uploadFolder);
 			}
 
 			window.fetch(apiUrl + '/drive/files/create', {
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 0d5a16126b..b5842876ac 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts">
 import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
 import type { PropType } from 'vue';
+import type { MisskeyEntity } from '@/types/date-separated-list.js';
 import MkAd from '@/components/global/MkAd.vue';
 import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { instance } from '@/instance.js';
-import { defaultStore } from '@/store.js';
-import type { MisskeyEntity } from '@/types/date-separated-list.js';
+import { prefer } from '@/preferences.js';
 
 export default defineComponent({
 	props: {
@@ -150,7 +150,7 @@ export default defineComponent({
 			[$style['direction-up']]: props.direction === 'up',
 		};
 
-		return () => defaultStore.state.animation ? h(TransitionGroup, {
+		return () => prefer.s.animation ? h(TransitionGroup, {
 			class: classes,
 			name: 'list',
 			tag: 'div',
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 44e3b59ade..f91b248d0c 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
 		{{ folder.name }}
 	</p>
-	<p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload">
+	<p v-if="prefer.s.uploadFolder == folder.id" :class="$style.upload">
 		{{ i18n.ts.uploadFolder }}
 	</p>
 	<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked">
@@ -40,9 +40,9 @@ import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	folder: Misskey.entities.DriveFolder;
@@ -244,8 +244,8 @@ function deleteFolder() {
 	misskeyApi('drive/folders/delete', {
 		folderId: props.folder.id,
 	}).then(() => {
-		if (defaultStore.state.uploadFolder === props.folder.id) {
-			defaultStore.set('uploadFolder', null);
+		if (prefer.s.uploadFolder === props.folder.id) {
+			prefer.set('uploadFolder', null);
 		}
 	}).catch(err => {
 		switch (err.id) {
@@ -266,7 +266,7 @@ function deleteFolder() {
 }
 
 function setAsUploadFolder() {
-	defaultStore.set('uploadFolder', props.folder.id);
+	prefer.set('uploadFolder', props.folder.id);
 }
 
 function onContextmenu(ev: MouseEvent) {
@@ -295,7 +295,7 @@ function onContextmenu(ev: MouseEvent) {
 		danger: true,
 		action: deleteFolder,
 	}];
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menu = menu.concat([{ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFolderId,
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 8be6d6f53d..6863e65b05 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -106,10 +106,10 @@ import XFile from '@/components/MkDrive.file.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { useStream } from '@/stream.js';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { uploadFile, uploads } from '@/scripts/upload.js';
 import { claimAchievement } from '@/scripts/achievements.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	initialFolder?: Misskey.entities.DriveFolder;
@@ -142,7 +142,7 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
 const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
 const uploadings = uploads;
 const connection = useStream().useChannel('drive');
-const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
+const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
 
 // ドロップされようとしているか
 const draghover = ref(false);
@@ -716,7 +716,7 @@ function onContextmenu(ev: MouseEvent) {
 }
 
 onMounted(() => {
-	if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
+	if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) {
 		nextTick(() => {
 			ilFilesObserver.observe(loadMoreFiles.value?.$el);
 		});
@@ -737,7 +737,7 @@ onMounted(() => {
 });
 
 onActivated(() => {
-	if (defaultStore.state.enableInfiniteScroll) {
+	if (prefer.s.enableInfiniteScroll) {
 		nextTick(() => {
 			ilFilesObserver.observe(loadMoreFiles.value?.$el);
 		});
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 62a1000674..3c5b3133da 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -134,14 +134,15 @@ import * as os from '@/os.js';
 import { isTouchUsing } from '@/scripts/touch.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.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 { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	showPinned?: boolean;
-  pinnedEmojis?: string[];
+	pinnedEmojis?: string[];
 	maxHeight?: number;
 	asDrawer?: boolean;
 	asWindow?: boolean;
@@ -163,8 +164,9 @@ const {
 	emojiPickerScale,
 	emojiPickerWidth,
 	emojiPickerHeight,
-	recentlyUsedEmojis,
-} = defaultStore.reactiveState;
+} = prefer.r;
+
+const recentlyUsedEmojis = store.reactiveState.recentlyUsedEmojis;
 
 const recentlyUsedEmojisDef = computed(() => {
 	return recentlyUsedEmojis.value.map(getDef);
@@ -317,7 +319,7 @@ watch(q, () => {
 			}
 			if (matches.size >= max) return matches;
 
-			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+			for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
 				for (const emoji of emojis) {
 					if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
 						matches.add(emoji);
@@ -334,7 +336,7 @@ watch(q, () => {
 			}
 			if (matches.size >= max) return matches;
 
-			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+			for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
 				for (const emoji of emojis) {
 					if (index[emoji.char].some(k => k.startsWith(newQ))) {
 						matches.add(emoji);
@@ -351,7 +353,7 @@ watch(q, () => {
 			}
 			if (matches.size >= max) return matches;
 
-			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+			for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
 				for (const emoji of emojis) {
 					if (index[emoji.char].some(k => k.includes(newQ))) {
 						matches.add(emoji);
@@ -413,7 +415,7 @@ function computeButtonTitle(ev: MouseEvent): void {
 
 function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
 	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
-	if (el && defaultStore.state.animation) {
+	if (el && prefer.s.animation) {
 		const rect = el.getBoundingClientRect();
 		const x = rect.left + (el.offsetWidth / 2);
 		const y = rect.top + (el.offsetHeight / 2);
@@ -427,10 +429,10 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef,
 
 	// 最近使った絵文字更新
 	if (!pinned.value?.includes(key)) {
-		let recents = defaultStore.state.recentlyUsedEmojis;
+		let recents = store.state.recentlyUsedEmojis;
 		recents = recents.filter((emoji) => emoji !== key);
 		recents.unshift(key);
-		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 	}
 }
 
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 21c712b441..6d7062b41c 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="modal"
 	v-slot="{ type, maxHeight }"
 	:zPriority="'middle'"
-	:preferType="defaultStore.state.emojiPickerStyle"
+	:preferType="prefer.s.emojiPickerStyle"
 	:hasInteractionWithOtherFocusTrappedEls="true"
 	:transparentBg="true"
 	:manualShowing="manualShowing"
@@ -40,16 +40,16 @@ import * as Misskey from 'misskey-js';
 import { shallowRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	manualShowing?: boolean | null;
 	src?: HTMLElement;
 	showPinned?: boolean;
-  pinnedEmojis?: string[],
+	pinnedEmojis?: string[],
 	asReactionPicker?: boolean;
 	targetNote?: Misskey.entities.Note;
-  choseAndClose?: boolean;
+	choseAndClose?: boolean;
 }>(), {
 	manualShowing: null,
 	showPinned: true,
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index fb1b5220fb..08b8d60d09 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</button>
 	</header>
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.folderToggleEnterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.folderToggleLeaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.folderToggleEnterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.folderToggleLeaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.folderToggleEnterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.folderToggleLeaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.folderToggleEnterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.folderToggleLeaveTo : ''"
 		@enter="enter"
 		@afterEnter="afterEnter"
 		@leave="leave"
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref, shallowRef, watch } from 'vue';
 import { miLocalStorage } from '@/local-storage.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { getBgColor } from '@/scripts/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 384c0c0b34..1f27e9e221 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -27,10 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 		<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
 			<Transition
-				:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
-				:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
-				:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
-				:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
+				:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''"
+				:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
+				:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
+				:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
 				@enter="enter"
 				@afterEnter="afterEnter"
 				@leave="leave"
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { nextTick, onMounted, ref, shallowRef } from 'vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { getBgColor } from '@/scripts/get-bg-color.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index c1dc67f776..bd45df843a 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -45,7 +45,8 @@ import { i18n } from '@/i18n.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { pleaseLogin } from '@/scripts/please-login.js';
 import { $i } from '@/account.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	user: Misskey.entities.UserDetailed,
@@ -100,7 +101,7 @@ async function onClick() {
 				userId: props.user.id,
 			});
 		} else {
-			if (defaultStore.state.alwaysConfirmFollow) {
+			if (prefer.s.alwaysConfirmFollow) {
 				const { canceled } = await os.confirm({
 					type: 'question',
 					text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }),
@@ -120,11 +121,11 @@ async function onClick() {
 			} else {
 				await misskeyApi('following/create', {
 					userId: props.user.id,
-					withReplies: defaultStore.state.defaultWithReplies,
+					withReplies: store.state.defaultWithReplies,
 				});
 				emit('update:user', {
 					...props.user,
-					withReplies: defaultStore.state.defaultWithReplies,
+					withReplies: store.state.defaultWithReplies,
 				});
 				hasPendingFollowRequestFromYou.value = true;
 
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 22f8355acf..49a6c65170 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -35,14 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { computed, ref } from 'vue';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	post: Misskey.entities.GalleryPost;
 }>();
 
 const hover = ref(false);
-const safe = computed(() => defaultStore.state.nsfw === 'ignore' || defaultStore.state.nsfw === 'respect' && !props.post.isSensitive);
+const safe = computed(() => prefer.s.nsfw === 'ignore' || prefer.s.nsfw === 'respect' && !props.post.isSensitive);
 const show = computed(() => safe.value || hover.value);
 
 function enterHover(): void {
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index 0cc0df9911..058a2d7c78 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -17,7 +17,7 @@ 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 { defaultStore } from '@/store.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';
@@ -106,7 +106,7 @@ async function renderChart() {
 
 	await nextTick();
 
-	const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
+	const color = store.state.darkMode ? '#b4e900' : '#86b300';
 
 	// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
 	const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue
index 196c962a06..ca597b43cb 100644
--- a/packages/frontend/src/components/MkHorizontalSwipe.vue
+++ b/packages/frontend/src/components/MkHorizontalSwipe.vue
@@ -28,12 +28,11 @@ 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 { defaultStore } from '@/store.js';
 import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
+import { prefer } from '@/preferences.js';
 
 const rootEl = shallowRef<HTMLDivElement>();
 
-// eslint-disable-next-line no-undef
 const tabModel = defineModel<string>('tab');
 
 const props = defineProps<{
@@ -44,7 +43,7 @@ const emit = defineEmits<{
 	(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void;
 }>();
 
-const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value);
+const shouldAnimate = computed(() => prefer.r.enableHorizontalSwipe.value || prefer.r.animation.value);
 
 // ▼ しきい値 ▼ //
 
@@ -72,7 +71,7 @@ const isSwipingForClass = ref(false);
 let swipeAborted = false;
 
 function touchStart(event: TouchEvent) {
-	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+	if (!prefer.r.enableHorizontalSwipe.value) return;
 
 	if (event.touches.length !== 1) return;
 
@@ -83,7 +82,7 @@ function touchStart(event: TouchEvent) {
 }
 
 function touchMove(event: TouchEvent) {
-	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+	if (!prefer.r.enableHorizontalSwipe.value) return;
 
 	if (event.touches.length !== 1) return;
 
@@ -134,7 +133,7 @@ function touchEnd(event: TouchEvent) {
 		return;
 	}
 
-	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+	if (!prefer.r.enableHorizontalSwipe.value) return;
 
 	if (event.touches.length !== 0) return;
 
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index c04d0864fb..37cbc5d06b 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
 	<TransitionGroup
-		:duration="defaultStore.state.animation && props.transition?.duration || undefined"
-		:enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
-		:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
-		:enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
-		:leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
-		:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
-		:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
+		:duration="prefer.s.animation && props.transition?.duration || undefined"
+		:enterActiveClass="prefer.s.animation && props.transition?.enterActiveClass || undefined"
+		:leaveActiveClass="prefer.s.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
+		:enterFromClass="prefer.s.animation && props.transition?.enterFromClass || undefined"
+		:leaveToClass="prefer.s.animation && props.transition?.leaveToClass || undefined"
+		:enterToClass="prefer.s.animation && props.transition?.enterToClass || undefined"
+		:leaveFromClass="prefer.s.animation && props.transition?.leaveFromClass || undefined"
 	>
 		<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
 		<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
@@ -60,7 +60,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
 import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue';
 import { v4 as uuid } from 'uuid';
 import { render } from 'buraha';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	transition?: {
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 32c1a2d172..9538979dd1 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { shallowRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import { navbarItemDef } from '@/navbar.js';
-import { defaultStore } from '@/store.js';
 import { deviceKind } from '@/scripts/device-kind.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	src?: HTMLElement;
@@ -50,7 +50,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 
-const menu = defaultStore.state.menu;
+const menu = prefer.s.menu;
 
 const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
 	type: def.to ? 'link' : 'button',
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 3d2795b37a..83ce212efd 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 	tabindex="0"
 	:class="[
 		$style.audioContainer,
-		(audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
+		(audio.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive,
 	]"
 	@contextmenu.stop
 	@keydown.stop
 >
 	<button v-if="hide" :class="$style.hidden" @click="show">
 		<div :class="$style.hiddenTextWrapper">
-			<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
-			<b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
+			<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
+			<b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
 		</div>
 	</button>
 
-	<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.nativeAudioContainer">
+	<div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.nativeAudioContainer">
 		<audio
 			ref="audioEl"
 			preload="metadata"
@@ -93,13 +93,13 @@ 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 { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 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 { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	audio: Misskey.entities.DriveFile;
@@ -155,10 +155,10 @@ const playerEl = shallowRef<HTMLDivElement>();
 const audioEl = shallowRef<HTMLAudioElement>();
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
-const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
+const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'));
 
 async function show() {
-	if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+	if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
 		const { canceled } = await os.confirm({
 			type: 'question',
 			text: i18n.ts.sensitiveMediaRevealConfirm,
@@ -240,7 +240,7 @@ function showMenu(ev: MouseEvent) {
 		menu.push({ type: 'divider' }, ...details);
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menu.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
@@ -407,7 +407,7 @@ onDeactivated(() => {
 	elapsedTimeMs.value = 0;
 	durationMs.value = 0;
 	bufferedEnd.value = 0;
-	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore');
+	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore');
 	stopAudioElWatch();
 	onceInit = false;
 	if (mediaTickFrameId) {
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 3e521e0a03..f23cf507fb 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -27,9 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
 import MkMediaAudio from '@/components/MkMediaAudio.vue';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	media: Misskey.entities.DriveFile;
@@ -38,7 +38,7 @@ const props = defineProps<{
 const hide = ref(true);
 
 async function show() {
-	if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+	if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
 		const { canceled } = await os.confirm({
 			type: 'question',
 			text: i18n.ts.sensitiveMediaRevealConfirm,
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 8ab990b926..8206e3d8f6 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
+<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
 	<component
 		:is="disableImageLink ? 'div' : 'a'"
 		v-bind="disableImageLink ? {
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	>
 		<ImgWithBlurhash
 			:hash="image.blurhash"
-			:src="(defaultStore.state.dataSaver.media && hide) ? null : url"
+			:src="(prefer.s.dataSaver.media && hide) ? null : url"
 			:forceBlurhash="hide"
 			:cover="hide || cover"
 			:alt="image.comment || image.name"
@@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-if="hide">
 		<div :class="$style.hiddenText">
 			<div :class="$style.hiddenTextWrapper">
-				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
-				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b>
+				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
+				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b>
 				<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span>
 			</div>
 		</div>
@@ -58,10 +58,10 @@ import { copyToClipboard } from '@/scripts/copy-to-clipboard';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import bytes from '@/filters/bytes.js';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { $i, iAmModerator } from '@/account.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	image: Misskey.entities.DriveFile;
@@ -77,9 +77,9 @@ const props = withDefaults(defineProps<{
 
 const hide = ref(true);
 
-const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
+const url = computed(() => (props.raw || prefer.s.loadRawImages)
 	? props.image.url
-	: defaultStore.state.disableShowingAnimatedImages
+	: prefer.s.disableShowingAnimatedImages
 		? getStaticImageUrl(props.image.url)
 		: props.image.thumbnailUrl,
 );
@@ -91,7 +91,7 @@ async function onclick(ev: MouseEvent) {
 
 	if (hide.value) {
 		ev.stopPropagation();
-		if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+		if (props.image.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
 			const { canceled } = await os.confirm({
 				type: 'question',
 				text: i18n.ts.sensitiveMediaRevealConfirm,
@@ -105,7 +105,7 @@ async function onclick(ev: MouseEvent) {
 
 // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
 watch(() => props.image, () => {
-	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
+	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.image.isSensitive && prefer.s.nsfw !== 'ignore');
 }, {
 	deep: true,
 	immediate: true,
@@ -166,7 +166,7 @@ function showMenu(ev: MouseEvent) {
 		menuItems.push({ type: 'divider' }, ...details);
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 32766f2029..1c9494f8d6 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -12,9 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:class="[
 				$style.medias,
 				count === 1 ? [$style.n1, {
-					[$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9',
-					[$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1',
-					[$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3',
+					[$style.n116_9]: prefer.s.mediaListWithOneImageAppearance === '16_9',
+					[$style.n11_1]: prefer.s.mediaListWithOneImageAppearance === '1_1',
+					[$style.n12_3]: prefer.s.mediaListWithOneImageAppearance === '2_3',
 				}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
 			]"
 		>
@@ -33,13 +33,13 @@ import * as Misskey from 'misskey-js';
 import PhotoSwipeLightbox from 'photoswipe/lightbox';
 import PhotoSwipe from 'photoswipe';
 import 'photoswipe/style.css';
+import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
 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 { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
-import { defaultStore } from '@/store.js';
 import { focusParent } from '@/scripts/focus.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	mediaList: Misskey.entities.DriveFile[];
@@ -75,7 +75,7 @@ async function calcAspectRatio() {
 		return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
 	};
 
-	switch (defaultStore.state.mediaListWithOneImageAppearance) {
+	switch (prefer.s.mediaListWithOneImageAppearance) {
 		case '16_9':
 			gallery.value.style.aspectRatio = ratioMax(16 / 9);
 			break;
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 682da22711..1433b17dc0 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:class="[
 		$style.videoContainer,
 		controlsShowing && $style.active,
-		(video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
+		(video.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive,
 	]"
 	@mouseover="onMouseOver"
 	@mouseleave="onMouseLeave"
@@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<button v-if="hide" :class="$style.hidden" @click="show">
 		<div :class="$style.hiddenTextWrapper">
-			<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
-			<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
+			<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
+			<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
 		</div>
 	</button>
 
-	<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.videoRoot">
+	<div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.videoRoot">
 		<video
 			ref="videoEl"
 			:class="$style.video"
@@ -116,13 +116,13 @@ import type { Keymap } from '@/scripts/hotkey.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard';
 import bytes from '@/filters/bytes.js';
 import { hms } from '@/filters/hms.js';
-import { defaultStore } from '@/store.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 MkMediaRange from '@/components/MkMediaRange.vue';
 import { $i, iAmModerator } from '@/account.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	video: Misskey.entities.DriveFile;
@@ -175,10 +175,10 @@ function hasFocus() {
 }
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
-const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
+const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'));
 
 async function show() {
-	if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+	if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
 		const { canceled } = await os.confirm({
 			type: 'question',
 			text: i18n.ts.sensitiveMediaRevealConfirm,
@@ -265,7 +265,7 @@ function showMenu(ev: MouseEvent) {
 		menu.push({ type: 'divider' }, ...details);
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menu.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
@@ -502,7 +502,7 @@ onDeactivated(() => {
 	elapsedTimeMs.value = 0;
 	durationMs.value = 0;
 	bufferedEnd.value = 0;
-	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore');
+	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore');
 	stopVideoElWatch();
 	onceInit = false;
 	if (mediaTickFrameId) {
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index 87c82f0a89..ff3442abe0 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -19,8 +19,8 @@ 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 { defaultStore } from '@/store.js';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	username: string;
@@ -36,7 +36,7 @@ const isMe = $i && (
 	`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
 );
 
-const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar
+const avatarUrl = computed(() => prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar
 	? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
 	: `/avatar/@${props.username}@${props.host}`,
 );
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 19588003fa..521c851d8b 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -43,13 +43,13 @@ 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 * as os from '@/os.js';
 import { isTouchUsing } from '@/scripts/touch.js';
-import { defaultStore } from '@/store.js';
 import { deviceKind } from '@/scripts/device-kind.js';
-import type { Keymap } from '@/scripts/hotkey.js';
 import { focusTrap } from '@/scripts/focus-trap.js';
 import { focusParent } from '@/scripts/focus.js';
+import { prefer } from '@/preferences.js';
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === 'BODY') return null;
@@ -106,7 +106,7 @@ const zIndex = os.claimZIndex(props.zPriority);
 const useSendAnime = ref(false);
 const type = computed<ModalTypes>(() => {
 	if (props.preferType === 'auto') {
-		if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
+		if ((prefer.s.menuStyle === 'drawer') || (prefer.s.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
 			return 'drawer';
 		} else {
 			return props.src != null ? 'popup' : 'dialog';
@@ -117,7 +117,7 @@ const type = computed<ModalTypes>(() => {
 });
 const isEnableBgTransparent = computed(() => props.transparentBg && (type.value === 'popup'));
 const transitionName = computed((() =>
-	defaultStore.state.animation
+	prefer.s.animation
 		? useSendAnime.value
 			? 'send'
 			: type.value === 'drawer'
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 8e1d854660..c88b5c16ee 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	v-show="!isDeleted"
 	ref="rootEl"
 	v-hotkey="keymap"
-	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]"
+	:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
 	:tabindex="isDeleted ? '-1' : '0'"
 >
 	<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
@@ -130,9 +130,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
 					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 					<i v-else class="ti ti-plus"></i>
-					<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
+					<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
 				</button>
-				<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
+				<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
 					<i class="ti ti-paperclip"></i>
 				</button>
 				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
@@ -178,13 +178,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, inject, onMounted, ref, shallowRef, watch, provide } from 'vue';
-import type { Ref } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { isLink } from '@@/js/is-link.js';
 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 MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -197,7 +199,6 @@ 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 type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { checkWordMute } from '@/scripts/check-word-mute.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
@@ -205,7 +206,7 @@ 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 { defaultStore, noteViewInterruptors } from '@/store.js';
+import { noteViewInterruptors } from '@/store.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
 import { $i } from '@/account.js';
@@ -219,9 +220,9 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { isEnabledUrlPreview } from '@/instance.js';
-import type { Keymap } from '@/scripts/hotkey.js';
 import { focusPrev, focusNext } from '@/scripts/focus.js';
 import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -284,13 +285,13 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 const isDeleted = ref(false);
 const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
 const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
-const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord);
+const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
 const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
 const translating = ref(false);
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
+const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
 const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
 const renoteCollapsed = ref(
-	defaultStore.state.collapseRenotes && isRenote && (
+	prefer.s.collapseRenotes && isRenote && (
 		($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
 		(appearNote.value.myReaction != null)
 	),
@@ -345,7 +346,7 @@ const keymap = {
 	},
 	'c': () => {
 		if (renoteCollapsed.value) return;
-		if (!defaultStore.state.showClipButtonInNoteFooter) return;
+		if (!prefer.s.showClipButtonInNoteFooter) return;
 		clip();
 	},
 	'o': () => {
@@ -479,7 +480,7 @@ function react(): void {
 			reaction: '❤️',
 		});
 		const el = reactButton.value;
-		if (el && defaultStore.state.animation) {
+		if (el && prefer.s.animation) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
@@ -490,7 +491,7 @@ function react(): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => {
-			if (defaultStore.state.confirmOnReact) {
+			if (prefer.s.confirmOnReact) {
 				const confirm = await os.confirm({
 					type: 'question',
 					text: i18n.tsx.reactAreYouSure({ emoji: reaction.replace('@.', '') }),
@@ -549,7 +550,7 @@ function onContextmenu(ev: MouseEvent): void {
 	if (ev.target && isLink(ev.target as HTMLElement)) return;
 	if (window.getSelection()?.toString() !== '') return;
 
-	if (defaultStore.state.useReactionPickerForContextMenu) {
+	if (prefer.s.useReactionPickerForContextMenu) {
 		ev.preventDefault();
 		react();
 	} else {
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 27d8a399cc..75c08087f3 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -146,9 +146,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
 				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 				<i v-else class="ti ti-plus"></i>
-				<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
+				<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
 			</button>
-			<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
+			<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
 				<i class="ti ti-paperclip"></i>
 			</button>
 			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
@@ -215,6 +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 { Paging } from '@/components/MkPagination.vue';
+import type { Keymap } from '@/scripts/hotkey.js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -226,7 +229,6 @@ 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 type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { checkWordMute } from '@/scripts/check-word-mute.js';
 import { userPage } from '@/filters/user.js';
 import { notePage } from '@/filters/note.js';
@@ -234,7 +236,7 @@ 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 { defaultStore, noteViewInterruptors } from '@/store.js';
+import { noteViewInterruptors } from '@/store.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
 import { $i } from '@/account.js';
@@ -248,12 +250,11 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination from '@/components/MkPagination.vue';
-import type { Paging } 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 type { Keymap } from '@/scripts/hotkey.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -303,7 +304,7 @@ const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
 const translating = ref(false);
 const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
 const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
+const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
 const conversation = ref<Misskey.entities.Note[]>([]);
 const replies = ref<Misskey.entities.Note[]>([]);
 const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
@@ -319,7 +320,7 @@ const keymap = {
 	'q': () => renote(),
 	'm': () => showMenu(),
 	'c': () => {
-		if (!defaultStore.state.showClipButtonInNoteFooter) return;
+		if (!prefer.s.showClipButtonInNoteFooter) return;
 		clip();
 	},
 	'o': () => galleryEl.value?.openGallery(),
@@ -442,7 +443,7 @@ function react(): void {
 			reaction: '❤️',
 		});
 		const el = reactButton.value;
-		if (el && defaultStore.state.animation) {
+		if (el && prefer.s.animation) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
@@ -453,7 +454,7 @@ function react(): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => {
-			if (defaultStore.state.confirmOnReact) {
+			if (prefer.s.confirmOnReact) {
 				const confirm = await os.confirm({
 					type: 'question',
 					text: i18n.tsx.reactAreYouSure({ emoji: reaction.replace('@.', '') }),
@@ -497,7 +498,7 @@ function onContextmenu(ev: MouseEvent): void {
 	if (ev.target && isLink(ev.target as HTMLElement)) return;
 	if (window.getSelection()?.toString() !== '') return;
 
-	if (defaultStore.state.useReactionPickerForContextMenu) {
+	if (prefer.s.useReactionPickerForContextMenu) {
 		ev.preventDefault();
 		react();
 	} else {
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index 750e32a9ff..8e5adec1a3 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -40,7 +40,6 @@ import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
-import { defaultStore } from '@/store.js';
 
 defineProps<{
 	note: Misskey.entities.Note;
diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue
index e51ea5a2de..764d9f6a32 100644
--- a/packages/frontend/src/components/MkNoteMediaGrid.vue
+++ b/packages/frontend/src/components/MkNoteMediaGrid.vue
@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template v-for="file in note.files">
 	<div
 		v-if="(((
-				(defaultStore.state.nsfw === 'force' || file.isSensitive) &&
-				defaultStore.state.nsfw !== 'ignore'
-			) || (defaultStore.state.dataSaver.media && file.type.startsWith('image/'))) &&
+			(prefer.s.nsfw === 'force' || file.isSensitive) &&
+			prefer.s.nsfw !== 'ignore'
+		) || (prefer.s.dataSaver.media && file.type.startsWith('image/'))) &&
 			!showingFiles.has(file.id)
 		)"
 		:class="[$style.filePreview, { [$style.square]: square }]"
@@ -18,15 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkDriveFileThumbnail
 			:file="file"
 			fit="cover"
-			:highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia"
+			:highlightWhenSensitive="prefer.s.highlightSensitiveMedia"
 			:forceBlurhash="true"
 			:large="true"
 			:class="$style.file"
 		/>
 		<div :class="$style.sensitive">
 			<div>
-				<div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div>
-				<div v-else><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div>
+				<div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div>
+				<div v-else><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div>
 				<div>{{ i18n.ts.clickToShow }}</div>
 			</div>
 		</div>
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkDriveFileThumbnail
 			:file="file"
 			fit="cover"
-			:highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia"
+			:highlightWhenSensitive="prefer.s.highlightSensitiveMedia"
 			:large="true"
 			:class="$style.file"
 		/>
@@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import { notePage } from '@/filters/note.js';
 import { i18n } from '@/i18n.js';
-import * as Misskey from 'misskey-js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import bytes from '@/filters/bytes.js';
 
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 470837ace5..4275262429 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -25,17 +25,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
+import * as Misskey from 'misskey-js';
+import type { notificationTypes } from '@@/js/const.js';
 import MkPagination from '@/components/MkPagination.vue';
 import XNotification from '@/components/MkNotification.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import type { notificationTypes } from '@@/js/const.js';
 import { infoImageUrl } from '@/instance.js';
-import { defaultStore } from '@/store.js';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
-import * as Misskey from 'misskey-js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	excludeTypes?: typeof notificationTypes[number][];
@@ -43,7 +43,7 @@ const props = defineProps<{
 
 const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
-const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? {
+const pagination = computed(() => prefer.r.useGroupedNotifications.value ? {
 	endpoint: 'i/notifications-grouped' as const,
 	limit: 20,
 	params: computed(() => ({
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index d9135ab517..d4b0180ef9 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <Transition
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
 	mode="out-in"
 >
 	<MkLoading v-if="fetching"/>
@@ -44,15 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts">
 import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
-import type { ComputedRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
 import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import type { ComputedRef } from 'vue';
 import type { MisskeyEntity } from '@/types/date-separated-list.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const SECOND_FETCH_LIMIT = 30;
 const TOLERANCE = 16;
@@ -140,7 +139,7 @@ const empty = computed(() => items.value.size === 0);
 const error = ref(false);
 const {
 	enableInfiniteScroll,
-} = defaultStore.reactiveState;
+} = prefer.r;
 
 const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value);
 const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body);
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index b39a4ad708..6aa460b506 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
 	<div v-show="useCw" :class="$style.cwOuter">
 		<input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
-		<div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div>		
+		<div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div>
 	</div>
 	<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
 		<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
@@ -123,7 +123,7 @@ import { Autocomplete } from '@/scripts/autocomplete.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { selectFiles } from '@/scripts/select-file.js';
-import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js';
+import { store, notePostInterruptors, postFormActions } from '@/store.js';
 import MkInfo from '@/components/MkInfo.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
@@ -135,6 +135,7 @@ 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 { prefer } from '@/preferences.js';
 
 const $i = signinRequired();
 
@@ -174,19 +175,18 @@ const text = ref(props.initialText ?? '');
 const files = ref(props.initialFiles ?? []);
 const poll = ref<PollEditorModelValue | null>(null);
 const useCw = ref<boolean>(!!props.initialCw);
-const showPreview = ref(defaultStore.state.showPreview);
-watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
-const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction);
-watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
+const showPreview = ref(store.state.showPreview);
+watch(showPreview, () => store.set('showPreview', showPreview.value));
+const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction);
+watch(showAddMfmFunction, () => prefer.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
 const cw = ref<string | null>(props.initialCw ?? null);
-const localOnly = ref(props.initialLocalOnly ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly));
-const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility));
+const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly));
+const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility));
 const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]);
 if (props.initialVisibleUsers) {
 	props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
 }
-const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
-const autocomplete = ref(null);
+const reactionAcceptance = ref(store.state.reactionAcceptance);
 const draghover = ref(false);
 const quoteId = ref<string | null>(null);
 const hasNotSpecifiedMentions = ref(false);
@@ -268,8 +268,8 @@ const canPost = computed((): boolean => {
 		(!poll.value || poll.value.choices.length >= 2);
 });
 
-const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
-const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags'));
+const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags'));
+const hashtags = computed(store.makeGetterSetter('postFormHashtags'));
 
 watch(text, () => {
 	checkMissingMention();
@@ -357,7 +357,7 @@ if (props.specified) {
 }
 
 // keep cw when reply
-if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
+if (prefer.s.keepCw && props.reply && props.reply.cw) {
 	useCw.value = true;
 	cw.value = props.reply.cw;
 }
@@ -456,7 +456,7 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities
 function upload(file: File, name?: string): void {
 	if (props.mock) return;
 
-	uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
+	uploadFile(file, prefer.s.uploadFolder, name).then(res => {
 		files.value.push(res);
 	});
 }
@@ -477,8 +477,8 @@ function setVisibility() {
 	}, {
 		changeVisibility: v => {
 			visibility.value = v;
-			if (defaultStore.state.rememberNoteVisibility) {
-				defaultStore.set('visibility', visibility.value);
+			if (prefer.s.rememberNoteVisibility) {
+				store.set('visibility', visibility.value);
 			}
 		},
 		closed: () => dispose(),
@@ -525,8 +525,8 @@ async function toggleLocalOnly() {
 	}
 
 	localOnly.value = !localOnly.value;
-	if (defaultStore.state.rememberNoteVisibility) {
-		defaultStore.set('localOnly', localOnly.value);
+	if (prefer.s.rememberNoteVisibility) {
+		store.set('localOnly', localOnly.value);
 	}
 }
 
@@ -594,6 +594,8 @@ function onCompositionEnd(ev: CompositionEvent) {
 	justEndedComposition.value = true;
 }
 
+const pastedFileName = 'yyyy-MM-dd HH-mm-ss [{{number}}]';
+
 async function onPaste(ev: ClipboardEvent) {
 	if (props.mock) return;
 	if (!ev.clipboardData) return;
@@ -604,7 +606,7 @@ async function onPaste(ev: ClipboardEvent) {
 			if (!file) continue;
 			const lio = file.name.lastIndexOf('.');
 			const ext = lio >= 0 ? file.name.slice(lio) : '';
-			const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+			const formatted = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
 			upload(file, formatted);
 		}
 	}
@@ -638,7 +640,7 @@ async function onPaste(ev: ClipboardEvent) {
 				return;
 			}
 
-			const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0');
+			const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0');
 			const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' });
 			upload(file, `${fileName}.txt`);
 		});
@@ -751,7 +753,7 @@ async function post(ev?: MouseEvent) {
 	if (ev) {
 		const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
 
-		if (el && defaultStore.state.animation) {
+		if (el && prefer.s.animation) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index c7774d50b2..83d0d66cc3 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -36,12 +36,12 @@ 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 { defaultStore } from '@/store';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
@@ -198,7 +198,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
 		action: () => { detachAndDeleteMedia(file); },
 	});
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
diff --git a/packages/frontend/src/components/MkPreferenceContainer.vue b/packages/frontend/src/components/MkPreferenceContainer.vue
new file mode 100644
index 0000000000..85fab462cd
--- /dev/null
+++ b/packages/frontend/src/components/MkPreferenceContainer.vue
@@ -0,0 +1,94 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+	<div :class="$style.body">
+		<slot></slot>
+	</div>
+	<div :class="$style.menu">
+		<i v-if="isAccountOverrided" class="ti ti-user-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i>
+		<div :class="$style.buttons">
+			<button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu"><i class="ti ti-dots"></i></button>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import type { PREF_DEF } from '@/preferences/def.js';
+import * as os from '@/os.js';
+import { profileManager } from '@/preferences.js';
+
+const props = withDefaults(defineProps<{
+	k: keyof typeof PREF_DEF;
+}>(), {
+});
+
+const isAccountOverrided = ref(profileManager.isAccountOverrided(props.k));
+
+function showMenu(ev: MouseEvent) {
+	const i = window.setInterval(() => {
+		isAccountOverrided.value = profileManager.isAccountOverrided(props.k);
+	}, 100);
+	os.popupMenu(profileManager.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
+		onClosing: () => {
+			window.clearInterval(i);
+		},
+	});
+}
+</script>
+
+<style lang="scss" module>
+.root {
+	position: relative;
+	display: flex;
+
+	&:hover {
+		&::after {
+			content: '';
+			position: absolute;
+			top: -8px;
+			left: -8px;
+			width: calc(100% + 16px);
+			height: calc(100% + 16px);
+			border-radius: 8px;
+			background: light-dark(rgba(0, 0, 0, 0.02), rgba(255, 255, 255, 0.02));
+			pointer-events: none;
+		}
+
+		.menu {
+			.buttons {
+				opacity: 0.7;
+			}
+		}
+	}
+
+	.body {
+		flex: 1;
+	}
+
+	.menu {
+		display: flex;
+		gap: 8px;
+		align-items: center;
+		margin-left: 12px;
+		font-size: 12px;
+		padding-left: 8px;
+		border-left: solid 1px var(--MI_THEME-divider);
+
+		&:hover {
+			.buttons {
+				opacity: 1;
+			}
+		}
+
+		.buttons {
+			opacity: 0.3;
+		}
+	}
+}
+</style>
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 41e475eade..2e453aeb8f 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -8,11 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="buttonEl"
 	v-ripple="canToggle"
 	class="_button"
-	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
+	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
 	@click="toggleReaction()"
 	@contextmenu.prevent.stop="menu"
 >
-	<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
+	<MkReactionIcon :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
 	<span :class="$style.count">{{ count }}</span>
 </button>
 </template>
@@ -30,11 +30,11 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
 import { $i } from '@/account.js';
 import MkReactionEffect from '@/components/MkReactionEffect.vue';
 import { claimAchievement } from '@/scripts/achievements.js';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import * as sound from '@/scripts/sound.js';
 import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
 import { customEmojisMap } from '@/custom-emojis.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	reaction: string;
@@ -90,7 +90,7 @@ async function toggleReaction() {
 			}
 		});
 	} else {
-		if (defaultStore.state.confirmOnReact) {
+		if (prefer.s.confirmOnReact) {
 			const confirm = await os.confirm({
 				type: 'question',
 				text: i18n.tsx.reactAreYouSure({ emoji: props.reaction.replace('@.', '') }),
@@ -135,7 +135,7 @@ async function menu(ev) {
 }
 
 function anime() {
-	if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return;
+	if (document.hidden || !prefer.s.animation || buttonEl.value == null) return;
 
 	const rect = buttonEl.value.getBoundingClientRect();
 	const x = rect.left + 16;
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 63b202f9f3..bb60db8d34 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <TransitionGroup
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''"
-	:moveClass="defaultStore.state.animation ? $style.transition_x_move : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
+	:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
 	tag="div" :class="$style.root"
 >
 	<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { inject, watch, ref } from 'vue';
 import XReaction from '@/components/MkReactionsViewer.reaction.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index 64b573c4d3..3ca680d8f2 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, nextTick, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.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';
@@ -75,7 +75,7 @@ async function renderChart() {
 
 	await nextTick();
 
-	const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
+	const color = store.state.darkMode ? '#b4e900' : '#86b300';
 
 	const getYYYYMMDD = (date: Date) => {
 		const y = date.getFullYear().toString().padStart(2, '0');
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index d41793b0fa..13cee1b1cd 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import tinycolor from 'tinycolor2';
-import { defaultStore } from '@/store.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';
@@ -42,7 +42,7 @@ const getDate = (ymd: string) => {
 onMounted(async () => {
 	let raw = await misskeyApi('retention', { });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 	const color = accent.toHex();
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 7bae240ddd..3cc9b69341 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		v-if="paginationQuery"
 		ref="tlComponent"
 		:pagination="paginationQuery"
-		:noGap="!defaultStore.state.showGapBetweenNotesInTimeline"
+		:noGap="!prefer.s.showGapBetweenNotesInTimeline"
 		@queue="emit('queue', $event)"
 		@status="prComponent?.setDisabled($event)"
 	/>
@@ -20,14 +20,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { BasicTimelineType } from '@/timelines.js';
+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 { $i } from '@/account.js';
 import { instance } from '@/instance.js';
-import { defaultStore } from '@/store.js';
-import type { Paging } from '@/components/MkPagination.vue';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
@@ -239,7 +239,7 @@ function updatePaginationQuery() {
 }
 
 function refreshEndpointAndChannel() {
-	if (!defaultStore.state.disableStreamingTimeline) {
+	if (!prefer.s.disableStreamingTimeline) {
 		disconnectChannel();
 		connectChannel();
 	}
diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue
index e256640649..ac795e312c 100644
--- a/packages/frontend/src/components/MkToast.vue
+++ b/packages/frontend/src/components/MkToast.vue
@@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_toast_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_toast_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_toast_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_toast_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_toast_leaveTo : ''"
 		appear @afterLeave="emit('closed')"
 	>
 		<div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }">
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 defineProps<{
 	message: string;
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index 10365d29b1..c30ca1ffe6 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <Transition
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_tooltip_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_tooltip_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_tooltip_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_tooltip_leaveTo : ''"
 	appear @afterLeave="emit('closed')"
 >
 	<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue';
 import * as os from '@/os.js';
 import { calcPopupPosition } from '@/scripts/popup-position.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	showing: boolean;
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 063e5dcad2..850dd2d257 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
 			scrolling="no"
 			:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
-			:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
+			:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
 		></iframe>
 	</div>
 	<div :class="$style.action">
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 <div v-else>
 	<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
-		<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
+		<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="prefer.s.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
 		</div>
 		<article :class="$style.body">
 			<header :class="$style.header">
@@ -92,7 +92,8 @@ import * as os from '@/os.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import MkButton from '@/components/MkButton.vue';
 import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 type SummalyResult = Awaited<ReturnType<typeof summaly>>;
 
diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue
index e972973dba..fd36d6a82b 100644
--- a/packages/frontend/src/components/MkUrlPreviewPopup.vue
+++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
 		<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false"/>
 	</Transition>
 </div>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, ref } from 'vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	showing: boolean;
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index 0164515a8a..9abf61e893 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="_panel" :class="$style.root">
-	<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
+	<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
 	<MkAvatar :class="$style.avatar" :user="user" indicator/>
 	<div :class="$style.title">
 		<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
@@ -42,7 +42,7 @@ 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 { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 defineProps<{
 	user: Misskey.entities.UserDetailed;
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 740202f28b..4027893a8e 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <Transition
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_popup_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_popup_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_popup_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_popup_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_popup_leaveTo : ''"
 	appear @afterLeave="emit('closed')"
 >
 	<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
 		<div v-if="user != null">
-			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
+			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
 				<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
 			</div>
 			<svg viewBox="0 0 128 128" :class="$style.avatarBack">
@@ -64,7 +64,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { getUserMenu } from '@/scripts/get-user-menu.js';
 import number from '@/filters/number.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.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';
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 1e93d9dbea..2c5757e24e 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -68,7 +68,7 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { instance } from '@/instance.js';
@@ -128,10 +128,10 @@ async function ok() {
 	dialogEl.value?.close();
 
 	// 最近使ったユーザー更新
-	let recents = defaultStore.state.recentlyUsedUsers;
+	let recents = store.state.recentlyUsedUsers;
 	recents = recents.filter(x => x !== selected.value?.id);
 	recents.unshift(selected.value.id);
-	defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
+	store.set('recentlyUsedUsers', recents.splice(0, 16));
 }
 
 function cancel() {
@@ -141,7 +141,7 @@ function cancel() {
 
 onMounted(() => {
 	misskeyApi('users/show', {
-		userIds: defaultStore.state.recentlyUsedUsers,
+		userIds: store.state.recentlyUsedUsers,
 	}).then(foundUsers => {
 		let _users = foundUsers;
 		_users = _users.filter((u) => {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index b7261129ef..cba6f6fd90 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -139,7 +139,7 @@ import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { host } from '@@/js/config.js';
 import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
 
 const emit = defineEmits<{
@@ -149,10 +149,10 @@ const emit = defineEmits<{
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
-const page = ref(defaultStore.state.accountSetupWizard);
+const page = ref(store.state.accountSetupWizard);
 
 watch(page, () => {
-	defaultStore.set('accountSetupWizard', page.value);
+	store.set('accountSetupWizard', page.value);
 });
 
 async function close(skip: boolean) {
@@ -165,11 +165,11 @@ async function close(skip: boolean) {
 	}
 
 	dialog.value?.close();
-	defaultStore.set('accountSetupWizard', -1);
+	store.set('accountSetupWizard', -1);
 }
 
 function setupComplete() {
-	defaultStore.set('accountSetupWizard', -1);
+	store.set('accountSetupWizard', -1);
 	dialog.value?.close();
 }
 
@@ -194,7 +194,7 @@ async function later(later: boolean) {
 	}
 
 	dialog.value?.close();
-	defaultStore.set('accountSetupWizard', 0);
+	store.set('accountSetupWizard', 0);
 }
 </script>
 
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index d098dad9a1..ac48b11c3f 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -18,7 +18,7 @@ import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import tinycolor from 'tinycolor2';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.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';
@@ -59,7 +59,7 @@ async function renderChart() {
 
 	await nextTick();
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const computedStyle = getComputedStyle(document.documentElement);
 	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 2953f656d4..4759d217e8 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <Transition
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_window_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_window_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_window_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_window_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_window_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_window_leaveTo : ''"
 	appear
 	@afterLeave="emit('closed')"
 >
@@ -58,7 +58,7 @@ import type { MenuItem } from '@/types/menu.js';
 import contains from '@/scripts/contains.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 type WindowButton = {
 	title: string;
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 1122976436..1c069d65fc 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 
 	<div class="poamfof">
-		<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
+		<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
 			<div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player">
 				<iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
 			</div>
@@ -25,10 +25,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
-import MkWindow from '@/components/MkWindow.vue';
 import { versatileLang } from '@@/js/intl-const.js';
+import MkWindow from '@/components/MkWindow.vue';
 import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	url: string;
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 08a78c8d81..4f9bfd02bd 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -45,9 +45,10 @@ import { url as local, host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import MkButton from '@/components/MkButton.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
 import { $i } from '@/account.js';
+import { prefer } from '@/preferences.js';
 
 type Ad = (typeof instance)['ads'][number];
 
@@ -66,7 +67,7 @@ const choseAd = (): Ad | null => {
 		return props.specify;
 	}
 
-	const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
+	const allAds = instance.ads.map(ad => store.state.mutedAds.includes(ad.id) ? {
 		...ad,
 		ratio: 0,
 	} : ad);
@@ -107,12 +108,12 @@ const chosen = ref(choseAd());
 
 const self = computed(() => chosen.value?.url.startsWith(local));
 
-const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
+const shouldHide = ref(!prefer.s.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
 
 function reduceFrequency(): void {
 	if (chosen.value == null) return;
-	if (defaultStore.state.mutedAds.includes(chosen.value.id)) return;
-	defaultStore.push('mutedAds', chosen.value.id);
+	if (store.state.mutedAds.includes(chosen.value.id)) return;
+	store.push('mutedAds', chosen.value.id);
 	os.success();
 	chosen.value = choseAd();
 	showMenu.value = false;
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 35c07bc80c..71c309fd89 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -48,11 +48,10 @@ import MkA from './MkA.vue';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { acct, userPage } from '@/filters/user.js';
 import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
-const animation = ref(defaultStore.state.animation);
-const squareAvatars = ref(defaultStore.state.squareAvatars);
-const useBlurEffect = ref(defaultStore.state.useBlurEffect);
+const animation = ref(prefer.s.animation);
+const squareAvatars = ref(prefer.s.squareAvatars);
 
 const props = withDefaults(defineProps<{
 	user: Misskey.entities.User;
@@ -75,7 +74,7 @@ const emit = defineEmits<{
 	(ev: 'click', v: MouseEvent): void;
 }>();
 
-const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations;
+const showDecoration = props.forceShowDecoration || prefer.s.showAvatarDecorations;
 
 const bound = computed(() => props.link
 	? { to: userPage(props.user), target: props.target }
@@ -83,7 +82,7 @@ const bound = computed(() => props.link
 
 const url = computed(() => {
 	if (props.user.avatarUrl == null) return null;
-	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
+	if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
 	return props.user.avatarUrl;
 });
 
@@ -93,7 +92,7 @@ function onClick(ev: MouseEvent): void {
 }
 
 function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
-	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(decoration.url);
+	if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(decoration.url);
 	return decoration.url;
 }
 
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index ec1d859080..82c5e2a1dc 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -28,7 +28,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, defineAsyncComponent, inject, ref } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
 import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { defaultStore } from '@/store.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 import * as os from '@/os.js';
 import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
@@ -37,6 +36,7 @@ import * as sound from '@/scripts/sound.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
 import { $i } from '@/account.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	name: string;
@@ -77,7 +77,7 @@ const url = computed(() => {
 				false,
 				true,
 			);
-	return defaultStore.reactiveState.disableShowingAnimatedImages.value
+	return prefer.s.disableShowingAnimatedImages
 		? getStaticImageUrl(proxied)
 		: proxied;
 });
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index f0acd3bc27..a00004fdb8 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -12,12 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, inject } from 'vue';
 import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js';
 import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js';
-import { defaultStore } from '@/store.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 { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	emoji: string;
@@ -27,9 +27,9 @@ const props = defineProps<{
 
 const react = inject<((name: string) => void) | null>('react', null);
 
-const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
+const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
-const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
+const useOsNativeEmojis = computed(() => prefer.s.emojiStyle === 'native');
 const url = computed(() => char2path(props.emoji));
 const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
 
diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue
index c594cc752b..b07e0775a3 100644
--- a/packages/frontend/src/components/global/MkError.vue
+++ b/packages/frontend/src/components/global/MkError.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
+<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
 	<div :class="$style.root">
 		<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 		<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { serverErrorImageUrl } from '@/instance.js';
 
 const emit = defineEmits<{
diff --git a/packages/frontend/src/components/global/MkFooterSpacer.vue b/packages/frontend/src/components/global/MkFooterSpacer.vue
index 1a75855fa1..d08645aaf3 100644
--- a/packages/frontend/src/components/global/MkFooterSpacer.vue
+++ b/packages/frontend/src/components/global/MkFooterSpacer.vue
@@ -4,11 +4,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div :class="[$style.spacer, defaultStore.reactiveState.darkMode.value ? $style.dark : $style.light]"></div>
+<div :class="[$style.spacer, store.reactiveState.darkMode.value ? $style.dark : $style.light]"></div>
 </template>
 
 <script lang="ts" setup>
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts
index 48d7e34d76..337e326ccd 100644
--- a/packages/frontend/src/components/global/MkMfm.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -3,11 +3,12 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { h, provide } from 'vue';
-import type { VNode, SetupContext } from 'vue';
+import { h } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { host } from '@@/js/config.js';
+import type { VNode, SetupContext } from 'vue';
+import type { MkABehavior } from '@/components/global/MkA.vue';
 import MkUrl from '@/components/global/MkUrl.vue';
 import MkTime from '@/components/global/MkTime.vue';
 import MkLink from '@/components/MkLink.vue';
@@ -19,8 +20,7 @@ import MkCodeInline from '@/components/MkCodeInline.vue';
 import MkGoogle from '@/components/MkGoogle.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
 import MkA from '@/components/global/MkA.vue';
-import type { MkABehavior } from '@/components/global/MkA.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 function safeParseFloat(str: unknown): number | null {
 	if (typeof str !== 'string' || str === '') return null;
@@ -81,7 +81,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 		return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
 	};
 
-	const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
+	const useAnim = prefer.s.advancedMfm && prefer.s.animatedMfm;
 
 	/**
 	 * Gen Vue Elements from MFM AST
@@ -188,17 +188,17 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					}
 					case 'x2': {
 						return h('span', {
-							class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
+							class: prefer.s.advancedMfm ? 'mfm-x2' : '',
 						}, genEl(token.children, scale * 2));
 					}
 					case 'x3': {
 						return h('span', {
-							class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
+							class: prefer.s.advancedMfm ? 'mfm-x3' : '',
 						}, genEl(token.children, scale * 3));
 					}
 					case 'x4': {
 						return h('span', {
-							class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
+							class: prefer.s.advancedMfm ? 'mfm-x4' : '',
 						}, genEl(token.children, scale * 4));
 					}
 					case 'font': {
@@ -241,14 +241,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 						break;
 					}
 					case 'position': {
-						if (!defaultStore.state.advancedMfm) break;
+						if (!prefer.s.advancedMfm) break;
 						const x = safeParseFloat(token.props.args.x) ?? 0;
 						const y = safeParseFloat(token.props.args.y) ?? 0;
 						style = `transform: translateX(${x}em) translateY(${y}em);`;
 						break;
 					}
 					case 'scale': {
-						if (!defaultStore.state.advancedMfm) {
+						if (!prefer.s.advancedMfm) {
 							style = '';
 							break;
 						}
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index aaef8b8fca..8a5e556293 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -8,13 +8,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.tabsInner">
 		<button
 			v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
-			class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]"
+			class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]"
 			@mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)"
 		>
 			<div :class="$style.tabInner">
 				<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
 				<div
-					v-if="!t.iconOnly || (!defaultStore.reactiveState.animation.value && t.key === tab)"
+					v-if="!t.iconOnly || (!prefer.s.animation && t.key === tab)"
 					:class="$style.tabTitle"
 				>
 					{{ t.title }}
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<div
 		ref="tabHighlightEl"
-		:class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]"
+		:class="[$style.tabHighlight, { [$style.animate]: prefer.s.animation }]"
 	></div>
 </div>
 </template>
@@ -41,20 +41,20 @@ export type Tab = {
 	onClick?: (ev: MouseEvent) => void;
 } & (
 	| {
-			iconOnly?: false;
-			title: string;
-			icon?: string;
-		}
+		iconOnly?: false;
+		title: string;
+		icon?: string;
+	}
 	| {
-			iconOnly: true;
-			icon: string;
-		}
+		iconOnly: true;
+		icon: string;
+	}
 );
 </script>
 
 <script lang="ts" setup>
 import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	tabs?: Tab[];
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 3ab3d10a40..2e963daa27 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <KeepAlive
-	:max="defaultStore.state.numberOfPageCache"
+	:max="prefer.s.numberOfPageCache"
 	:exclude="pageCacheController"
 >
 	<Suspense :timeout="0">
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
 import type { IRouter, Resolved, RouteDef } from '@/nirax.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { globalEvents } from '@/events.js';
 import MkLoadingPage from '@/pages/_loading_.vue';
 
diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue
index c5ec626cf4..66a78cb7fd 100644
--- a/packages/frontend/src/components/global/SearchMarker.vue
+++ b/packages/frontend/src/components/global/SearchMarker.vue
@@ -36,7 +36,7 @@ const rootEl = useTemplateRef('root');
 const rootElMutationObserver = new MutationObserver(() => {
 	checkChildren();
 });
-const injectedSearchMarkerId = inject<Ref<string | null>>('inAppSearchMarkerId');
+const injectedSearchMarkerId = inject<Ref<string | null> | null>('inAppSearchMarkerId', null);
 const searchMarkerId = computed(() => injectedSearchMarkerId?.value ?? window.location.hash.slice(1));
 const highlighted = ref(props.markerId === searchMarkerId.value);
 
diff --git a/packages/frontend/src/deck.ts b/packages/frontend/src/deck.ts
new file mode 100644
index 0000000000..dee9a3db78
--- /dev/null
+++ b/packages/frontend/src/deck.ts
@@ -0,0 +1,306 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+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 { store } from '@/store.js';
+
+type ColumnWidget = {
+	name: string;
+	id: string;
+	data: Record<string, any>;
+};
+
+export const columnTypes = [
+	'main',
+	'widgets',
+	'notifications',
+	'tl',
+	'antenna',
+	'list',
+	'channel',
+	'mentions',
+	'direct',
+	'roleTimeline',
+] as const;
+
+export type ColumnType = typeof columnTypes[number];
+
+export type Column = {
+	id: string;
+	type: ColumnType;
+	name: string | null;
+	width: number;
+	widgets?: ColumnWidget[];
+	active?: boolean;
+	flexible?: boolean;
+	antennaId?: string;
+	listId?: string;
+	channelId?: string;
+	roleId?: string;
+	excludeTypes?: typeof notificationTypes[number][];
+	tl?: BasicTimelineType;
+	withRenotes?: boolean;
+	withReplies?: boolean;
+	withSensitive?: boolean;
+	onlyFiles?: boolean;
+	soundSetting?: SoundStore;
+};
+
+export const loadDeck = async () => {
+	let deck;
+
+	try {
+		deck = await misskeyApi('i/registry/get', {
+			scope: ['client', 'deck', 'profiles'],
+			key: store.state['deck.profile'],
+		});
+	} catch (err) {
+		if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
+			// 後方互換性のため
+			if (store.state['deck.profile'] === 'default') {
+				saveDeck();
+				return;
+			}
+
+			store.set('deck.columns', []);
+			store.set('deck.layout', []);
+			return;
+		}
+		throw err;
+	}
+
+	store.set('deck.columns', deck.columns);
+	store.set('deck.layout', deck.layout);
+};
+
+export async function forceSaveDeck() {
+	await misskeyApi('i/registry/set', {
+		scope: ['client', 'deck', 'profiles'],
+		key: store.state['deck.profile'],
+		value: {
+			columns: store.reactiveState['deck.columns'].value,
+			layout: store.reactiveState['deck.layout'].value,
+		},
+	});
+}
+
+// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
+export const saveDeck = throttle(1000, () => {
+	forceSaveDeck();
+});
+
+export async function getProfiles(): Promise<string[]> {
+	return await misskeyApi('i/registry/keys', {
+		scope: ['client', 'deck', 'profiles'],
+	});
+}
+
+export async function deleteProfile(key: string): Promise<void> {
+	return await misskeyApi('i/registry/remove', {
+		scope: ['client', 'deck', 'profiles'],
+		key: key,
+	});
+}
+
+export function addColumn(column: Column) {
+	if (column.name === undefined) column.name = null;
+	store.push('deck.columns', column);
+	store.push('deck.layout', [column.id]);
+	saveDeck();
+}
+
+export function removeColumn(id: Column['id']) {
+	store.set('deck.columns', store.state['deck.columns'].filter(c => c.id !== id));
+	store.set('deck.layout', store.state['deck.layout']
+		.map(ids => ids.filter(_id => _id !== id))
+		.filter(ids => ids.length > 0));
+	saveDeck();
+}
+
+export function swapColumn(a: Column['id'], b: Column['id']) {
+	const aX = store.state['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1);
+	const aY = store.state['deck.layout'][aX].findIndex(id => id === a);
+	const bX = store.state['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1);
+	const bY = store.state['deck.layout'][bX].findIndex(id => id === b);
+	const layout = deepClone(store.state['deck.layout']);
+	layout[aX][aY] = b;
+	layout[bX][bY] = a;
+	store.set('deck.layout', layout);
+	saveDeck();
+}
+
+export function swapLeftColumn(id: Column['id']) {
+	const layout = deepClone(store.state['deck.layout']);
+	store.state['deck.layout'].some((ids, i) => {
+		if (ids.includes(id)) {
+			const left = store.state['deck.layout'][i - 1];
+			if (left) {
+				layout[i - 1] = store.state['deck.layout'][i];
+				layout[i] = left;
+				store.set('deck.layout', layout);
+			}
+			return true;
+		}
+		return false;
+	});
+	saveDeck();
+}
+
+export function swapRightColumn(id: Column['id']) {
+	const layout = deepClone(store.state['deck.layout']);
+	store.state['deck.layout'].some((ids, i) => {
+		if (ids.includes(id)) {
+			const right = store.state['deck.layout'][i + 1];
+			if (right) {
+				layout[i + 1] = store.state['deck.layout'][i];
+				layout[i] = right;
+				store.set('deck.layout', layout);
+			}
+			return true;
+		}
+		return false;
+	});
+	saveDeck();
+}
+
+export function swapUpColumn(id: Column['id']) {
+	const layout = deepClone(store.state['deck.layout']);
+	const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id));
+	const ids = deepClone(store.state['deck.layout'][idsIndex]);
+	ids.some((x, i) => {
+		if (x === id) {
+			const up = ids[i - 1];
+			if (up) {
+				ids[i - 1] = id;
+				ids[i] = up;
+
+				layout[idsIndex] = ids;
+				store.set('deck.layout', layout);
+			}
+			return true;
+		}
+		return false;
+	});
+	saveDeck();
+}
+
+export function swapDownColumn(id: Column['id']) {
+	const layout = deepClone(store.state['deck.layout']);
+	const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id));
+	const ids = deepClone(store.state['deck.layout'][idsIndex]);
+	ids.some((x, i) => {
+		if (x === id) {
+			const down = ids[i + 1];
+			if (down) {
+				ids[i + 1] = id;
+				ids[i] = down;
+
+				layout[idsIndex] = ids;
+				store.set('deck.layout', layout);
+			}
+			return true;
+		}
+		return false;
+	});
+	saveDeck();
+}
+
+export function stackLeftColumn(id: Column['id']) {
+	let layout = deepClone(store.state['deck.layout']);
+	const i = store.state['deck.layout'].findIndex(ids => ids.includes(id));
+	layout = layout.map(ids => ids.filter(_id => _id !== id));
+	layout[i - 1].push(id);
+	layout = layout.filter(ids => ids.length > 0);
+	store.set('deck.layout', layout);
+	saveDeck();
+}
+
+export function popRightColumn(id: Column['id']) {
+	let layout = deepClone(store.state['deck.layout']);
+	const i = store.state['deck.layout'].findIndex(ids => ids.includes(id));
+	const affected = layout[i];
+	layout = layout.map(ids => ids.filter(_id => _id !== id));
+	layout.splice(i + 1, 0, [id]);
+	layout = layout.filter(ids => ids.length > 0);
+	store.set('deck.layout', layout);
+
+	const columns = deepClone(store.state['deck.columns']);
+	for (const column of columns) {
+		if (affected.includes(column.id)) {
+			column.active = true;
+		}
+	}
+	store.set('deck.columns', columns);
+
+	saveDeck();
+}
+
+export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
+	const columns = deepClone(store.state['deck.columns']);
+	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
+	const column = deepClone(store.state['deck.columns'][columnIndex]);
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
+	column.widgets.unshift(widget);
+	columns[columnIndex] = column;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
+
+export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
+	const columns = deepClone(store.state['deck.columns']);
+	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
+	const column = deepClone(store.state['deck.columns'][columnIndex]);
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
+	column.widgets = column.widgets.filter(w => w.id !== widget.id);
+	columns[columnIndex] = column;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
+
+export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
+	const columns = deepClone(store.state['deck.columns']);
+	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
+	const column = deepClone(store.state['deck.columns'][columnIndex]);
+	if (column == null) return;
+	column.widgets = widgets;
+	columns[columnIndex] = column;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
+
+export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
+	const columns = deepClone(store.state['deck.columns']);
+	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
+	const column = deepClone(store.state['deck.columns'][columnIndex]);
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
+	column.widgets = column.widgets.map(w => w.id === widgetId ? {
+		...w,
+		data: widgetData,
+	} : w);
+	columns[columnIndex] = column;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
+
+export function updateColumn(id: Column['id'], column: Partial<Column>) {
+	const columns = deepClone(store.state['deck.columns']);
+	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
+	const currentColumn = deepClone(store.state['deck.columns'][columnIndex]);
+	if (currentColumn == null) return;
+	for (const [k, v] of Object.entries(column)) {
+		currentColumn[k] = v;
+	}
+	columns[columnIndex] = currentColumn;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts
index 60242837f2..c34f351fb3 100644
--- a/packages/frontend/src/directives/click-anime.ts
+++ b/packages/frontend/src/directives/click-anime.ts
@@ -4,11 +4,11 @@
  */
 
 import type { Directive } from 'vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 export default {
 	mounted(el: HTMLElement, binding, vn) {
-		if (!defaultStore.state.animation) return;
+		if (!prefer.s.animation) return;
 
 		const target = el.children[0];
 
diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts
index 99845c57c3..614cd37011 100644
--- a/packages/frontend/src/directives/ripple.ts
+++ b/packages/frontend/src/directives/ripple.ts
@@ -4,14 +4,14 @@
  */
 
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { popup } from '@/os.js';
 
 export default {
 	mounted(el, binding, vn) {
 		// 明示的に false であればバインドしない
 		if (binding.value === false) return;
-		if (!defaultStore.state.animation) return;
+		if (!prefer.s.animation) return;
 
 		el.addEventListener('click', () => {
 			const rect = el.getBoundingClientRect();
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 9c8863f863..3977edb91b 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -19,7 +19,6 @@ export type Keys = (
 	'drafts' |
 	'hashtags' |
 	'wallpaper' |
-	'theme' |
 	'colorScheme' |
 	'useSystemFont' |
 	'fontSize' |
@@ -28,13 +27,17 @@ export type Keys = (
 	'locale' |
 	'localeVersion' |
 	'theme' |
+	'themeId' |
 	'customCss' |
 	'message_drafts' |
 	'scratchpad' |
 	'debug' |
+	'preferences' |
+	'latestPreferencesUpdate' |
+	'hidePreferencesRestoreSuggestion' |
 	`miux:${string}` |
 	`ui:folder:${string}` |
-	`themes:${string}` |
+	`themes:${string}` | // DEPRECATED
 	`aiscript:${string}` |
 	'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
 	'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 6e48366092..2142432953 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -6,15 +6,15 @@
 // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
 
 import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
-import type { Component, Ref } from 'vue';
 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 { MenuItem } from '@/types/menu.js';
 import type { PostFormProps } from '@/types/post-form.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
 import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
@@ -626,7 +626,7 @@ export async function selectRole(params: {
 }): Promise<
 	{ canceled: true; result: undefined; } |
 	{ canceled: false; result: Misskey.entities.Role[] }
-> {
+	> {
 	return new Promise((resolve) => {
 		popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, {
 			done: roles => {
@@ -699,8 +699,8 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n
 
 export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
 	if (
-		defaultStore.state.contextMenu === 'native' ||
-		(defaultStore.state.contextMenu === 'appWithShift' && !ev.shiftKey)
+		prefer.s.contextMenu === 'native' ||
+		(prefer.s.contextMenu === 'appWithShift' && !ev.shiftKey)
 	) {
 		return Promise.resolve();
 	}
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index f09a8e4285..9e3a6777f1 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkLoading v-if="!loaded"/>
-<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
+<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
 	<div v-show="loaded" :class="$style.root">
 		<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
 		<div class="_gaps">
@@ -27,15 +27,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
 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 { version } from '@@/js/config.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { serverErrorImageUrl } from '@/instance.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 29772ae00a..6e5b52ea8a 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -143,7 +143,7 @@ import MkInfo from '@/components/MkInfo.vue';
 import { physics } from '@/scripts/physics.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { defaultStore } from '@/store.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';
@@ -406,7 +406,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
 const containerEl = shallowRef<HTMLElement>();
 
 function iconLoaded() {
-	const emojis = defaultStore.state.reactions;
+	const emojis = store.state.reactions;
 	const containerWidth = containerEl.value.offsetWidth;
 	for (let i = 0; i < 32; i++) {
 		easterEggEmojis.value.push({
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index 22173bb888..c7a45a502e 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
 			</div>
 
-			<MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
+			<MkInfo v-if="!store.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
 				{{ i18n.ts._abuseUserReport.resolveTutorial }}
 			</MkInfo>
 
@@ -68,7 +68,7 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 const reports = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -93,7 +93,7 @@ function resolved(reportId) {
 }
 
 function closeTutorial() {
-	defaultStore.set('abusesTutorial', false);
+	store.set('abusesTutorial', false);
 }
 
 const headerActions = computed(() => []);
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 d6ee8ea49c..6537b844c3 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
@@ -78,6 +78,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 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 { GridSetting } from '@/components/grid/grid.js';
+import type { GridRow } from '@/components/grid/row.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import {
 	emptyStrToEmptyArray,
@@ -88,7 +93,6 @@ import MkGrid from '@/components/grid/MkGrid.vue';
 import { i18n } from '@/i18n.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { defaultStore } from '@/store.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
@@ -99,11 +103,7 @@ import { extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js
 import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
 import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
 
-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 { GridSetting } from '@/components/grid/grid.js';
-import type { GridRow } from '@/components/grid/row.js';
+import { prefer } from '@/preferences.js';
 
 const MAXIMUM_EMOJI_REGISTER_COUNT = 100;
 
@@ -244,8 +244,8 @@ function setupGrid(): GridSetting {
 
 const uploadFolders = ref<FolderItem[]>([]);
 const gridItems = ref<GridItem[]>([]);
-const selectedFolderId = ref(defaultStore.state.uploadFolder);
-const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
+const selectedFolderId = ref(prefer.s.uploadFolder);
+const keepOriginalUploading = ref(prefer.s.keepOriginalUploading);
 const directoryToCategory = ref<boolean>(false);
 const registerButtonDisabled = ref<boolean>(false);
 const requestLogs = ref<RequestLogItem[]>([]);
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 79dd6fd5fd..1a72f9050c 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -17,7 +17,7 @@ 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 { defaultStore } from '@/store.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';
@@ -54,7 +54,7 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const colorRead = '#3498db';
 	const colorWrite = '#2ecc71';
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index 570fcddc07..197f792755 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -27,7 +27,7 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { alpha } from '@/scripts/color.js';
 import { initChart } from '@/scripts/init-chart.js';
 
@@ -68,7 +68,7 @@ onMounted(async () => {
 
 	const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 	const succColor = '#87e000';
 	const failColor = '#ff4400';
 
diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue
index 292e2e1dbc..67c0297dec 100644
--- a/packages/frontend/src/pages/admin/overview.instances.vue
+++ b/packages/frontend/src/pages/admin/overview.instances.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
 		<MkLoading v-if="fetching"/>
 		<div v-else :class="$style.instances">
 			<MkA v-for="(instance, i) in instances" :key="instance.id" v-tooltip.mfm.noDelay="`${instance.name}\n${instance.host}\n${instance.softwareName} ${instance.softwareVersion}`" :to="`/instance-info/${instance.host}`" :class="$style.instance">
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const instances = ref<Misskey.entities.FederationInstance[]>([]);
 const fetching = ref(true);
diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue
index f0691534c8..4d503c0035 100644
--- a/packages/frontend/src/pages/admin/overview.moderators.vue
+++ b/packages/frontend/src/pages/admin/overview.moderators.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
 		<MkLoading v-if="fetching"/>
 		<div v-else :class="$style.root" class="_panel">
 			<MkA v-for="user in moderators" :key="user.id" class="user" :to="`/admin/user/${user.id}`">
@@ -18,9 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as Misskey from 'misskey-js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { prefer } from '@/preferences.js';
 
 const moderators = ref<Misskey.entities.UserDetailed[] | null>(null);
 const fetching = ref(true);
diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue
index 2efc17c888..7b83641fbd 100644
--- a/packages/frontend/src/pages/admin/overview.queue.chart.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
-import { defaultStore } from '@/store.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';
@@ -67,7 +67,7 @@ const color =
 	'?' as never;
 
 onMounted(() => {
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	chartInstance = new Chart(chartEl.value, {
 		type: 'line',
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 222e9f4673..72b93ee97c 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
 		<MkLoading v-if="fetching"/>
 		<div v-else :class="$style.root">
 			<div class="item _panel users">
@@ -68,7 +68,7 @@ import MkNumberDiff from '@/components/MkNumberDiff.vue';
 import MkNumber from '@/components/MkNumber.vue';
 import { i18n } from '@/i18n.js';
 import { customEmojis } from '@/custom-emojis.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const stats = ref<Misskey.entities.StatsResponse | null>(null);
 const usersComparedToThePrevDay = ref<number>();
diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue
index 8c9d7a8197..8de4f298ce 100644
--- a/packages/frontend/src/pages/admin/overview.users.vue
+++ b/packages/frontend/src/pages/admin/overview.users.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.root">
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
 		<MkLoading v-if="fetching"/>
 		<div v-else class="users">
 			<MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/admin/user/${user.id}`" class="user">
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null);
 const fetching = ref(true);
diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue
index cc18898172..debb48a15a 100644
--- a/packages/frontend/src/pages/admin/queue.chart.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
-import { defaultStore } from '@/store.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';
@@ -67,7 +67,7 @@ const color =
 	'?' as never;
 
 onMounted(() => {
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	chartInstance = new Chart(chartEl.value, {
 		type: 'line',
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index 56c10fb292..b8903a8ea6 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="800">
 		<Transition
-			:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
-			:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
-			:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
-			:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
+			:enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''"
+			:leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''"
+			:enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''"
+			:leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''"
 			mode="out-in"
 		>
 			<div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
@@ -56,7 +56,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { $i, updateAccountPartial } from '@/account.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	announcementId: string;
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 4a91165d50..d333204ff8 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
 
 				<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
-				<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
+				<MkPostForm v-if="$i && prefer.r.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
 
 				<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
 			</div>
@@ -75,6 +75,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
+import type { PageHeaderItem } from '@/types/page-header.js';
 import MkPostForm from '@/components/MkPostForm.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
@@ -85,16 +87,14 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import MkNotes from '@/components/MkNotes.vue';
-import { url } from '@@/js/config.js';
 import { favoritedChannelsCache } from '@/cache.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 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 type { PageHeaderItem } from '@/types/page-header.js';
 import { isSupportShare } from '@/scripts/navigator.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { notesSearchAvailable } from '@/scripts/check-permissions.js';
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index 10099e6291..55c984e1bf 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
-				<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
+				<img v-if="store.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
 				<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
 				<canvas ref="canvasEl" :class="$style.canvas"/>
 				<Transition
@@ -195,6 +195,8 @@ import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch
 import * as Matter from 'matter-js';
 import * as Misskey from 'misskey-js';
 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 MkRippleEffect from '@/components/MkRippleEffect.vue';
@@ -203,15 +205,14 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { useInterval } from '@@/js/use-interval.js';
-import { apiUrl } from '@@/js/config.js';
 import { $i } from '@/account.js';
 import * as sound from '@/scripts/sound.js';
 import MkRange from '@/components/MkRange.vue';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { prefer } from '@/preferences.js';
 
 type FrontendMonoDefinition = {
 	id: string;
@@ -586,8 +587,8 @@ const showConfig = ref(false);
 const replaying = ref(false);
 const replayPlaybackRate = ref(1);
 const currentFrame = ref(0);
-const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
-const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
+const bgmVolume = ref(prefer.s['game.dropAndFusion'].bgmVolume);
+const sfxVolume = ref(prefer.s['game.dropAndFusion'].sfxVolume);
 
 watch(replayPlaybackRate, (newValue) => {
 	game.replayPlaybackRate = newValue;
@@ -623,7 +624,7 @@ function loadMonoTextures() {
 		if (renderer.textures[mono.img]) return;
 
 		let src = mono.img;
-		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+
 		if (monoTextureUrls[mono.img]) {
 			src = monoTextureUrls[mono.img];
 			// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -649,7 +650,6 @@ function loadMonoTextures() {
 function getTextureImageUrl(mono: Mono) {
 	const def = monoDefinitions.value.find(x => x.id === mono.id)!;
 
-	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 	if (monoTextureUrls[def.img]) {
 		return monoTextureUrls[def.img];
 
@@ -853,13 +853,13 @@ function exportLog() {
 }
 
 function updateSettings<
-	K extends keyof typeof defaultStore.state.dropAndFusion,
-	V extends typeof defaultStore.state.dropAndFusion[K],
+	K extends keyof typeof prefer.s['game.dropAndFusion'],
+	V extends typeof prefer.s['game.dropAndFusion'][K],
 >(key: K, value: V) {
 	const changes: { [P in K]?: V } = {};
 	changes[key] = value;
-	defaultStore.set('dropAndFusion', {
-		...defaultStore.state.dropAndFusion,
+	prefer.set('game.dropAndFusion', {
+		...prefer.s['game.dropAndFusion'],
 		...changes,
 	});
 }
@@ -909,8 +909,8 @@ function getGameImageDriveFile() {
 				formData.append('name', `bubble-game-${Date.now()}.png`);
 				formData.append('isSensitive', 'false');
 				formData.append('i', $i.token);
-				if (defaultStore.state.uploadFolder) {
-					formData.append('folderId', defaultStore.state.uploadFolder);
+				if (prefer.s.uploadFolder) {
+					formData.append('folderId', prefer.s.uploadFolder);
 				}
 
 				window.fetch(apiUrl + '/drive/files/create', {
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 6294a3f4a2..532ec87dac 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="700">
-		<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
+		<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
 			<div v-if="flash" :key="flash.id">
-				<Transition :name="defaultStore.state.animation ? 'zoom' : ''" mode="out-in">
+				<Transition :name="prefer.s.animation ? 'zoom' : ''" mode="out-in">
 					<div v-if="started" :class="$style.started">
 						<div class="main _panel">
 							<MkAsUi v-if="root" :component="root" :components="components"/>
@@ -79,7 +79,7 @@ import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
 import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkCode from '@/components/MkCode.vue';
-import { defaultStore } from '@/store.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';
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index feb4c60611..7e1b7be16c 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="1000" :marginMin="16" :marginMax="32">
 		<div class="_root">
-			<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
+			<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
 				<div v-if="post" class="rkxwuolj">
 					<div class="files">
 						<div v-for="file in post.files" :key="file.id" class="file">
@@ -65,6 +65,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
+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';
@@ -72,15 +74,13 @@ 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 { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { defaultStore } from '@/store.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 { useRouter } from '@/router/supplier.js';
-import type { MenuItem } from '@/types/menu.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 58f8b865bb..423bd611e6 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -45,18 +45,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue';
+import type { Extension } from '@/components/MkExtensionInstaller.vue';
+import type { AiScriptPluginMeta } from '@/plugin.js';
 import MkLoading from '@/components/global/MkLoading.vue';
 import MkExtensionInstaller from '@/components/MkExtensionInstaller.vue';
-import type { Extension } from '@/components/MkExtensionInstaller.vue';
 import MkButton from '@/components/MkButton.vue';
 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 { parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
-import type { AiScriptPluginMeta } from '@/scripts/install-plugin.js';
-import { parseThemeCode, installTheme } from '@/scripts/install-theme.js';
+import { parsePluginMeta, installPlugin } from '@/plugin.js';
+import { parseThemeCode, installTheme } from '@/scripts/theme.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 69e404bd85..acedeeeb7d 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -67,15 +67,15 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkInput from '@/components/MkInput.vue';
 import { userListsCache } from '@/cache.js';
 import { signinRequired } from '@/account.js';
-import { defaultStore } from '@/store.js';
 import MkPagination from '@/components/MkPagination.vue';
 import { mainRouter } from '@/router/main.js';
+import { prefer } from '@/preferences.js';
 
 const $i = signinRequired();
 
 const {
 	enableInfiniteScroll,
-} = defaultStore.reactiveState;
+} = prefer.r;
 
 const props = defineProps<{
 	listId: string;
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 0791c1343b..eaeef2e566 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="800">
 		<div>
-			<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
+			<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
 				<div v-if="note">
 					<div v-if="showNext" class="_margin">
 						<MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
@@ -61,7 +61,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { dateString } from '@/filters/date.js';
 import MkClipPreview from '@/components/MkClipPreview.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { pleaseLogin } from '@/scripts/please-login.js';
 import { getAppearNote } from '@/scripts/get-appear-note.js';
 import { serverContext, assertServerContext } from '@/server-context.js';
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index d9ad7babb7..3444ef49aa 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="800">
 		<Transition
-			:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
-			:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
-			:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
-			:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
+			:enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''"
+			:leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''"
+			:enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''"
+			:leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''"
 			mode="out-in"
 		>
 			<div v-if="page" :key="page.id" class="_gaps">
@@ -100,11 +100,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
+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 { url } from '@@/js/config.js';
 import MkMediaImage from '@/components/MkMediaImage.vue';
 import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
@@ -113,7 +114,7 @@ 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 { pageViewInterruptors, defaultStore } from '@/store.js';
+import { pageViewInterruptors } from '@/store.js';
 import { deepClone } from '@/scripts/clone.js';
 import { $i } from '@/account.js';
 import { isSupportShare } from '@/scripts/navigator.js';
@@ -121,7 +122,7 @@ import { instance } from '@/instance.js';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
-import type { MenuItem } from '@/types/menu.js';
+import { prefer } from '@/preferences.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue
index b703be1fe1..f464f728ff 100644
--- a/packages/frontend/src/pages/settings/accessibility.vue
+++ b/packages/frontend/src/pages/settings/accessibility.vue
@@ -8,49 +8,63 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="_gaps_m">
 		<div class="_gaps_s">
 			<SearchMarker :keywords="['animation', 'motion', 'reduce']">
-				<MkSwitch v-model="reduceAnimation">
-					<template #label><SearchLabel>{{ i18n.ts.reduceUiAnimation }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="animation">
+					<MkSwitch v-model="reduceAnimation">
+						<template #label><SearchLabel>{{ i18n.ts.reduceUiAnimation }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif']">
-				<MkSwitch v-model="disableShowingAnimatedImages">
-					<template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="disableShowingAnimatedImages">
+					<MkSwitch v-model="disableShowingAnimatedImages">
+						<template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['mfm', 'enable', 'show', 'animated']">
-				<MkSwitch v-model="animatedMfm">
-					<template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="animatedMfm">
+					<MkSwitch v-model="animatedMfm">
+						<template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['swipe', 'horizontal', 'tab']">
-				<MkSwitch v-model="enableHorizontalSwipe">
-					<template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="enableHorizontalSwipe">
+					<MkSwitch v-model="enableHorizontalSwipe">
+						<template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['keep', 'screen', 'display', 'on']">
-				<MkSwitch v-model="keepScreenOn">
-					<template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="keepScreenOn">
+					<MkSwitch v-model="keepScreenOn">
+						<template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['native', 'system', 'video', 'audio', 'player', 'media']">
-				<MkSwitch v-model="useNativeUIForVideoAudioPlayer">
-					<template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="useNativeUiForVideoAudioPlayer">
+					<MkSwitch v-model="useNativeUiForVideoAudioPlayer">
+						<template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 		</div>
 
 		<SearchMarker :keywords="['contextmenu', 'system', 'native']">
-			<MkSelect v-model="contextMenu">
-				<template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template>
-				<option value="app">{{ i18n.ts._contextMenu.app }}</option>
-				<option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option>
-				<option value="native">{{ i18n.ts._contextMenu.native }}</option>
-			</MkSelect>
+			<MkPreferenceContainer k="contextMenu">
+				<MkSelect v-model="contextMenu">
+					<template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template>
+					<option value="app">{{ i18n.ts._contextMenu.app }}</option>
+					<option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option>
+					<option value="native">{{ i18n.ts._contextMenu.native }}</option>
+				</MkSelect>
+			</MkPreferenceContainer>
 		</SearchMarker>
 	</div>
 </SearchMarker>
@@ -60,18 +74,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref, watch } from 'vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { reloadAsk } from '@/scripts/reload-ask.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
-const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
-const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
-const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
-const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
-const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
-const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
-const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
+const reduceAnimation = prefer.model('animation', v => !v, v => !v);
+const animatedMfm = prefer.model('animatedMfm');
+const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages');
+const keepScreenOn = prefer.model('keepScreenOn');
+const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
+const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
+const contextMenu = prefer.model('contextMenu');
 
 watch([
 	keepScreenOn,
diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue
index 465c2a38c2..b23f32aec4 100644
--- a/packages/frontend/src/pages/settings/appearance.vue
+++ b/packages/frontend/src/pages/settings/appearance.vue
@@ -10,73 +10,85 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div class="_gaps_m">
 				<div class="_gaps_s">
 					<SearchMarker :keywords="['blur']">
-						<MkSwitch v-model="useBlurEffect">
-							<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="useBlurEffect">
+							<MkSwitch v-model="useBlurEffect">
+								<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['blur', 'modal']">
-						<MkSwitch v-model="useBlurEffectForModal">
-							<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="useBlurEffectForModal">
+							<MkSwitch v-model="useBlurEffectForModal">
+								<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail']">
-						<MkSwitch v-model="highlightSensitiveMedia">
-							<template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="highlightSensitiveMedia">
+							<MkSwitch v-model="highlightSensitiveMedia">
+								<template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['avatar', 'icon', 'square']">
-						<MkSwitch v-model="squareAvatars">
-							<template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="squareAvatars">
+							<MkSwitch v-model="squareAvatars">
+								<template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
-						<MkSwitch v-model="showAvatarDecorations">
-							<template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="showAvatarDecorations">
+							<MkSwitch v-model="showAvatarDecorations">
+								<template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['note', 'timeline', 'gap']">
-						<MkSwitch v-model="showGapBetweenNotesInTimeline">
-							<template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template>
-						</MkSwitch>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['font', 'system', 'native']">
-						<MkSwitch v-model="useSystemFont">
-							<template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="showGapBetweenNotesInTimeline">
+							<MkSwitch v-model="showGapBetweenNotesInTimeline">
+								<template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['effect', 'show']">
-						<MkSwitch v-model="enableSeasonalScreenEffect">
-							<template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="enableSeasonalScreenEffect">
+							<MkSwitch v-model="enableSeasonalScreenEffect">
+								<template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 				</div>
 
 				<SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']">
-					<MkSelect v-model="menuStyle">
-						<template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template>
-						<option value="auto">{{ i18n.ts.auto }}</option>
-						<option value="popup">{{ i18n.ts.popup }}</option>
-						<option value="drawer">{{ i18n.ts.drawer }}</option>
-					</MkSelect>
+					<MkPreferenceContainer k="menuStyle">
+						<MkSelect v-model="menuStyle">
+							<template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template>
+							<option value="auto">{{ i18n.ts.auto }}</option>
+							<option value="popup">{{ i18n.ts.popup }}</option>
+							<option value="drawer">{{ i18n.ts.drawer }}</option>
+						</MkSelect>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
-					<div>
-						<MkRadios v-model="emojiStyle">
-							<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
-							<option value="native">{{ i18n.ts.native }}</option>
-							<option value="fluentEmoji">Fluent Emoji</option>
-							<option value="twemoji">Twemoji</option>
-						</MkRadios>
-						<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
-					</div>
+					<MkPreferenceContainer k="emojiStyle">
+						<div>
+							<MkRadios v-model="emojiStyle">
+								<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
+								<option value="native">{{ i18n.ts.native }}</option>
+								<option value="fluentEmoji">Fluent Emoji</option>
+								<option value="twemoji">Twemoji</option>
+							</MkRadios>
+							<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
+						</div>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['font', 'size']">
@@ -88,6 +100,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<option value="3"><span style="font-size: 17px;">Aa</span></option>
 					</MkRadios>
 				</SearchMarker>
+
+				<SearchMarker :keywords="['font', 'system', 'native']">
+					<MkSwitch v-model="useSystemFont">
+						<template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template>
+					</MkSwitch>
+				</SearchMarker>
 			</div>
 		</FormSection>
 
@@ -97,46 +115,56 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps_m">
 					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
-						<MkRadios v-model="reactionsDisplaySize">
-							<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
-							<option value="small">{{ i18n.ts.small }}</option>
-							<option value="medium">{{ i18n.ts.medium }}</option>
-							<option value="large">{{ i18n.ts.large }}</option>
-						</MkRadios>
+						<MkPreferenceContainer k="reactionsDisplaySize">
+							<MkRadios v-model="reactionsDisplaySize">
+								<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
+								<option value="small">{{ i18n.ts.small }}</option>
+								<option value="medium">{{ i18n.ts.medium }}</option>
+								<option value="large">{{ i18n.ts.large }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display', 'width', 'limit']">
-						<MkSwitch v-model="limitWidthOfReaction">
-							<template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="limitWidthOfReaction">
+							<MkSwitch v-model="limitWidthOfReaction">
+								<template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
-						<MkRadios v-model="mediaListWithOneImageAppearance">
-							<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
-							<option value="expand">{{ i18n.ts.default }}</option>
-							<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
-							<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
-							<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
-						</MkRadios>
+						<MkPreferenceContainer k="mediaListWithOneImageAppearance">
+							<MkRadios v-model="mediaListWithOneImageAppearance">
+								<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
+								<option value="expand">{{ i18n.ts.default }}</option>
+								<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
+								<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
+								<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']">
-						<MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
-							<template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template>
-							<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
-							<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
-							<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
-						</MkSelect>
+						<MkPreferenceContainer k="instanceTicker">
+							<MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
+								<template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template>
+								<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
+								<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
+								<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
+							</MkSelect>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility']">
-						<MkSelect v-model="nsfw">
-							<template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template>
-							<option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option>
-							<option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option>
-							<option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option>
-						</MkSelect>
+						<MkPreferenceContainer k="nsfw">
+							<MkSelect v-model="nsfw">
+								<template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template>
+								<option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option>
+								<option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option>
+								<option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option>
+							</MkSelect>
+						</MkPreferenceContainer>
 					</SearchMarker>
 				</div>
 			</FormSection>
@@ -148,21 +176,25 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps_m">
 					<SearchMarker :keywords="['position']">
-						<MkRadios v-model="notificationPosition">
-							<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
-							<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
-							<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
-							<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
-							<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
-						</MkRadios>
+						<MkPreferenceContainer k="notificationPosition">
+							<MkRadios v-model="notificationPosition">
+								<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
+								<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
+								<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
+								<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
+								<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['stack', 'axis', 'direction']">
-						<MkRadios v-model="notificationStackAxis">
-							<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
-							<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
-							<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
-						</MkRadios>
+						<MkPreferenceContainer k="notificationStackAxis">
+							<MkRadios v-model="notificationStackAxis">
+								<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
+								<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
+								<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
@@ -183,7 +215,7 @@ import * as Misskey from 'misskey-js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkRadios from '@/components/MkRadios.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { reloadAsk } from '@/scripts/reload-ask.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -194,26 +226,27 @@ import { claimAchievement } from '@/scripts/achievements.js';
 import MkButton from '@/components/MkButton.vue';
 import FormSection from '@/components/form/section.vue';
 import { instance } from '@/instance.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
 const fontSize = ref(miLocalStorage.getItem('fontSize'));
 const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
 
-const showAvatarDecorations = computed(defaultStore.makeGetterSetter('showAvatarDecorations'));
-const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
-const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle'));
-const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
-const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
-const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
-const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
-const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
-const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
-const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance'));
-const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
-const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
-const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
-const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
-const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
-const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
+const showAvatarDecorations = prefer.model('showAvatarDecorations');
+const emojiStyle = prefer.model('emojiStyle');
+const menuStyle = prefer.model('menuStyle');
+const useBlurEffectForModal = prefer.model('useBlurEffectForModal');
+const useBlurEffect = prefer.model('useBlurEffect');
+const highlightSensitiveMedia = prefer.model('highlightSensitiveMedia');
+const squareAvatars = prefer.model('squareAvatars');
+const enableSeasonalScreenEffect = prefer.model('enableSeasonalScreenEffect');
+const showGapBetweenNotesInTimeline = prefer.model('showGapBetweenNotesInTimeline');
+const mediaListWithOneImageAppearance = prefer.model('mediaListWithOneImageAppearance');
+const reactionsDisplaySize = prefer.model('reactionsDisplaySize');
+const limitWidthOfReaction = prefer.model('limitWidthOfReaction');
+const notificationPosition = prefer.model('notificationPosition');
+const notificationStackAxis = prefer.model('notificationStackAxis');
+const nsfw = prefer.model('nsfw');
+const instanceTicker = prefer.model('instanceTicker');
 
 watch(fontSize, () => {
 	if (fontSize.value == null) {
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index e574ec7dc0..e1965b6d36 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -23,14 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
-import { deckStore } from '@/ui/deck/deck-store.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { prefer } from '@/preferences.js';
 
-const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
-const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpleUiForNonRootPages'));
-const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
-const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
+const navWindow = prefer.model('deck.navWindow');
+const useSimpleUiForNonRootPages = prefer.model('deck.useSimpleUiForNonRootPages');
+const alwaysShowMainColumn = prefer.model('deck.alwaysShowMainColumn');
+const columnAlign = prefer.model('deck.columnAlign');
 
 const headerActions = computed(() => []);
 
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 0138aac1c5..8e61b11dbe 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -50,17 +50,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</FormLink>
 
 				<SearchMarker :keywords="['keep', 'original', 'raw', 'upload']">
-					<MkSwitch v-model="keepOriginalUploading">
-						<template #label><SearchLabel>{{ i18n.ts.keepOriginalUploading }}</SearchLabel></template>
-						<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalUploadingDescription }}</SearchKeyword></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="keepOriginalUploading">
+						<MkSwitch v-model="keepOriginalUploading">
+							<template #label><SearchLabel>{{ i18n.ts.keepOriginalUploading }}</SearchLabel></template>
+							<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalUploadingDescription }}</SearchKeyword></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['keep', 'original', 'filename']">
-					<MkSwitch v-model="keepOriginalFilename">
-						<template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template>
-						<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="keepOriginalFilename">
+						<MkSwitch v-model="keepOriginalFilename">
+							<template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template>
+							<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file']">
@@ -93,11 +97,12 @@ import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import bytes from '@/filters/bytes.js';
-import { defaultStore } from '@/store.js';
 import MkChart from '@/components/MkChart.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { signinRequired } from '@/account.js';
+import { prefer } from '@/preferences.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
 const $i = signinRequired();
 
@@ -120,8 +125,8 @@ const meterStyle = computed(() => {
 	};
 });
 
-const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading'));
-const keepOriginalFilename = computed(defaultStore.makeGetterSetter('keepOriginalFilename'));
+const keepOriginalUploading = prefer.model('keepOriginalUploading');
+const keepOriginalFilename = prefer.model('keepOriginalFilename');
 
 misskeyApi('drive').then(info => {
 	capacity.value = info.capacity;
@@ -129,9 +134,9 @@ misskeyApi('drive').then(info => {
 	fetching.value = false;
 });
 
-if (defaultStore.state.uploadFolder) {
+if (prefer.s.uploadFolder) {
 	misskeyApi('drive/folders/show', {
-		folderId: defaultStore.state.uploadFolder,
+		folderId: prefer.s.uploadFolder,
 	}).then(response => {
 		uploadFolder.value = response;
 	});
@@ -139,11 +144,11 @@ if (defaultStore.state.uploadFolder) {
 
 function chooseUploadFolder() {
 	os.selectDriveFolder(false).then(async folder => {
-		defaultStore.set('uploadFolder', folder[0] ? folder[0].id : null);
+		prefer.set('uploadFolder', folder[0] ? folder[0].id : null);
 		os.success();
-		if (defaultStore.state.uploadFolder) {
+		if (prefer.s.uploadFolder) {
 			uploadFolder.value = await misskeyApi('drive/folders/show', {
-				folderId: defaultStore.state.uploadFolder,
+				folderId: prefer.s.uploadFolder,
 			});
 		} else {
 			uploadFolder.value = null;
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index b16c943676..d2060a9112 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -89,37 +89,45 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #label>{{ i18n.ts.emojiPickerDisplay }}</template>
 
 		<div class="_gaps_m">
-			<MkRadios v-model="emojiPickerScale">
-				<template #label>{{ i18n.ts.size }}</template>
-				<option :value="1">{{ i18n.ts.small }}</option>
-				<option :value="2">{{ i18n.ts.medium }}</option>
-				<option :value="3">{{ i18n.ts.large }}</option>
-			</MkRadios>
-
-			<MkRadios v-model="emojiPickerWidth">
-				<template #label>{{ i18n.ts.numberOfColumn }}</template>
-				<option :value="1">5</option>
-				<option :value="2">6</option>
-				<option :value="3">7</option>
-				<option :value="4">8</option>
-				<option :value="5">9</option>
-			</MkRadios>
-
-			<MkRadios v-model="emojiPickerHeight">
-				<template #label>{{ i18n.ts.height }}</template>
-				<option :value="1">{{ i18n.ts.small }}</option>
-				<option :value="2">{{ i18n.ts.medium }}</option>
-				<option :value="3">{{ i18n.ts.large }}</option>
-				<option :value="4">{{ i18n.ts.large }}+</option>
-			</MkRadios>
-
-			<MkSelect v-model="emojiPickerStyle">
-				<template #label>{{ i18n.ts.style }}</template>
-				<template #caption>{{ i18n.ts.needReloadToApply }}</template>
-				<option value="auto">{{ i18n.ts.auto }}</option>
-				<option value="popup">{{ i18n.ts.popup }}</option>
-				<option value="drawer">{{ i18n.ts.drawer }}</option>
-			</MkSelect>
+			<MkPreferenceContainer k="emojiPickerScale">
+				<MkRadios v-model="emojiPickerScale">
+					<template #label>{{ i18n.ts.size }}</template>
+					<option :value="1">{{ i18n.ts.small }}</option>
+					<option :value="2">{{ i18n.ts.medium }}</option>
+					<option :value="3">{{ i18n.ts.large }}</option>
+				</MkRadios>
+			</MkPreferenceContainer>
+
+			<MkPreferenceContainer k="emojiPickerWidth">
+				<MkRadios v-model="emojiPickerWidth">
+					<template #label>{{ i18n.ts.numberOfColumn }}</template>
+					<option :value="1">5</option>
+					<option :value="2">6</option>
+					<option :value="3">7</option>
+					<option :value="4">8</option>
+					<option :value="5">9</option>
+				</MkRadios>
+			</MkPreferenceContainer>
+
+			<MkPreferenceContainer k="emojiPickerHeight">
+				<MkRadios v-model="emojiPickerHeight">
+					<template #label>{{ i18n.ts.height }}</template>
+					<option :value="1">{{ i18n.ts.small }}</option>
+					<option :value="2">{{ i18n.ts.medium }}</option>
+					<option :value="3">{{ i18n.ts.large }}</option>
+					<option :value="4">{{ i18n.ts.large }}+</option>
+				</MkRadios>
+			</MkPreferenceContainer>
+
+			<MkPreferenceContainer k="emojiPickerStyle">
+				<MkSelect v-model="emojiPickerStyle">
+					<template #label>{{ i18n.ts.style }}</template>
+					<template #caption>{{ i18n.ts.needReloadToApply }}</template>
+					<option value="auto">{{ i18n.ts.auto }}</option>
+					<option value="popup">{{ i18n.ts.popup }}</option>
+					<option value="drawer">{{ i18n.ts.drawer }}</option>
+				</MkSelect>
+			</MkPreferenceContainer>
 		</div>
 	</FormSection>
 </div>
@@ -127,14 +135,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
-import type { Ref } from 'vue';
 import Sortable from 'vuedraggable';
+import type { Ref } from 'vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
 import FormSection from '@/components/form/section.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { deepClone } from '@/scripts/clone.js';
@@ -143,14 +151,16 @@ import { emojiPicker } from '@/scripts/emoji-picker.js';
 import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
 import MkEmoji from '@/components/global/MkEmoji.vue';
 import MkFolder from '@/components/MkFolder.vue';
+import { prefer } from '@/preferences.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
-const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions));
-const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis));
+const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(store.state.reactions));
+const pinnedEmojis: Ref<string[]> = ref(deepClone(store.state.pinnedEmojis));
 
-const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale'));
-const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
-const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
-const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle'));
+const emojiPickerScale = prefer.model('emojiPickerScale');
+const emojiPickerWidth = prefer.model('emojiPickerWidth');
+const emojiPickerHeight = prefer.model('emojiPickerHeight');
+const emojiPickerStyle = prefer.model('emojiPickerStyle');
 
 const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
 const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);
@@ -210,7 +220,7 @@ async function setDefault(itemsRef: Ref<string[]>) {
 	});
 	if (canceled) return;
 
-	itemsRef.value = deepClone(defaultStore.def.reactions.default);
+	itemsRef.value = deepClone(store.def.reactions.default);
 }
 
 async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
@@ -230,13 +240,13 @@ function getHTMLElement(ev: MouseEvent): HTMLElement {
 }
 
 watch(pinnedEmojisForReaction, () => {
-	defaultStore.set('reactions', pinnedEmojisForReaction.value);
+	store.set('reactions', pinnedEmojisForReaction.value);
 }, {
 	deep: true,
 });
 
 watch(pinnedEmojis, () => {
-	defaultStore.set('pinnedEmojis', pinnedEmojis.value);
+	store.set('pinnedEmojis', pinnedEmojis.value);
 }, {
 	deep: true,
 });
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 6b67a9a1a8..949f9019d9 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -155,11 +155,11 @@ import { selectFile } from '@/scripts/select-file.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { $i } from '@/account.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 const excludeMutingUsers = ref(false);
 const excludeInactiveUsers = ref(false);
-const withReplies = ref(defaultStore.state.defaultWithReplies);
+const withReplies = ref(store.state.defaultWithReplies);
 
 const onExportSuccess = () => {
 	os.alert({
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 458605d545..6203e7f698 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div v-if="!narrow || currentPage?.route.name == null" class="nav">
 					<div class="baaadecd">
 						<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+						<MkInfo v-if="!store.reactiveState.enablePreferencesAutoCloudBackup.value && store.reactiveState.showPreferencesAutoCloudBackupSuggestion.value" class="info">
+							<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
+							<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
+						</MkInfo>
 						<MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="SETTING_INDEX"></MkSuperMenu>
 					</div>
 				</div>
@@ -41,6 +45,8 @@ import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata }
 import * as os from '@/os.js';
 import { useRouter } from '@/router/supplier.js';
 import { searchIndexes } from '@/scripts/autogen/settings-search-index.js';
+import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
+import { store } from '@/store.js';
 
 const SETTING_INDEX = searchIndexes; // TODO: lazy load
 
@@ -65,6 +71,10 @@ const ro = new ResizeObserver((entries, observer) => {
 	narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
 });
 
+function skipAutoBackup() {
+	store.set('showPreferencesAutoCloudBackupSuggestion', false);
+}
+
 const menuDef = computed<SuperMenuDef[]>(() => [{
 	items: [{
 		icon: 'ti ti-user',
@@ -168,10 +178,12 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
 	}],
 }, {
 	items: [{
-		icon: 'ti ti-device-floppy',
-		text: i18n.ts.preferencesBackups,
-		to: '/settings/preferences-backups',
-		active: currentPage.value?.route.name === 'preferences-backups',
+		type: 'button',
+		icon: 'ti ti-settings-2',
+		text: i18n.ts.preferencesProfile,
+		action: async (ev: MouseEvent) => {
+			os.popupMenu(getPreferencesProfileMenu(), ev.currentTarget ?? ev.target);
+		},
 	}, {
 		type: 'button',
 		icon: 'ti ti-trash',
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 4aac2a25bd..3620c05ca8 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -186,8 +186,8 @@ import { signinRequired } from '@/account.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { defaultStore } from '@/store';
 import { reloadAsk } from '@/scripts/reload-ask.js';
+import { prefer } from '@/preferences.js';
 
 const $i = signinRequired();
 
@@ -210,7 +210,7 @@ const expandedRenoteMuteItems = ref([]);
 const expandedMuteItems = ref([]);
 const expandedBlockItems = ref([]);
 
-const showSoftWordMutedWord = computed(defaultStore.makeGetterSetter('showSoftWordMutedWord'));
+const showSoftWordMutedWord = prefer.model('showSoftWordMutedWord');
 
 watch([
 	showSoftWordMutedWord,
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index c38cdc4fc2..5e1dedd709 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -53,22 +53,24 @@ import FormSlot from '@/components/form/slot.vue';
 import MkContainer from '@/components/MkContainer.vue';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { reloadAsk } from '@/scripts/reload-ask.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { prefer } from '@/preferences.js';
+import { PREF_DEF } from '@/preferences/def.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
-const items = ref(defaultStore.state.menu.map(x => ({
+const items = ref(prefer.s.menu.map(x => ({
 	id: Math.random().toString(),
 	type: x,
 })));
 
-const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
+const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 
 async function addItem() {
-	const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
+	const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k));
 	const { canceled, result: item } = await os.select({
 		title: i18n.ts.addItem,
 		items: [...menu.map(k => ({
@@ -89,12 +91,12 @@ function removeItem(index: number) {
 }
 
 async function save() {
-	defaultStore.set('menu', items.value.map(x => x.type));
+	prefer.set('menu', items.value.map(x => x.type));
 	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 }
 
 function reset() {
-	items.value = defaultStore.def.menu.default.map(x => ({
+	items.value = PREF_DEF.menu.default.map(x => ({
 		id: Math.random().toString(),
 		type: x,
 	}));
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 9742c548e7..21133d72e7 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -117,20 +117,21 @@ 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 { defaultStore } from '@/store.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 FormSection from '@/components/form/section.vue';
+import { prefer } from '@/preferences.js';
 
 const $i = signinRequired();
 
-const reportError = computed(defaultStore.makeGetterSetter('reportError'));
-const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine'));
-const skipNoteRender = computed(defaultStore.makeGetterSetter('skipNoteRender'));
-const devMode = computed(defaultStore.makeGetterSetter('devMode'));
-const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
+const reportError = prefer.model('reportError');
+const enableCondensedLine = prefer.model('enableCondensedLine');
+const skipNoteRender = prefer.model('skipNoteRender');
+const devMode = prefer.model('devMode');
+const defaultWithReplies = computed(store.makeGetterSetter('defaultWithReplies'));
 
 watch(skipNoteRender, async () => {
 	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue
index 3ab26e80d9..c79aec91c0 100644
--- a/packages/frontend/src/pages/settings/plugin.install.vue
+++ b/packages/frontend/src/pages/settings/plugin.install.vue
@@ -23,10 +23,10 @@ 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 { installPlugin } from '@/scripts/install-plugin.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/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 3c3dcfe41e..fe57812a1d 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -4,72 +4,92 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div class="_gaps_m">
-	<FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
-
-	<FormSection>
-		<template #label>{{ i18n.ts.manage }}</template>
-		<div class="_gaps_s">
-			<div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_m" style="padding: 20px;">
-				<div class="_gaps_s">
-					<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
-					<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
-				</div>
-
-				<div class="_gaps_s">
-					<MkKeyValue>
-						<template #key>{{ i18n.ts.author }}</template>
-						<template #value>{{ plugin.author }}</template>
-					</MkKeyValue>
-					<MkKeyValue>
-						<template #key>{{ i18n.ts.description }}</template>
-						<template #value>{{ plugin.description }}</template>
-					</MkKeyValue>
-					<MkKeyValue>
-						<template #key>{{ i18n.ts.permission }}</template>
-						<template #value>
-							<ul style="margin-top: 0; margin-bottom: 0;">
-								<li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
-								<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
-							</ul>
-						</template>
-					</MkKeyValue>
-				</div>
-
-				<div class="_buttons">
-					<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
-					<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
-				</div>
-
-				<MkFolder>
-					<template #icon><i class="ti ti-terminal-2"></i></template>
-					<template #label>{{ i18n.ts._plugin.viewLog }}</template>
-
-					<div class="_gaps_s">
+<SearchMarker path="/settings/plugin" :label="i18n.ts.plugins" :keywords="['plugin']" icon="ti ti-plug">
+	<div class="_gaps_m">
+		<FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
+
+		<FormSection>
+			<template #label>{{ i18n.ts.manage }}</template>
+			<div class="_gaps_s">
+				<MkFolder v-for="plugin in plugins" :key="plugin.installId">
+					<template #icon><i class="ti ti-plug"></i></template>
+					<template #suffix>
+						<i v-if="plugin.active" class="ti ti-player-play" style="color: var(--MI_THEME-accent);"></i>
+						<i v-else class="ti ti-player-pause" style="opacity: 0.7;"></i>
+					</template>
+					<template #label>
+						<div :style="plugin.active ? '' : 'opacity: 0.7;'">
+							{{ plugin.name }}
+							<span style="margin-left: 1em; opacity: 0.7;">v{{ plugin.version }}</span>
+						</div>
+					</template>
+					<template #caption>
+						{{ plugin.description }}
+					</template>
+					<template #footer>
 						<div class="_buttons">
-							<MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+							<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
+							<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
 						</div>
+					</template>
 
-						<MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/>
-					</div>
-				</MkFolder>
-
-				<MkFolder>
-					<template #icon><i class="ti ti-code"></i></template>
-					<template #label>{{ i18n.ts._plugin.viewSource }}</template>
+					<div class="_gaps_m">
+						<div class="_gaps_s">
+							<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
+							<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
+						</div>
 
-					<div class="_gaps_s">
-						<div class="_buttons">
-							<MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+						<div class="_gaps_s">
+							<MkKeyValue>
+								<template #key>{{ i18n.ts.author }}</template>
+								<template #value>{{ plugin.author }}</template>
+							</MkKeyValue>
+							<MkKeyValue>
+								<template #key>{{ i18n.ts.description }}</template>
+								<template #value>{{ plugin.description }}</template>
+							</MkKeyValue>
+							<MkKeyValue>
+								<template #key>{{ i18n.ts.permission }}</template>
+								<template #value>
+									<ul style="margin-top: 0; margin-bottom: 0;">
+										<li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
+										<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
+									</ul>
+								</template>
+							</MkKeyValue>
 						</div>
 
-						<MkCode :code="plugin.src ?? ''" lang="is"/>
+						<MkFolder>
+							<template #icon><i class="ti ti-terminal-2"></i></template>
+							<template #label>{{ i18n.ts._plugin.viewLog }}</template>
+
+							<div class="_gaps_s">
+								<div class="_buttons">
+									<MkButton inline @click="copy(pluginLogs.get(plugin.installId)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+								</div>
+
+								<MkCode :code="pluginLogs.get(plugin.installId)?.join('\n') ?? ''"/>
+							</div>
+						</MkFolder>
+
+						<MkFolder>
+							<template #icon><i class="ti ti-code"></i></template>
+							<template #label>{{ i18n.ts._plugin.viewSource }}</template>
+
+							<div class="_gaps_s">
+								<div class="_buttons">
+									<MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+								</div>
+
+								<MkCode :code="plugin.src ?? ''" lang="ais"/>
+							</div>
+						</MkFolder>
 					</div>
 				</MkFolder>
 			</div>
-		</div>
-	</FormSection>
-</div>
+		</FormSection>
+	</div>
+</SearchMarker>
 </template>
 
 <script lang="ts" setup>
@@ -83,19 +103,16 @@ 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 { ColdDeviceStorage } from '@/store.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { pluginLogs } from '@/plugin.js';
+import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin } from '@/plugin.js';
+import { prefer } from '@/preferences.js';
 
-const plugins = ref(ColdDeviceStorage.get('plugins'));
+const plugins = prefer.r.plugins;
 
 async function uninstall(plugin) {
-	ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id));
-	await os.apiWithDialog('i/revoke-token', {
-		token: plugin.token,
-	});
+	await uninstallPlugin(plugin);
 	nextTick(() => {
 		unisonReload();
 	});
@@ -106,30 +123,15 @@ function copy(text) {
 	os.success();
 }
 
-// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
 async function config(plugin) {
-	const config = plugin.config;
-	for (const key in plugin.configData) {
-		config[key].default = plugin.configData[key];
-	}
-
-	const { canceled, result } = await os.form(plugin.name, config);
-	if (canceled) return;
-
-	const coldPlugins = ColdDeviceStorage.get('plugins');
-	coldPlugins.find(p => p.id === plugin.id)!.configData = result;
-	ColdDeviceStorage.set('plugins', coldPlugins);
-
+	await configPlugin(plugin);
 	nextTick(() => {
 		location.reload();
 	});
 }
 
 function changeActive(plugin, active) {
-	const coldPlugins = ColdDeviceStorage.get('plugins');
-	coldPlugins.find(p => p.id === plugin.id)!.active = active;
-	ColdDeviceStorage.set('plugins', coldPlugins);
-
+	changePluginActive(plugin, active);
 	nextTick(() => {
 		location.reload();
 	});
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
deleted file mode 100644
index 7388e014ed..0000000000
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ /dev/null
@@ -1,465 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div class="_gaps_m">
-	<div :class="$style.buttons">
-		<MkButton inline primary @click="saveNew">{{ i18n.ts._preferencesBackups.saveNew }}</MkButton>
-		<MkButton inline @click="loadFile">{{ i18n.ts._preferencesBackups.loadFile }}</MkButton>
-	</div>
-
-	<FormSection>
-		<template #label>{{ i18n.ts._preferencesBackups.list }}</template>
-		<template v-if="profiles && Object.keys(profiles).length > 0">
-			<div class="_gaps_s">
-				<div
-					v-for="(profile, id) in profiles"
-					:key="id"
-					class="_panel"
-					:class="$style.profile"
-					@click="$event => menu($event, id)"
-					@contextmenu.prevent.stop="$event => menu($event, id)"
-				>
-					<div :class="$style.profileName">{{ profile.name }}</div>
-					<div :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.createdAt({ date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
-					<div v-if="profile.updatedAt" :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.updatedAt({ date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
-				</div>
-			</div>
-		</template>
-		<div v-else-if="profiles">
-			<MkInfo>{{ i18n.ts._preferencesBackups.noBackups }}</MkInfo>
-		</div>
-		<MkLoading v-else/>
-	</FormSection>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import { v4 as uuid } from 'uuid';
-import { version, host } from '@@/js/config.js';
-import FormSection from '@/components/form/section.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { ColdDeviceStorage, defaultStore } from '@/store.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
-import { useStream } from '@/stream.js';
-import { $i } from '@/account.js';
-import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { miLocalStorage } from '@/local-storage.js';
-
-const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
-	'collapseRenotes',
-	'menu',
-	'visibility',
-	'localOnly',
-	'statusbars',
-	'widgets',
-	'tl',
-	'pinnedUserLists',
-	'overridedDeviceKind',
-	'serverDisconnectedBehavior',
-	'nsfw',
-	'highlightSensitiveMedia',
-	'animation',
-	'animatedMfm',
-	'advancedMfm',
-	'showReactionsCount',
-	'loadRawImages',
-	'imageNewTab',
-	'dataSaver',
-	'disableShowingAnimatedImages',
-	'emojiStyle',
-	'menuStyle',
-	'useBlurEffectForModal',
-	'useBlurEffect',
-	'showFixedPostForm',
-	'showFixedPostFormInChannel',
-	'enableInfiniteScroll',
-	'useReactionPickerForContextMenu',
-	'showGapBetweenNotesInTimeline',
-	'instanceTicker',
-	'emojiPickerScale',
-	'emojiPickerWidth',
-	'emojiPickerHeight',
-	'emojiPickerStyle',
-	'defaultSideView',
-	'menuDisplay',
-	'reportError',
-	'squareAvatars',
-	'showAvatarDecorations',
-	'numberOfPageCache',
-	'showNoteActionsOnlyHover',
-	'showClipButtonInNoteFooter',
-	'reactionsDisplaySize',
-	'forceShowAds',
-	'aiChanMode',
-	'devMode',
-	'mediaListWithOneImageAppearance',
-	'notificationPosition',
-	'notificationStackAxis',
-	'keepScreenOn',
-	'defaultWithReplies',
-	'disableStreamingTimeline',
-	'useGroupedNotifications',
-	'sound_masterVolume',
-	'sound_note',
-	'sound_noteMy',
-	'sound_notification',
-];
-const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
-	'lightTheme',
-	'darkTheme',
-	'syncDeviceDarkMode',
-	'plugins',
-];
-
-const scope = ['clientPreferencesProfiles'];
-
-const profileProps = ['name', 'createdAt', 'updatedAt', 'misskeyVersion', 'settings', 'host'];
-
-type Profile = {
-	name: string;
-	createdAt: string;
-	updatedAt: string | null;
-	misskeyVersion: string;
-	host: string;
-	settings: {
-		hot: Record<keyof typeof defaultStoreSaveKeys, unknown>;
-		cold: Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
-		fontSize: string | null;
-		useSystemFont: 't' | null;
-		wallpaper: string | null;
-	};
-};
-
-const connection = $i && useStream().useChannel('main');
-
-const profiles = ref<Record<string, Profile> | null>(null);
-
-misskeyApi('i/registry/get-all', { scope })
-	.then(res => {
-		profiles.value = res || {};
-	});
-
-function isObject(value: unknown): value is Record<string, unknown> {
-	return value != null && typeof value === 'object' && !Array.isArray(value);
-}
-
-function validate(profile: any): void {
-	if (!isObject(profile)) throw new Error('not an object');
-
-	// Check if unnecessary properties exist
-	if (Object.keys(profile).some(key => !profileProps.includes(key))) throw new Error('Unnecessary properties exist');
-
-	if (!profile.name) throw new Error('Missing required prop: name');
-	if (!profile.misskeyVersion) throw new Error('Missing required prop: misskeyVersion');
-
-	// Check if createdAt and updatedAt is Date
-	// https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date
-	if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt as any).getTime())) throw new Error('createdAt is falsy or not Date');
-	if (profile.updatedAt) {
-		if (Number.isNaN(new Date(profile.updatedAt as any).getTime())) {
-			throw new Error('updatedAt is not Date');
-		}
-	} else if (profile.updatedAt !== null) {
-		throw new Error('updatedAt is not null');
-	}
-
-	if (!profile.settings) throw new Error('Missing required prop: settings');
-	if (!isObject(profile.settings)) throw new Error('Invalid prop: settings');
-}
-
-function getSettings(): Profile['settings'] {
-	const hot = {} as Record<keyof typeof defaultStoreSaveKeys, unknown>;
-	for (const key of defaultStoreSaveKeys) {
-		hot[key] = defaultStore.state[key];
-	}
-
-	const cold = {} as Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
-	for (const key of coldDeviceStorageSaveKeys) {
-		cold[key] = ColdDeviceStorage.get(key);
-	}
-
-	return {
-		hot,
-		cold,
-		fontSize: miLocalStorage.getItem('fontSize'),
-		useSystemFont: miLocalStorage.getItem('useSystemFont') as 't' | null,
-		wallpaper: miLocalStorage.getItem('wallpaper'),
-	};
-}
-
-async function saveNew(): Promise<void> {
-	if (!profiles.value) return;
-
-	const { canceled, result: name } = await os.inputText({
-		title: i18n.ts._preferencesBackups.inputName,
-		default: '',
-	});
-	if (canceled) return;
-
-	if (Object.values(profiles.value).some(x => x.name === name)) {
-		return os.alert({
-			title: i18n.ts._preferencesBackups.cannotSave,
-			text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }),
-		});
-	}
-
-	const id = uuid();
-	const profile: Profile = {
-		name,
-		createdAt: (new Date()).toISOString(),
-		updatedAt: null,
-		misskeyVersion: version,
-		host,
-		settings: getSettings(),
-	};
-	await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
-}
-
-function loadFile(): void {
-	const input = document.createElement('input');
-	input.type = 'file';
-	input.multiple = false;
-	input.onchange = async () => {
-		if (!profiles.value) return;
-		if (!input.files || input.files.length === 0) return;
-
-		const file = input.files[0];
-
-		if (file.type !== 'application/json') {
-			return os.alert({
-				type: 'error',
-				title: i18n.ts._preferencesBackups.cannotLoad,
-				text: i18n.ts._preferencesBackups.invalidFile,
-			});
-		}
-
-		let profile: Profile;
-		try {
-			profile = JSON.parse(await file.text()) as unknown as Profile;
-			validate(profile);
-		} catch (err) {
-			return os.alert({
-				type: 'error',
-				title: i18n.ts._preferencesBackups.cannotLoad,
-				text: (err as any)?.message ?? '',
-			});
-		}
-
-		const id = uuid();
-		await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
-
-		// 一応廃棄
-		(window as any).__misskey_input_ref__ = null;
-	};
-
-	// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
-	// iOS Safari で正常に動かす為のおまじない
-	(window as any).__misskey_input_ref__ = input;
-
-	input.click();
-}
-
-async function applyProfile(id: string): Promise<void> {
-	if (!profiles.value) return;
-
-	const profile = profiles.value[id];
-
-	const { canceled: cancel1 } = await os.confirm({
-		type: 'warning',
-		title: i18n.ts._preferencesBackups.apply,
-		text: i18n.tsx._preferencesBackups.applyConfirm({ name: profile.name }),
-	});
-	if (cancel1) return;
-
-	// TODO: バージョン or ホストが違ったらさらに警告を表示
-
-	const settings = profile.settings;
-
-	// defaultStore
-	for (const key of defaultStoreSaveKeys) {
-		if (settings.hot[key] !== undefined) {
-			defaultStore.set(key, settings.hot[key]);
-		}
-	}
-
-	// coldDeviceStorage
-	for (const key of coldDeviceStorageSaveKeys) {
-		if (settings.cold[key] !== undefined) {
-			ColdDeviceStorage.set(key, settings.cold[key]);
-		}
-	}
-
-	// fontSize
-	if (settings.fontSize) {
-		miLocalStorage.setItem('fontSize', settings.fontSize);
-	} else {
-		miLocalStorage.removeItem('fontSize');
-	}
-
-	// useSystemFont
-	if (settings.useSystemFont) {
-		miLocalStorage.setItem('useSystemFont', settings.useSystemFont);
-	} else {
-		miLocalStorage.removeItem('useSystemFont');
-	}
-
-	// wallpaper
-	if (settings.wallpaper != null) {
-		miLocalStorage.setItem('wallpaper', settings.wallpaper);
-	} else {
-		miLocalStorage.removeItem('wallpaper');
-	}
-
-	const { canceled: cancel2 } = await os.confirm({
-		type: 'info',
-		text: i18n.ts.reloadToApplySetting,
-	});
-	if (cancel2) return;
-
-	unisonReload();
-}
-
-async function deleteProfile(id: string): Promise<void> {
-	if (!profiles.value) return;
-
-	const { canceled } = await os.confirm({
-		type: 'info',
-		title: i18n.ts.delete,
-		text: i18n.tsx.deleteAreYouSure({ x: profiles.value[id].name }),
-	});
-	if (canceled) return;
-
-	await os.apiWithDialog('i/registry/remove', { scope, key: id });
-	delete profiles.value[id];
-}
-
-async function save(id: string): Promise<void> {
-	if (!profiles.value) return;
-
-	const { name, createdAt } = profiles.value[id];
-
-	const { canceled } = await os.confirm({
-		type: 'info',
-		title: i18n.ts._preferencesBackups.save,
-		text: i18n.tsx._preferencesBackups.saveConfirm({ name }),
-	});
-	if (canceled) return;
-
-	const profile: Profile = {
-		name,
-		createdAt,
-		updatedAt: (new Date()).toISOString(),
-		misskeyVersion: version,
-		host,
-		settings: getSettings(),
-	};
-	await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
-}
-
-async function rename(id: string): Promise<void> {
-	if (!profiles.value) return;
-
-	const { canceled: cancel1, result: name } = await os.inputText({
-		title: i18n.ts._preferencesBackups.inputName,
-		default: '',
-	});
-	if (cancel1 || profiles.value[id].name === name) return;
-
-	if (Object.values(profiles.value).some(x => x.name === name)) {
-		return os.alert({
-			title: i18n.ts._preferencesBackups.cannotSave,
-			text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }),
-		});
-	}
-
-	const registry = Object.assign({}, { ...profiles.value[id] });
-
-	const { canceled: cancel2 } = await os.confirm({
-		type: 'info',
-		title: i18n.ts.rename,
-		text: i18n.tsx._preferencesBackups.renameConfirm({ old: registry.name, new: name }),
-	});
-	if (cancel2) return;
-
-	registry.name = name;
-	await os.apiWithDialog('i/registry/set', { scope, key: id, value: registry });
-}
-
-function menu(ev: MouseEvent, profileId: string) {
-	if (!profiles.value) return;
-
-	return os.popupMenu([{
-		text: i18n.ts._preferencesBackups.apply,
-		icon: 'ti ti-check',
-		action: () => applyProfile(profileId),
-	}, {
-		type: 'a',
-		text: i18n.ts.download,
-		icon: 'ti ti-download',
-		href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })),
-		download: `${profiles.value[profileId].name}.json`,
-	}, { type: 'divider' }, {
-		text: i18n.ts.rename,
-		icon: 'ti ti-forms',
-		action: () => rename(profileId),
-	}, {
-		text: i18n.ts._preferencesBackups.save,
-		icon: 'ti ti-device-floppy',
-		action: () => save(profileId),
-	}, { type: 'divider' }, {
-		text: i18n.ts.delete,
-		icon: 'ti ti-trash',
-		action: () => deleteProfile(profileId),
-		danger: true,
-	}], (ev.currentTarget ?? ev.target ?? undefined) as unknown as HTMLElement | undefined);
-}
-
-onMounted(() => {
-	// streamingのuser storage updateイベントを監視して更新
-	connection?.on('registryUpdated', ({ scope: recievedScope, key, value }) => {
-		if (!recievedScope || recievedScope.length !== scope.length || recievedScope[0] !== scope[0]) return;
-		if (!profiles.value) return;
-
-		profiles.value[key] = value;
-	});
-});
-
-onUnmounted(() => {
-	connection?.off('registryUpdated');
-});
-
-definePageMetadata(() => ({
-	title: i18n.ts.preferencesBackups,
-	icon: 'ti ti-device-floppy',
-}));
-</script>
-
-<style lang="scss" module>
-.buttons {
-	display: flex;
-	gap: var(--MI-margin);
-	flex-wrap: wrap;
-}
-
-.profile {
-	padding: 20px;
-	cursor: pointer;
-
-	&Name {
-		font-weight: 700;
-	}
-
-	&Time {
-		font-size: .85em;
-		opacity: .7;
-	}
-}
-</style>
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index fe718bfa69..2df621eaa6 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -33,30 +33,36 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<FormSection>
 			<div class="_gaps_s">
 				<SearchMarker :keywords="['post', 'form', 'timeline']">
-					<MkSwitch v-model="showFixedPostForm">
-						<template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="showFixedPostForm">
+						<MkSwitch v-model="showFixedPostForm">
+							<template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['post', 'form', 'timeline', 'channel']">
-					<MkSwitch v-model="showFixedPostFormInChannel">
-						<template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="showFixedPostFormInChannel">
+						<MkSwitch v-model="showFixedPostFormInChannel">
+							<template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['pinned', 'list']">
 					<MkFolder>
 						<template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template>
 						<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
-						<MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
+						<MkButton v-if="prefer.r.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
 						<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
 					</MkFolder>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn']">
-					<MkSwitch v-model="enableQuickAddMfmFunction">
-						<template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="enableQuickAddMfmFunction">
+						<MkSwitch v-model="enableQuickAddMfmFunction">
+							<template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 			</div>
 		</FormSection>
@@ -68,40 +74,52 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="_gaps_m">
 					<div class="_gaps_s">
 						<SearchMarker :keywords="['renote']">
-							<MkSwitch v-model="collapseRenotes">
-								<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
-								<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="collapseRenotes">
+								<MkSwitch v-model="collapseRenotes">
+									<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
+									<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['hover', 'show', 'footer', 'action']">
-							<MkSwitch v-model="showNoteActionsOnlyHover">
-								<template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="showNoteActionsOnlyHover">
+								<MkSwitch v-model="showNoteActionsOnlyHover">
+									<template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['footer', 'action', 'clip', 'show']">
-							<MkSwitch v-model="showClipButtonInNoteFooter">
-								<template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="showClipButtonInNoteFooter">
+								<MkSwitch v-model="showClipButtonInNoteFooter">
+									<template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced']">
-							<MkSwitch v-model="advancedMfm">
-								<template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="advancedMfm">
+								<MkSwitch v-model="advancedMfm">
+									<template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['reaction', 'count', 'show']">
-							<MkSwitch v-model="showReactionsCount">
-								<template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="showReactionsCount">
+								<MkSwitch v-model="showReactionsCount">
+									<template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment']">
-							<MkSwitch v-model="loadRawImages">
-								<template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="loadRawImages">
+								<MkSwitch v-model="loadRawImages">
+									<template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 					</div>
 				</div>
@@ -114,9 +132,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps_m">
 					<SearchMarker :keywords="['group']">
-						<MkSwitch v-model="useGroupedNotifications">
-							<template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="useGroupedNotifications">
+							<MkSwitch v-model="useGroupedNotifications">
+								<template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 				</div>
 			</FormSection>
@@ -129,62 +149,88 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="_gaps_m">
 					<div class="_gaps_s">
 						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab']">
-							<MkSwitch v-model="imageNewTab">
-								<template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="imageNewTab">
+								<MkSwitch v-model="imageNewTab">
+									<template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['reaction', 'picker', 'contextmenu', 'open']">
-							<MkSwitch v-model="useReactionPickerForContextMenu">
-								<template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="useReactionPickerForContextMenu">
+								<MkSwitch v-model="useReactionPickerForContextMenu">
+									<template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['load', 'auto', 'more']">
-							<MkSwitch v-model="enableInfiniteScroll">
-								<template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="enableInfiniteScroll">
+								<MkSwitch v-model="enableInfiniteScroll">
+									<template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['disable', 'streaming', 'timeline']">
-							<MkSwitch v-model="disableStreamingTimeline">
-								<template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="disableStreamingTimeline">
+								<MkSwitch v-model="disableStreamingTimeline">
+									<template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['follow', 'confirm', 'always']">
-							<MkSwitch v-model="alwaysConfirmFollow">
-								<template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="alwaysConfirmFollow">
+								<MkSwitch v-model="alwaysConfirmFollow">
+									<template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm']">
-							<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
-								<template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="confirmWhenRevealingSensitiveMedia">
+								<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
+									<template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['reaction', 'confirm']">
-							<MkSwitch v-model="confirmOnReact">
-								<template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="confirmOnReact">
+								<MkSwitch v-model="confirmOnReact">
+									<template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['remember', 'keep', 'note', 'cw']">
+							<MkPreferenceContainer k="keepCw">
+								<MkSwitch v-model="keepCw">
+									<template #label><SearchLabel>{{ i18n.ts.keepCw }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 					</div>
 
 					<SearchMarker :keywords="['server', 'disconnect', 'reconnect', 'reload', 'streaming']">
-						<MkSelect v-model="serverDisconnectedBehavior">
-							<template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template>
-							<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
-							<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
-							<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
-						</MkSelect>
+						<MkPreferenceContainer k="serverDisconnectedBehavior">
+							<MkSelect v-model="serverDisconnectedBehavior">
+								<template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template>
+								<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
+								<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
+								<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
+							</MkSelect>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['cache', 'page']">
-						<MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing>
-							<template #label><SearchLabel>{{ i18n.ts.numberOfPageCache }}</SearchLabel></template>
-							<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
-						</MkRange>
+						<MkPreferenceContainer k="numberOfPageCache">
+							<MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing>
+								<template #label><SearchLabel>{{ i18n.ts.numberOfPageCache }}</SearchLabel></template>
+								<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
+							</MkRange>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :label="i18n.ts.dataSaver" :keywords="['datasaver']">
@@ -229,18 +275,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps">
 					<SearchMarker :keywords="['ad', 'show']">
-						<MkSwitch v-model="forceShowAds">
-							<template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="forceShowAds">
+							<MkSwitch v-model="forceShowAds">
+								<template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker>
-						<MkRadios v-model="hemisphere">
-							<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template>
-							<option value="N">{{ i18n.ts._hemisphere.N }}</option>
-							<option value="S">{{ i18n.ts._hemisphere.S }}</option>
-							<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
-						</MkRadios>
+						<MkPreferenceContainer k="hemisphere">
+							<MkRadios v-model="hemisphere">
+								<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template>
+								<option value="N">{{ i18n.ts._hemisphere.N }}</option>
+								<option value="S">{{ i18n.ts._hemisphere.S }}</option>
+								<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
+							</MkRadios>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['emoji', 'dictionary', 'additional', 'extra']">
@@ -248,8 +298,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<template #label><SearchLabel>{{ i18n.ts.additionalEmojiDictionary }}</SearchLabel></template>
 							<div class="_buttons">
 								<template v-for="lang in emojiIndexLangs" :key="lang">
-									<MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
-									<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
+									<MkButton v-if="store.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
+									<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ store.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
 								</template>
 							</div>
 						</MkFolder>
@@ -272,7 +322,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
-import * as Misskey from 'misskey-js';
 import { langs } from '@@/js/config.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
@@ -284,41 +333,44 @@ import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
 import MkLink from '@/components/MkLink.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { instance } from '@/instance.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { reloadAsk } from '@/scripts/reload-ask.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
+import { prefer } from '@/preferences.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
 const lang = ref(miLocalStorage.getItem('lang'));
-const dataSaver = ref(defaultStore.state.dataSaver);
-
-const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere'));
-const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
-const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
-const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
-const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
-const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
-const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
-const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
-const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
-const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
-const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
-const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
-const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
-const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel'));
-const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
-const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
-const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
-const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
-const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
-const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
-const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
-const confirmOnReact = computed(defaultStore.makeGetterSetter('confirmOnReact'));
-const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
+const dataSaver = ref(prefer.s.dataSaver);
+
+const overridedDeviceKind = computed(store.makeGetterSetter('overridedDeviceKind'));
+
+const keepCw = prefer.model('keepCw');
+const serverDisconnectedBehavior = prefer.model('serverDisconnectedBehavior');
+const hemisphere = prefer.model('hemisphere');
+const showNoteActionsOnlyHover = prefer.model('showNoteActionsOnlyHover');
+const showClipButtonInNoteFooter = prefer.model('showClipButtonInNoteFooter');
+const collapseRenotes = prefer.model('collapseRenotes');
+const advancedMfm = prefer.model('advancedMfm');
+const showReactionsCount = prefer.model('showReactionsCount');
+const enableQuickAddMfmFunction = prefer.model('enableQuickAddMfmFunction');
+const forceShowAds = prefer.model('forceShowAds');
+const loadRawImages = prefer.model('loadRawImages');
+const imageNewTab = prefer.model('imageNewTab');
+const showFixedPostForm = prefer.model('showFixedPostForm');
+const showFixedPostFormInChannel = prefer.model('showFixedPostFormInChannel');
+const numberOfPageCache = prefer.model('numberOfPageCache');
+const enableInfiniteScroll = prefer.model('enableInfiniteScroll');
+const useReactionPickerForContextMenu = prefer.model('useReactionPickerForContextMenu');
+const disableStreamingTimeline = prefer.model('disableStreamingTimeline');
+const useGroupedNotifications = prefer.model('useGroupedNotifications');
+const alwaysConfirmFollow = prefer.model('alwaysConfirmFollow');
+const confirmWhenRevealingSensitiveMedia = prefer.model('confirmWhenRevealingSensitiveMedia');
+const confirmOnReact = prefer.model('confirmOnReact');
+const contextMenu = prefer.model('contextMenu');
 
 watch(lang, () => {
 	miLocalStorage.setItem('lang', lang.value as string);
@@ -356,7 +408,7 @@ function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) {
 
 function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 	async function main() {
-		const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
+		const currentIndexes = store.state.additionalUnicodeEmojiIndexes;
 
 		function download() {
 			switch (lang) {
@@ -368,7 +420,7 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 		}
 
 		currentIndexes[lang] = await download();
-		await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
+		await store.set('additionalUnicodeEmojiIndexes', currentIndexes);
 	}
 
 	os.promiseDialog(main());
@@ -376,9 +428,9 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 
 function removeEmojiIndex(lang: string) {
 	async function main() {
-		const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
+		const currentIndexes = store.state.additionalUnicodeEmojiIndexes;
 		delete currentIndexes[lang];
-		await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
+		await store.set('additionalUnicodeEmojiIndexes', currentIndexes);
 	}
 
 	os.promiseDialog(main());
@@ -393,16 +445,17 @@ async function setPinnedList() {
 		})),
 	});
 	if (canceled) return;
+	if (list == null) return;
 
-	defaultStore.set('pinnedUserLists', [list]);
+	prefer.set('pinnedUserLists', [list]);
 }
 
 function removePinnedList() {
-	defaultStore.set('pinnedUserLists', []);
+	prefer.set('pinnedUserLists', []);
 }
 
 function enableAllDataSaver() {
-	const g = { ...defaultStore.state.dataSaver };
+	const g = { ...prefer.s.dataSaver };
 
 	Object.keys(g).forEach((key) => { g[key] = true; });
 
@@ -410,7 +463,7 @@ function enableAllDataSaver() {
 }
 
 function disableAllDataSaver() {
-	const g = { ...defaultStore.state.dataSaver };
+	const g = { ...prefer.s.dataSaver };
 
 	Object.keys(g).forEach((key) => { g[key] = false; });
 
@@ -418,7 +471,7 @@ function disableAllDataSaver() {
 }
 
 watch(dataSaver, (to) => {
-	defaultStore.set('dataSaver', to);
+	prefer.set('dataSaver', to);
 }, {
 	deep: true,
 });
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index cd0d54a73b..792b4147da 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -172,38 +172,41 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<FormSection>
 			<div class="_gaps_m">
 				<SearchMarker :keywords="['remember', 'keep', 'note', 'visibility']">
-					<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">
-						<template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="rememberNoteVisibility">
+						<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">
+							<template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['default', 'note', 'visibility']">
-					<MkFolder v-if="!rememberNoteVisibility">
-						<template #label><SearchLabel>{{ i18n.ts.defaultNoteVisibility }}</SearchLabel></template>
-						<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
-						<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
-						<template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
-						<template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
-
-						<div class="_gaps_m">
-							<MkSelect v-model="defaultNoteVisibility">
-								<option value="public">{{ i18n.ts._visibility.public }}</option>
-								<option value="home">{{ i18n.ts._visibility.home }}</option>
-								<option value="followers">{{ i18n.ts._visibility.followers }}</option>
-								<option value="specified">{{ i18n.ts._visibility.specified }}</option>
-							</MkSelect>
-							<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
-						</div>
-					</MkFolder>
+					<MkDisableSection :disabled="rememberNoteVisibility">
+						<MkFolder>
+							<template #label><SearchLabel>{{ i18n.ts.defaultNoteVisibility }}</SearchLabel></template>
+							<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
+							<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
+							<template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
+							<template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
+
+							<div class="_gaps_m">
+								<MkPreferenceContainer k="defaultNoteVisibility">
+									<MkSelect v-model="defaultNoteVisibility">
+										<option value="public">{{ i18n.ts._visibility.public }}</option>
+										<option value="home">{{ i18n.ts._visibility.home }}</option>
+										<option value="followers">{{ i18n.ts._visibility.followers }}</option>
+										<option value="specified">{{ i18n.ts._visibility.specified }}</option>
+									</MkSelect>
+								</MkPreferenceContainer>
+
+								<MkPreferenceContainer k="defaultNoteLocalOnly">
+									<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
+								</MkPreferenceContainer>
+							</div>
+						</MkFolder>
+					</MkDisableSection>
 				</SearchMarker>
 			</div>
 		</FormSection>
-
-		<SearchMarker :keywords="['remember', 'keep', 'note', 'cw']">
-			<MkSwitch v-model="keepCw" @update:modelValue="save()">
-				<template #label><SearchLabel>{{ i18n.ts.keepCw }}</SearchLabel></template>
-			</MkSwitch>
-		</SearchMarker>
 	</div>
 </SearchMarker>
 </template>
@@ -215,7 +218,6 @@ 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 { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { signinRequired } from '@/account.js';
@@ -225,6 +227,8 @@ import { formatDateTimeString } from '@/scripts/format-time-string.js';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
 import MkDisableSection from '@/components/MkDisableSection.vue';
+import { prefer } from '@/preferences.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
 const $i = signinRequired();
 
@@ -241,10 +245,9 @@ const publicReactions = ref($i.publicReactions);
 const followingVisibility = ref($i.followingVisibility);
 const followersVisibility = ref($i.followersVisibility);
 
-const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
-const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
-const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
-const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
+const defaultNoteVisibility = prefer.model('defaultNoteVisibility');
+const defaultNoteLocalOnly = prefer.model('defaultNoteLocalOnly');
+const rememberNoteVisibility = prefer.model('rememberNoteVisibility');
 
 const makeNotesFollowersOnlyBefore_type = computed(() => {
 	if (makeNotesFollowersOnlyBefore.value == null) {
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 51148a1f72..f9ddbbc9ed 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -168,7 +168,7 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { globalEvents } from '@/events.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
@@ -177,7 +177,7 @@ const $i = signinRequired();
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
-const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
+const reactionAcceptance = computed(store.makeGetterSetter('reactionAcceptance'));
 
 function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
 	return lang != null && lang in langmap;
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 1df2d89277..808ae06f41 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -7,21 +7,27 @@ SPDX-License-Identifier: AGPL-3.0-only
 <SearchMarker path="/settings/sounds" :label="i18n.ts.sounds" :keywords="['sounds']" icon="ti ti-music">
 	<div class="_gaps_m">
 		<SearchMarker :keywords="['mute']">
-			<MkSwitch v-model="notUseSound">
-				<template #label><SearchLabel>{{ i18n.ts.notUseSound }}</SearchLabel></template>
-			</MkSwitch>
+			<MkPreferenceContainer k="sound.notUseSound">
+				<MkSwitch v-model="notUseSound">
+					<template #label><SearchLabel>{{ i18n.ts.notUseSound }}</SearchLabel></template>
+				</MkSwitch>
+			</MkPreferenceContainer>
 		</SearchMarker>
 
 		<SearchMarker :keywords="['active', 'mute']">
-			<MkSwitch v-model="useSoundOnlyWhenActive">
-				<template #label><SearchLabel>{{ i18n.ts.useSoundOnlyWhenActive }}</SearchLabel></template>
-			</MkSwitch>
+			<MkPreferenceContainer k="sound.useSoundOnlyWhenActive">
+				<MkSwitch v-model="useSoundOnlyWhenActive">
+					<template #label><SearchLabel>{{ i18n.ts.useSoundOnlyWhenActive }}</SearchLabel></template>
+				</MkSwitch>
+			</MkPreferenceContainer>
 		</SearchMarker>
 
 		<SearchMarker :keywords="['volume', 'master']">
-			<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
-				<template #label><SearchLabel>{{ i18n.ts.masterVolume }}</SearchLabel></template>
-			</MkRange>
+			<MkPreferenceContainer k="sound.masterVolume">
+				<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
+					<template #label><SearchLabel>{{ i18n.ts.masterVolume }}</SearchLabel></template>
+				</MkRange>
+			</MkPreferenceContainer>
 		</SearchMarker>
 
 		<FormSection>
@@ -52,7 +58,8 @@ 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 { SoundStore } from '@/store.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { prefer } from '@/preferences.js';
 import MkRange from '@/components/MkRange.vue';
 import MkButton from '@/components/MkButton.vue';
 import FormSection from '@/components/form/section.vue';
@@ -60,18 +67,19 @@ 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 { defaultStore } from '@/store.js';
 import MkSwitch from '@/components/MkSwitch.vue';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
+import { PREF_DEF } from '@/preferences/def.js';
 
-const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'));
-const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
-const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
+const notUseSound = prefer.model('sound.notUseSound');
+const useSoundOnlyWhenActive = prefer.model('sound.useSoundOnlyWhenActive');
+const masterVolume = prefer.model('sound.masterVolume');
 
 const sounds = ref<Record<OperationType, Ref<SoundStore>>>({
-	note: defaultStore.reactiveState.sound_note,
-	noteMy: defaultStore.reactiveState.sound_noteMy,
-	notification: defaultStore.reactiveState.sound_notification,
-	reaction: defaultStore.reactiveState.sound_reaction,
+	note: prefer.r['sound.on.note'],
+	noteMy: prefer.r['sound.on.noteMy'],
+	notification: prefer.r['sound.on.notification'],
+	reaction: prefer.r['sound.on.reaction'],
 });
 
 function getSoundTypeName(f: SoundType): string {
@@ -93,14 +101,14 @@ async function updated(type: keyof typeof sounds.value, sound) {
 		volume: sound.volume,
 	};
 
-	defaultStore.set(`sound_${type}`, v);
+	prefer.set(`sound.on.${type}`, v);
 	sounds.value[type] = v;
 }
 
 function reset() {
 	for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
-		const v = defaultStore.def[`sound_${sound}`].default;
-		defaultStore.set(`sound_${sound}`, v);
+		const v = PREF_DEF[`sound.on.${sound}`].default;
+		prefer.set(`sound.on.${sound}`, v);
 		sounds.value[sound] = v;
 	}
 }
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index 140b6beb14..ede395e51e 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -94,17 +94,17 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkRange from '@/components/MkRange.vue';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { deepClone } from '@/scripts/clone.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	_id: string;
 	userLists: Misskey.entities.UserList[] | null;
 }>();
 
-const statusbar = reactive(deepClone(defaultStore.state.statusbars.find(x => x.id === props._id)));
+const statusbar = reactive(deepClone(prefer.s.statusbars.find(x => x.id === props._id)));
 
 watch(() => statusbar.type, () => {
 	if (statusbar.type === 'rss') {
@@ -134,13 +134,13 @@ watch(() => statusbar.type, () => {
 watch(statusbar, save);
 
 async function save() {
-	const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
-	const statusbars = deepClone(defaultStore.state.statusbars);
+	const i = prefer.s.statusbars.findIndex(x => x.id === props._id);
+	const statusbars = deepClone(prefer.s.statusbars);
 	statusbars[i] = deepClone(statusbar);
-	defaultStore.set('statusbars', statusbars);
+	prefer.set('statusbars', statusbars);
 }
 
 function del() {
-	defaultStore.set('statusbars', defaultStore.state.statusbars.filter(x => x.id !== props._id));
+	prefer.set('statusbars', prefer.s.statusbars.filter(x => x.id !== props._id));
 }
 </script>
diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue
index 1ae3de7994..068d28bc4e 100644
--- a/packages/frontend/src/pages/settings/statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.vue
@@ -22,11 +22,11 @@ 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 { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { prefer } from '@/preferences.js';
 
-const statusbars = defaultStore.reactiveState.statusbars;
+const statusbars = prefer.r.statusbars;
 
 const userLists = ref<Misskey.entities.UserList[] | null>(null);
 
@@ -37,13 +37,13 @@ onMounted(() => {
 });
 
 async function add() {
-	defaultStore.push('statusbars', {
+	prefer.set('statusbars', [...statusbars.value, {
 		id: uuid(),
 		type: null,
 		black: false,
 		size: 'medium',
 		props: {},
-	});
+	}]);
 }
 
 const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue
index 4f05d3784c..b19b12aaab 100644
--- a/packages/frontend/src/pages/settings/theme.install.vue
+++ b/packages/frontend/src/pages/settings/theme.install.vue
@@ -20,7 +20,7 @@ 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/install-theme.js';
+import { parseThemeCode, previewTheme, installTheme } from '@/scripts/theme.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index b0e4ce13d5..41de2aa6a6 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -75,6 +75,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, onActivated, ref, watch } from 'vue';
 import JSON5 from 'json5';
+import defaultLightTheme from '@@/themes/l-light.json5';
+import defaultDarkTheme from '@@/themes/d-green-lime.json5';
 import type { MkSelectItem } from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
@@ -84,15 +86,15 @@ 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 { ColdDeviceStorage, defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { uniqueBy } from '@/scripts/array.js';
-import { fetchThemes, getThemes } from '@/theme-store.js';
+import { getThemes } from '@/theme-store.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { reloadAsk } from '@/scripts/reload-ask.js';
-import * as os from '@/os.js';
+import { prefer } from '@/preferences.js';
 
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
@@ -169,39 +171,39 @@ const darkThemeSelectorItems = computed(() => {
 	return items;
 });
 
-const darkTheme = ColdDeviceStorage.ref('darkTheme');
+const darkTheme = prefer.r.darkTheme;
 const darkThemeId = computed({
 	get() {
-		return darkTheme.value.id;
+		return darkTheme.value ? darkTheme.value.id : defaultDarkTheme.id;
 	},
 	set(id) {
 		const t = themes.value.find(x => x.id === id);
 		if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
-			ColdDeviceStorage.set('darkTheme', t);
+			prefer.set('darkTheme', t);
 		}
 	},
 });
-const lightTheme = ColdDeviceStorage.ref('lightTheme');
+const lightTheme = prefer.r.lightTheme;
 const lightThemeId = computed({
 	get() {
-		return lightTheme.value.id;
+		return lightTheme.value ? lightTheme.value.id : defaultLightTheme.id;
 	},
 	set(id) {
 		const t = themes.value.find(x => x.id === id);
 		if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
-			ColdDeviceStorage.set('lightTheme', t);
+			prefer.set('lightTheme', t);
 		}
 	},
 });
 
-const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
-const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
+const darkMode = computed(store.makeGetterSetter('darkMode'));
+const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode');
 const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
 const themesCount = installedThemes.value.length;
 
 watch(syncDeviceDarkMode, () => {
 	if (syncDeviceDarkMode.value) {
-		defaultStore.set('darkMode', isDeviceDarkmode());
+		store.set('darkMode', isDeviceDarkmode());
 	}
 });
 
@@ -215,12 +217,6 @@ watch(wallpaper, async () => {
 });
 
 onActivated(() => {
-	fetchThemes().then(() => {
-		installedThemes.value = getThemes();
-	});
-});
-
-fetchThemes().then(() => {
 	installedThemes.value = getThemes();
 });
 
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index b669e25179..5fc921c55c 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -26,7 +26,7 @@ import MkButton from '@/components/MkButton.vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
 import { genEmbedCode } from '@/scripts/get-embed-code.js';
 
@@ -44,11 +44,11 @@ const pagination = {
 const notes = ref<InstanceType<typeof MkNotes>>();
 
 async function post() {
-	defaultStore.set('postFormHashtags', props.tag);
-	defaultStore.set('postFormWithHashtags', true);
+	store.set('postFormHashtags', props.tag);
+	store.set('postFormWithHashtags', true);
 	await os.post();
-	defaultStore.set('postFormHashtags', '');
-	defaultStore.set('postFormWithHashtags', false);
+	store.set('postFormHashtags', '');
+	store.set('postFormWithHashtags', false);
 	notes.value?.pagingComponent?.reload();
 }
 
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 76567cc403..c701030f9e 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -78,24 +78,23 @@ import { toUnicode } from 'punycode.js';
 import tinycolor from 'tinycolor2';
 import { v4 as uuid } from 'uuid';
 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 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 type { Theme } from '@/scripts/theme.js';
-import { host } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { ColdDeviceStorage, defaultStore } from '@/store.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 { prefer } from '@/preferences.js';
 
 const bgColors = [
 	{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
@@ -201,10 +200,10 @@ async function saveAs() {
 	if (description.value) theme.value.desc = description.value;
 	await addTheme(theme.value);
 	applyTheme(theme.value);
-	if (defaultStore.state.darkMode) {
-		ColdDeviceStorage.set('darkTheme', theme.value);
+	if (store.state.darkMode) {
+		prefer.set('darkTheme', theme.value);
 	} else {
-		ColdDeviceStorage.set('lightTheme', theme.value);
+		prefer.set('lightTheme', theme.value);
 	}
 	changed.value = false;
 	os.alert({
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 044a1908ab..4c15194672 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSpacer :contentMax="800">
 		<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
 			<div :key="src" ref="rootEl">
-				<MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
+				<MkInfo v-if="isBasicTimeline(src) && !store.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
 					{{ i18n.ts._timelineDescription[src] }}
 				</MkInfo>
-				<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
+				<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
 				<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
 				<div :class="$style.tl">
 					<MkTimeline
@@ -36,25 +36,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
+import { scroll } from '@@/js/scroll.js';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
+import type { MenuItem } from '@/types/menu.js';
+import type { BasicTimelineType } from '@/timelines.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { scroll } from '@@/js/scroll.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { deepMerge } from '@/scripts/merge.js';
-import type { MenuItem } from '@/types/menu.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
-import type { BasicTimelineType } from '@/timelines.js';
+import { prefer } from '@/preferences.js';
 
 provide('shouldOmitHeaderTitle', true);
 
@@ -66,18 +67,18 @@ type TimelinePageSrc = BasicTimelineType | `list:${string}`;
 const queue = ref(0);
 const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
 const src = computed<TimelinePageSrc>({
-	get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
+	get: () => ($i ? store.reactiveState.tl.value.src : srcWhenNotSignin.value),
 	set: (x) => saveSrc(x),
 });
 const withRenotes = computed<boolean>({
-	get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
+	get: () => store.reactiveState.tl.value.filter.withRenotes,
 	set: (x) => saveTlFilter('withRenotes', x),
 });
 
 // computed内での無限ループを防ぐためのフラグ
 const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>(
-	defaultStore.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
-	defaultStore.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
+	store.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
+	store.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
 	false,
 );
 
@@ -87,7 +88,7 @@ const withReplies = computed<boolean>({
 		if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') {
 			return false;
 		} else {
-			return defaultStore.reactiveState.tl.value.filter.withReplies;
+			return store.reactiveState.tl.value.filter.withReplies;
 		}
 	},
 	set: (x) => saveTlFilter('withReplies', x),
@@ -97,7 +98,7 @@ const onlyFiles = computed<boolean>({
 		if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') {
 			return false;
 		} else {
-			return defaultStore.reactiveState.tl.value.filter.onlyFiles;
+			return store.reactiveState.tl.value.filter.onlyFiles;
 		}
 	},
 	set: (x) => saveTlFilter('onlyFiles', x),
@@ -114,7 +115,7 @@ watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => {
 });
 
 const withSensitive = computed<boolean>({
-	get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
+	get: () => store.reactiveState.tl.value.filter.withSensitive,
 	set: (x) => saveTlFilter('withSensitive', x),
 });
 
@@ -195,23 +196,23 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 }
 
 function saveSrc(newSrc: TimelinePageSrc): void {
-	const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
+	const out = deepMerge({ src: newSrc }, store.state.tl);
 
 	if (newSrc.startsWith('userList:')) {
 		const id = newSrc.substring('userList:'.length);
-		out.userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id) ?? null;
+		out.userList = prefer.r.pinnedUserLists.value.find(l => l.id === id) ?? null;
 	}
 
-	defaultStore.set('tl', out);
+	store.set('tl', out);
 	if (['local', 'global'].includes(newSrc)) {
 		srcWhenNotSignin.value = newSrc as 'local' | 'global';
 	}
 }
 
-function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
+function saveTlFilter(key: keyof typeof store.state.tl.filter, newValue: boolean) {
 	if (key !== 'withReplies' || $i) {
-		const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
-		defaultStore.set('tl', out);
+		const out = deepMerge({ filter: { [key]: newValue } }, store.state.tl);
+		store.set('tl', out);
 	}
 }
 
@@ -230,9 +231,9 @@ function focus(): void {
 
 function closeTutorial(): void {
 	if (!isBasicTimeline(src.value)) return;
-	const before = defaultStore.state.timelineTutorials;
+	const before = store.state.timelineTutorials;
 	before[src.value] = true;
-	defaultStore.set('timelineTutorials', before);
+	store.set('timelineTutorials', before);
 }
 
 function switchTlIfNeeded() {
@@ -298,7 +299,7 @@ const headerActions = computed(() => {
 	return tmp;
 });
 
-const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
+const headerTabs = computed(() => [...(prefer.r.pinnedUserLists.value.map(l => ({
 	key: 'list:' + l.id,
 	title: l.name,
 	icon: 'ti ti-star',
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index 7b74ea67ca..64d20f7945 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -20,7 +20,7 @@ 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 { defaultStore } from '@/store.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';
@@ -64,7 +64,7 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const colorFollowLocal = '#008FFB';
 	const colorFollowRemote = '#008FFB88';
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index 8c7484ae08..cb171928eb 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -20,7 +20,7 @@ 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 { defaultStore } from '@/store.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';
@@ -64,7 +64,7 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const colorNormal = '#008FFB';
 	const colorReply = '#FEB019';
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index a073626cbb..2804211f95 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -20,7 +20,7 @@ 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 { defaultStore } from '@/store.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';
@@ -64,7 +64,7 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const colorUser = '#3498db';
 	const colorVisitor = '#2ecc71';
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 8ebcf975b7..932d944dde 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -176,7 +176,6 @@ import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
 import { $i, iAmModerator } from '@/account.js';
 import { dateString } from '@/filters/date.js';
 import { confetti } from '@/scripts/confetti.js';
@@ -185,6 +184,7 @@ import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFf
 import { useRouter } from '@/router/supplier.js';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import MkSparkle from '@/components/MkSparkle.vue';
+import { prefer } from '@/preferences.js';
 
 function calcAge(birthdate: string): number {
 	const date = new Date(birthdate);
@@ -236,7 +236,7 @@ watch(moderationNote, async () => {
 
 const style = computed(() => {
 	if (props.user.bannerUrl == null) return {};
-	if (defaultStore.state.disableShowingAnimatedImages) {
+	if (prefer.s.disableShowingAnimatedImages) {
 		return {
 			backgroundImage: `url(${ getStaticImageUrl(props.user.bannerUrl) })`,
 		};
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index 918b81b204..778d5c1981 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -11,7 +11,7 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { useStream } from '@/stream.js';
 import { deepClone } from '@/scripts/clone.js';
 import { deepMerge } from '@/scripts/merge.js';
@@ -148,7 +148,7 @@ export class Storage<T extends StateDef> {
 			if ($i) {
 				// api関数と循環参照なので一応setTimeoutしておく
 				window.setTimeout(async () => {
-					await defaultStore.ready;
+					await store.ready;
 
 					misskeyApi('i/registry/get-all', { scope: ['client', this.key] })
 						.then(kvs => {
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index e319a8c398..609c819053 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -3,39 +3,208 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { ref } from 'vue';
+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 { inputText } from '@/os.js';
-import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
-import type { Plugin } from '@/store.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 { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
+
+export type Plugin = {
+	installId: string;
+	name: string;
+	active: boolean;
+	config?: Record<string, { default: any }>;
+	configData: Record<string, any>;
+	src: string | null;
+	version: string;
+	author?: string;
+	description?: string;
+	permissions?: string[];
+};
+
+export type AiScriptPluginMeta = {
+	name: string;
+	version: string;
+	author: string;
+	description?: string;
+	permissions?: string[];
+	config?: Record<string, any>;
+};
 
 const parser = new Parser();
+
+export function isSupportedAiScriptVersion(version: string): boolean {
+	try {
+		return (compareVersions(version, '0.12.0') >= 0);
+	} catch (err) {
+		return false;
+	}
+}
+
+export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> {
+	if (!code) {
+		throw new Error('code is required');
+	}
+
+	const lv = utils.getLangVersion(code);
+	if (lv == null) {
+		throw new Error('No language version annotation found');
+	} else if (!isSupportedAiScriptVersion(lv)) {
+		throw new Error(`Aiscript version '${lv}' is not supported`);
+	}
+
+	let ast;
+	try {
+		ast = parser.parse(code);
+	} catch (err) {
+		throw new Error('Aiscript syntax error');
+	}
+
+	const meta = Interpreter.collectMetadata(ast);
+	if (meta == null) {
+		throw new Error('Meta block not found');
+	}
+
+	const metadata = meta.get(null);
+	if (metadata == null) {
+		throw new Error('Metadata not found');
+	}
+
+	const { name, version, author, description, permissions, config } = metadata;
+	if (name == null || version == null || author == null) {
+		throw new Error('Required property not found');
+	}
+
+	return {
+		name,
+		version,
+		author,
+		description,
+		permissions,
+		config,
+	};
+}
+
+export async function authorizePlugin(plugin: Plugin) {
+	if (plugin.permissions == null || plugin.permissions.length === 0) return;
+	if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) return;
+
+	const token = await new Promise<string>((res, rej) => {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
+			title: i18n.ts.tokenRequested,
+			information: i18n.ts.pluginTokenRequestedDescription,
+			initialName: plugin.name,
+			initialPermissions: plugin.permissions,
+		}, {
+			done: async result => {
+				const { name, permissions } = result;
+				const { token } = await misskeyApi('miauth/gen-token', {
+					session: null,
+					name: name,
+					permission: permissions,
+				});
+				res(token);
+			},
+			closed: () => dispose(),
+		});
+	});
+
+	store.set('pluginTokens', {
+		...store.state.pluginTokens,
+		[plugin.installId]: token,
+	});
+}
+
+export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
+	if (!code) return;
+
+	let realMeta: AiScriptPluginMeta;
+	if (!meta) {
+		realMeta = await parsePluginMeta(code);
+	} else {
+		realMeta = meta;
+	}
+
+	const installId = uuid();
+
+	const plugin = {
+		...realMeta,
+		installId,
+		active: true,
+		configData: {},
+		src: code,
+	};
+
+	prefer.set('plugins', prefer.s.plugins.concat(plugin));
+
+	await authorizePlugin(plugin);
+}
+
+export async function uninstallPlugin(plugin: Plugin) {
+	prefer.set('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId));
+	if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) {
+		await os.apiWithDialog('i/revoke-token', {
+			token: store.state.pluginTokens[plugin.installId],
+		});
+		const pluginTokens = { ...store.state.pluginTokens };
+		delete pluginTokens[plugin.installId];
+		store.set('pluginTokens', pluginTokens);
+	}
+}
+
+export async function configPlugin(plugin: Plugin) {
+	if (plugin.config == null) {
+		throw new Error('This plugin does not have a config');
+	}
+
+	const config = plugin.config;
+	for (const key in plugin.configData) {
+		config[key].default = plugin.configData[key];
+	}
+
+	const { canceled, result } = await os.form(plugin.name, config);
+	if (canceled) return;
+
+	prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, configData: result } : x));
+}
+
+export function changePluginActive(plugin: Plugin, active: boolean) {
+	prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, active } : x));
+}
+
 const pluginContexts = new Map<string, Interpreter>();
 export const pluginLogs = ref(new Map<string, string[]>());
 
-export async function install(plugin: Plugin): Promise<void> {
+export async function launchPlugin(plugin: Plugin): Promise<void> {
 	// 後方互換性のため
 	if (plugin.src == null) return;
 
+	await authorizePlugin(plugin);
+
 	const aiscript = new Interpreter(createPluginEnv({
 		plugin: plugin,
-		storageKey: 'plugins:' + plugin.id,
+		storageKey: 'plugins:' + plugin.installId,
 	}), {
 		in: aiScriptReadline,
 		out: (value): void => {
 			console.log(value);
-			pluginLogs.value.get(plugin.id).push(utils.reprValue(value));
+			pluginLogs.value.get(plugin.installId).push(utils.reprValue(value));
 		},
 		log: (): void => {
 		},
 		err: (err): void => {
-			pluginLogs.value.get(plugin.id).push(`${err}`);
+			pluginLogs.value.get(plugin.installId).push(`${err}`);
 			throw err; // install時のtry-catchに反応させる
 		},
 	});
 
-	initPlugin({ plugin, aiscript });
+	pluginContexts.set(plugin.installId, aiscript);
+	pluginLogs.value.set(plugin.installId, []);
 
 	aiscript.exec(parser.parse(plugin.src)).then(
 		() => {
@@ -49,47 +218,36 @@ export async function install(plugin: Plugin): Promise<void> {
 }
 
 function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
+	const id = opts.plugin.installId;
+
 	const config = new Map<string, values.Value>();
 	for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
 		config.set(k, utils.jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default));
 	}
 
 	return {
-		...createAiScriptEnv({ ...opts, token: opts.plugin.token }),
-		//#region Deprecated
-		'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
-			utils.assertString(title);
-			registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => {
-			utils.assertString(title);
-			registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
-			utils.assertString(title);
-			registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		//#endregion
+		...createAiScriptEnv({ ...opts, token: store.state.pluginTokens[id] }),
+
 		'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
 			utils.assertString(title);
-			registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
+			registerPostFormAction({ pluginId: id, title: title.value, handler });
 		}),
 		'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
 			utils.assertString(title);
-			registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
+			registerUserAction({ pluginId: id, title: title.value, handler });
 		}),
 		'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
 			utils.assertString(title);
-			registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
+			registerNoteAction({ pluginId: id, title: title.value, handler });
 		}),
 		'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
-			registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler });
+			registerNoteViewInterruptor({ pluginId: id, handler });
 		}),
 		'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
-			registerNotePostInterruptor({ pluginId: opts.plugin.id, handler });
+			registerNotePostInterruptor({ pluginId: id, handler });
 		}),
 		'Plugin:register_page_view_interruptor': values.FN_NATIVE(([handler]) => {
-			registerPageViewInterruptor({ pluginId: opts.plugin.id, handler });
+			registerPageViewInterruptor({ pluginId: id, handler });
 		}),
 		'Plugin:open_url': values.FN_NATIVE(([url]) => {
 			utils.assertString(url);
@@ -99,11 +257,6 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
 	};
 }
 
-function initPlugin({ plugin, aiscript }): void {
-	pluginContexts.set(plugin.id, aiscript);
-	pluginLogs.value.set(plugin.id, []);
-}
-
 function registerPostFormAction({ pluginId, title, handler }): void {
 	postFormActions.push({
 		title, handler: (form, update) => {
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
new file mode 100644
index 0000000000..03da3be465
--- /dev/null
+++ b/packages/frontend/src/preferences.ts
@@ -0,0 +1,81 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { v4 as uuid } from 'uuid';
+import type { PreferencesProfile } from '@/preferences/profile.js';
+import { cloudBackup } from '@/preferences/utility.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { ProfileManager } from '@/preferences/profile.js';
+import { store } from '@/store.js';
+import { $i } from '@/account.js';
+
+const TAB_ID = uuid();
+
+function createProfileManager() {
+	let profile: PreferencesProfile;
+
+	const savedProfileRaw = miLocalStorage.getItem('preferences');
+	if (savedProfileRaw == null) {
+		profile = ProfileManager.newProfile();
+		miLocalStorage.setItem('preferences', JSON.stringify(profile));
+	} else {
+		profile = ProfileManager.normalizeProfile(JSON.parse(savedProfileRaw));
+	}
+
+	return new ProfileManager(profile);
+}
+
+export const profileManager = createProfileManager();
+profileManager.addListener('updated', ({ profile: p }) => {
+	miLocalStorage.setItem('preferences', JSON.stringify(p));
+	miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
+});
+export const prefer = profileManager.store;
+
+let latestSyncedAt = Date.now();
+
+function syncBetweenTabs() {
+	const latest = miLocalStorage.getItem('latestPreferencesUpdate');
+	if (latest == null) return;
+
+	const latestTab = latest.split('/')[0];
+	const latestAt = parseInt(latest.split('/')[1]);
+
+	if (latestTab === TAB_ID) return;
+	if (latestAt <= latestSyncedAt) return;
+
+	profileManager.rewriteProfile(ProfileManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!)));
+
+	latestSyncedAt = Date.now();
+
+	if (_DEV_) console.log('prefer:synced');
+}
+
+window.setInterval(syncBetweenTabs, 5000);
+
+document.addEventListener('visibilitychange', () => {
+	if (document.visibilityState === 'visible') {
+		syncBetweenTabs();
+	}
+});
+
+let latestBackupAt = 0;
+
+window.setInterval(() => {
+	if ($i == null) return;
+	if (!store.state.enablePreferencesAutoCloudBackup) return;
+	if (document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
+	if (profileManager.profile.modifiedAt <= latestBackupAt) return;
+
+	cloudBackup().then(() => {
+		latestBackupAt = Date.now();
+	});
+}, 1000 * 60 * 3);
+
+if (_DEV_) {
+	(window as any).profileManager = profileManager;
+	(window as any).prefer = prefer;
+	(window as any).cloudBackup = cloudBackup;
+}
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
new file mode 100644
index 0000000000..19284de6a1
--- /dev/null
+++ b/packages/frontend/src/preferences/def.ts
@@ -0,0 +1,308 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+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 { Plugin } from '@/plugin.js';
+import { DEFAULT_DEVICE_KIND } from '@/scripts/device-kind.js';
+
+/** サウンド設定 */
+export type SoundStore = {
+	type: Exclude<SoundType, '_driveFile_'>;
+	volume: number;
+} | {
+	type: '_driveFile_';
+
+	/** ドライブのファイルID */
+	fileId: string;
+
+	/** ファイルURL（こちらが優先される） */
+	fileUrl: string;
+
+	volume: number;
+};
+
+export const PREF_DEF = {
+	pinnedUserLists: {
+		accountDependent: true,
+		default: [] as Misskey.entities.UserList[],
+	},
+	uploadFolder: {
+		accountDependent: true,
+		default: null as string | null,
+	},
+
+	themes: {
+		default: [] as Theme[],
+	},
+	lightTheme: {
+		default: null as Theme | null,
+	},
+	darkTheme: {
+		default: null as Theme | null,
+	},
+	syncDeviceDarkMode: {
+		default: true,
+	},
+	defaultNoteVisibility: {
+		default: 'public' as (typeof Misskey.noteVisibilities)[number],
+	},
+	defaultNoteLocalOnly: {
+		default: false,
+	},
+	keepCw: {
+		default: true,
+	},
+	keepOriginalUploading: {
+		default: false,
+	},
+	rememberNoteVisibility: {
+		default: false,
+	},
+	reportError: {
+		default: false,
+	},
+	collapseRenotes: {
+		default: true,
+	},
+	menu: {
+		default: [
+			'notifications',
+			'clips',
+			'drive',
+			'followRequests',
+			'-',
+			'explore',
+			'announcements',
+			'search',
+			'-',
+			'ui',
+		],
+	},
+	statusbars: {
+		default: [] as {
+			name: string;
+			id: string;
+			type: string;
+			size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
+			black: boolean;
+			props: Record<string, any>;
+		}[],
+	},
+	serverDisconnectedBehavior: {
+		default: 'quiet' as 'quiet' | 'reload' | 'dialog',
+	},
+	nsfw: {
+		default: 'respect' as 'respect' | 'force' | 'ignore',
+	},
+	highlightSensitiveMedia: {
+		default: false,
+	},
+	animation: {
+		default: !window.matchMedia('(prefers-reduced-motion)').matches,
+	},
+	animatedMfm: {
+		default: !window.matchMedia('(prefers-reduced-motion)').matches,
+	},
+	advancedMfm: {
+		default: true,
+	},
+	showReactionsCount: {
+		default: false,
+	},
+	enableQuickAddMfmFunction: {
+		default: false,
+	},
+	loadRawImages: {
+		default: false,
+	},
+	imageNewTab: {
+		default: false,
+	},
+	disableShowingAnimatedImages: {
+		default: window.matchMedia('(prefers-reduced-motion)').matches,
+	},
+	emojiStyle: {
+		default: 'twemoji', // twemoji / fluentEmoji / native
+	},
+	menuStyle: {
+		default: 'auto' as 'auto' | 'popup' | 'drawer',
+	},
+	useBlurEffectForModal: {
+		default: DEFAULT_DEVICE_KIND === 'desktop',
+	},
+	useBlurEffect: {
+		default: DEFAULT_DEVICE_KIND === 'desktop',
+	},
+	showFixedPostForm: {
+		default: false,
+	},
+	showFixedPostFormInChannel: {
+		default: false,
+	},
+	enableInfiniteScroll: {
+		default: true,
+	},
+	useReactionPickerForContextMenu: {
+		default: false,
+	},
+	showGapBetweenNotesInTimeline: {
+		default: false,
+	},
+	instanceTicker: {
+		default: 'remote' as 'none' | 'remote' | 'always',
+	},
+	emojiPickerScale: {
+		default: 1,
+	},
+	emojiPickerWidth: {
+		default: 1,
+	},
+	emojiPickerHeight: {
+		default: 2,
+	},
+	emojiPickerStyle: {
+		default: 'auto' as 'auto' | 'popup' | 'drawer',
+	},
+	squareAvatars: {
+		default: false,
+	},
+	showAvatarDecorations: {
+		default: true,
+	},
+	numberOfPageCache: {
+		default: 3,
+	},
+	showNoteActionsOnlyHover: {
+		default: false,
+	},
+	showClipButtonInNoteFooter: {
+		default: false,
+	},
+	reactionsDisplaySize: {
+		default: 'medium' as 'small' | 'medium' | 'large',
+	},
+	limitWidthOfReaction: {
+		default: true,
+	},
+	forceShowAds: {
+		default: false,
+	},
+	aiChanMode: {
+		default: false,
+	},
+	devMode: {
+		default: false,
+	},
+	mediaListWithOneImageAppearance: {
+		default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
+	},
+	notificationPosition: {
+		default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom',
+	},
+	notificationStackAxis: {
+		default: 'horizontal' as 'vertical' | 'horizontal',
+	},
+	enableCondensedLine: {
+		default: true,
+	},
+	keepScreenOn: {
+		default: false,
+	},
+	disableStreamingTimeline: {
+		default: false,
+	},
+	useGroupedNotifications: {
+		default: true,
+	},
+	dataSaver: {
+		default: {
+			media: false,
+			avatar: false,
+			urlPreview: false,
+			code: false,
+		} as Record<string, boolean>,
+	},
+	hemisphere: {
+		default: hemisphere as 'N' | 'S',
+	},
+	enableSeasonalScreenEffect: {
+		default: false,
+	},
+	enableHorizontalSwipe: {
+		default: true,
+	},
+	useNativeUiForVideoAudioPlayer: {
+		default: false,
+	},
+	keepOriginalFilename: {
+		default: true,
+	},
+	alwaysConfirmFollow: {
+		default: true,
+	},
+	confirmWhenRevealingSensitiveMedia: {
+		default: false,
+	},
+	contextMenu: {
+		default: 'app' as 'app' | 'appWithShift' | 'native',
+	},
+	skipNoteRender: {
+		default: true,
+	},
+	showSoftWordMutedWord: {
+		default: false,
+	},
+	confirmOnReact: {
+		default: false,
+	},
+	plugins: {
+		default: [] as Plugin[],
+	},
+	'sound.masterVolume': {
+		default: 0.3,
+	},
+	'sound.notUseSound': {
+		default: false,
+	},
+	'sound.useSoundOnlyWhenActive': {
+		default: false,
+	},
+	'sound.on.note': {
+		default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
+	},
+	'sound.on.noteMy': {
+		default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
+	},
+	'sound.on.notification': {
+		default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
+	},
+	'sound.on.reaction': {
+		default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
+	},
+	'deck.alwaysShowMainColumn': {
+		default: true,
+	},
+	'deck.navWindow': {
+		default: true,
+	},
+	'deck.useSimpleUiForNonRootPages': {
+		default: true,
+	},
+	'deck.columnAlign': {
+		default: 'left' as 'left' | 'right' | 'center',
+	},
+	'game.dropAndFusion': {
+		default: {
+			bgmVolume: 0.25,
+			sfxVolume: 1,
+		},
+	},
+} satisfies Record<string, {
+	default: any;
+	accountDependent?: boolean;
+}>;
diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/profile.ts
new file mode 100644
index 0000000000..516fa2b557
--- /dev/null
+++ b/packages/frontend/src/preferences/profile.ts
@@ -0,0 +1,236 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref, watch } from 'vue';
+import { v4 as uuid } from 'uuid';
+import { host, version } from '@@/js/config.js';
+import { EventEmitter } from 'eventemitter3';
+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 { i18n } from '@/i18n.js';
+
+// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
+
+//type DottedToNested<T extends Record<string, any>> = {
+//	[K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K];
+//};
+
+type PREF = typeof PREF_DEF;
+type ValueOf<K extends keyof PREF> = PREF[K]['default'];
+type Account = string; // <host>/<userId>
+
+type Cond = {
+	server: string | null; // 将来のため
+	account: Account | null;
+	device: string | null; // 将来のため
+};
+
+export type PreferencesProfile = {
+	id: string;
+	version: string;
+	type: 'main';
+	modifiedAt: number;
+	name: string;
+	preferences: {
+		[K in keyof PREF]: [Cond, ValueOf<K>][];
+	};
+	syncByAccount: [Account, keyof PREF][],
+};
+
+export class ProfileManager extends EventEmitter<{
+	updated: (ctx: {
+		profile: PreferencesProfile
+	}) => void;
+}> {
+	public profile: PreferencesProfile;
+	public store: Store<{
+		[K in keyof PREF]: ValueOf<K>;
+	}>;
+
+	constructor(profile: PreferencesProfile) {
+		super();
+		this.profile = profile;
+
+		const states = this.genStates();
+
+		this.store = new Store(states);
+		this.store.addListener('updated', ({ key, value }) => {
+			console.log('prefer:set', key, value);
+
+			const record = this.getMatchedRecord(key);
+			if (record[0].account == null && PREF_DEF[key].accountDependent) {
+				this.profile.preferences[key].push([{
+					server: null,
+					account: `${host}/${$i!.id}`,
+					device: null,
+				}, value]);
+				this.save();
+				return;
+			}
+
+			record[1] = value;
+			this.save();
+		});
+	}
+
+	private genStates() {
+		const states = {} as { [K in keyof PREF]: ValueOf<K> };
+		let key: keyof PREF;
+		for (key in PREF_DEF) {
+			const record = this.getMatchedRecord(key);
+			states[key] = record[1];
+		}
+
+		return states;
+	}
+
+	public static newProfile(): PreferencesProfile {
+		const data = {} as PreferencesProfile['preferences'];
+		let key: keyof PREF;
+		for (key in PREF_DEF) {
+			data[key] = [[{
+				server: null,
+				account: null,
+				device: null,
+			}, PREF_DEF[key].default]];
+		}
+		return {
+			id: uuid(),
+			version: version,
+			type: 'main',
+			modifiedAt: Date.now(),
+			name: '',
+			preferences: data,
+			syncByAccount: [],
+		};
+	}
+
+	public static normalizeProfile(profile: any): PreferencesProfile {
+		const data = {} as PreferencesProfile['preferences'];
+		let key: keyof PREF;
+		for (key in PREF_DEF) {
+			const records = profile.preferences[key];
+			if (records == null || records.length === 0) {
+				data[key] = [[{
+					server: null,
+					account: null,
+					device: null,
+				}, PREF_DEF[key].default]];
+				continue;
+			} else {
+				data[key] = records;
+			}
+		}
+
+		return {
+			...profile,
+			preferences: data,
+		};
+	}
+
+	public save() {
+		this.profile.modifiedAt = Date.now();
+		this.profile.version = version;
+		this.emit('updated', { profile: this.profile });
+	}
+
+	public getMatchedRecord<K extends keyof PREF>(key: K): [Cond, ValueOf<K>] {
+		const records = this.profile.preferences[key];
+
+		if ($i == null) return records.find(([cond, v]) => cond.account == null)!;
+
+		const accountOverrideRecord = records.find(([cond, v]) => cond.account === `${host}/${$i!.id}`);
+		if (accountOverrideRecord) return accountOverrideRecord;
+
+		const record = records.find(([cond, v]) => cond.account == null);
+		return record!;
+	}
+
+	public isAccountOverrided<K extends keyof PREF>(key: K): boolean {
+		if ($i == null) return false;
+		return this.profile.preferences[key].some(([cond, v]) => cond.account === `${host}/${$i!.id}`) ?? false;
+	}
+
+	public setAccountOverride<K extends keyof PREF>(key: K) {
+		if ($i == null) return;
+		if (PREF_DEF[key].accountDependent) throw new Error('already account-dependent');
+		if (this.isAccountOverrided(key)) return;
+
+		const records = this.profile.preferences[key];
+		records.push([{
+			server: null,
+			account: `${host}/${$i!.id}`,
+			device: null,
+		}, this.store.s[key]]);
+
+		this.save();
+	}
+
+	public clearAccountOverride<K extends keyof PREF>(key: K) {
+		if ($i == null) return;
+		if (PREF_DEF[key].accountDependent) throw new Error('cannot clear override for this account-dependent property');
+
+		const records = this.profile.preferences[key];
+
+		const index = records.findIndex(([cond, v]) => cond.account === `${host}/${$i!.id}`);
+		if (index === -1) return;
+
+		records.splice(index, 1);
+
+		this.store.rewrite(key, this.getMatchedRecord(key)[1]);
+
+		this.save();
+	}
+
+	public renameProfile(name: string) {
+		this.profile.name = name;
+		this.save();
+	}
+
+	public rewriteProfile(profile: PreferencesProfile) {
+		this.profile = profile;
+		const states = this.genStates();
+		for (const key in states) {
+			this.store.rewrite(key, states[key]);
+		}
+	}
+
+	public getPerPrefMenu<K extends keyof PREF>(key: K): MenuItem[] {
+		const overrideByAccount = ref(this.isAccountOverrided(key));
+
+		watch(overrideByAccount, () => {
+			if (overrideByAccount.value) {
+				this.setAccountOverride(key);
+			} else {
+				this.clearAccountOverride(key);
+			}
+		});
+
+		return [{
+			icon: 'ti ti-copy',
+			text: i18n.ts.copyPreferenceId,
+			action: () => {
+				copyToClipboard(key);
+			},
+		}, {
+			icon: 'ti ti-refresh',
+			text: i18n.ts.resetToDefaultValue,
+			danger: true,
+			action: () => {
+				this.store.set(key, PREF_DEF[key].default);
+			},
+		}, {
+			type: 'divider',
+		}, {
+			type: 'switch',
+			icon: 'ti ti-user-cog',
+			text: i18n.ts.overrideByAccount,
+			ref: overrideByAccount,
+		}];
+	}
+}
diff --git a/packages/frontend/src/preferences/store.ts b/packages/frontend/src/preferences/store.ts
new file mode 100644
index 0000000000..1029346d81
--- /dev/null
+++ b/packages/frontend/src/preferences/store.ts
@@ -0,0 +1,92 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed, onUnmounted, ref, watch } from 'vue';
+import { EventEmitter } from 'eventemitter3';
+import type { Ref, WritableComputedRef } from 'vue';
+
+// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
+
+//type DottedToNested<T extends Record<string, any>> = {
+//	[K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K];
+//};
+
+type StoreEvent<Data extends Record<string, any>> = {
+	updated: <K extends keyof Data>(ctx: {
+		key: K;
+		value: Data[K];
+	}) => void;
+};
+
+export class Store<Data extends Record<string, any>> extends EventEmitter<StoreEvent<Data>> {
+	/**
+	 * static の略 (static が予約語のため)
+	 */
+	public s = {} as {
+		[K in keyof Data]: Data[K];
+	};
+
+	/**
+	 * reactive の略
+	 */
+	public r = {} as {
+		[K in keyof Data]: Ref<Data[K]>;
+	};
+
+	constructor(data: { [K in keyof Data]: Data[K] }) {
+		super();
+
+		for (const key in data) {
+			this.s[key] = data[key];
+			this.r[key] = ref(this.s[key]);
+		}
+	}
+
+	public set<K extends keyof Data>(key: K, value: Data[K]) {
+		this.r[key].value = this.s[key] = value;
+		this.emit('updated', { key, value });
+	}
+
+	public rewrite<K extends keyof Data>(key: K, value: Data[K]) {
+		this.r[key].value = this.s[key] = value;
+	}
+
+	/**
+	 * 特定のキーの、簡易的なcomputed refを作ります
+	 * 主にvue上で設定コントロールのmodelとして使う用
+	 */
+	public model<K extends keyof Data, V extends Data[K] = Data[K]>(
+		key: K,
+		getter?: (v: Data[K]) => V,
+		setter?: (v: V) => Data[K],
+	): WritableComputedRef<V> {
+		const valueRef = ref(this.s[key]);
+
+		const stop = watch(this.r[key], val => {
+			valueRef.value = val;
+		});
+
+		// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
+		onUnmounted(() => {
+			stop();
+		});
+
+		// TODO: VueのcustomRef使うと良い感じになるかも
+		return computed({
+			get: () => {
+				if (getter) {
+					return getter(valueRef.value);
+				} else {
+					return valueRef.value;
+				}
+			},
+			set: (value) => {
+				const val = setter ? setter(value) : value;
+				this.set(key, val);
+				valueRef.value = val;
+			},
+		});
+	}
+}
diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
new file mode 100644
index 0000000000..7979695231
--- /dev/null
+++ b/packages/frontend/src/preferences/utility.ts
@@ -0,0 +1,222 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+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 { 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';
+
+export function getPreferencesProfileMenu(): MenuItem[] {
+	const autoBackupEnabled = ref(store.state.enablePreferencesAutoCloudBackup);
+
+	watch(autoBackupEnabled, () => {
+		if (autoBackupEnabled.value) {
+			if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') {
+				autoBackupEnabled.value = false;
+				os.alert({
+					type: 'warning',
+					title: i18n.ts._preferencesBackup.youNeedToNameYourProfileToEnableAutoBackup,
+				});
+				return;
+			}
+
+			store.set('enablePreferencesAutoCloudBackup', true);
+		} else {
+			store.set('enablePreferencesAutoCloudBackup', false);
+		}
+	});
+
+	const menu: MenuItem[] = [{
+		type: 'label',
+		text: profileManager.profile.name || `(${i18n.ts.noName})`,
+	}, {
+		text: i18n.ts.rename,
+		icon: 'ti ti-pencil',
+		action: () => {
+			renameProfile();
+		},
+	}, {
+		type: 'switch',
+		icon: 'ti ti-cloud-up',
+		text: i18n.ts._preferencesBackup.autoBackup,
+		ref: autoBackupEnabled,
+	}, {
+		text: i18n.ts.export,
+		icon: 'ti ti-download',
+		action: () => {
+			exportCurrentProfile();
+		},
+	}, {
+		type: 'divider',
+	}, {
+		text: i18n.ts._preferencesBackup.restoreFromBackup,
+		icon: 'ti ti-cloud-down',
+		action: () => {
+			restoreFromCloudBackup();
+		},
+	}, {
+		text: i18n.ts.import,
+		icon: 'ti ti-upload',
+		action: () => {
+			importProfile();
+		},
+	}];
+
+	if (prefer.s.devMode) {
+		menu.push({
+			type: 'divider',
+		}, {
+			text: 'Copy profile as text',
+			icon: 'ti ti-clipboard',
+			action: () => {
+				copyToClipboard(JSON.stringify(profileManager.profile, null, '\t'));
+			},
+		});
+	}
+
+	return menu;
+}
+
+async function renameProfile() {
+	const { canceled, result: name } = await os.inputText({
+		title: i18n.ts._preferencesProfile.profileName,
+		text: i18n.ts._preferencesProfile.profileNameDescription + '\n' + i18n.ts._preferencesProfile.profileNameDescription2,
+		placeholder: profileManager.profile.name || null,
+		default: profileManager.profile.name || null,
+	});
+	if (canceled || name == null || name.trim() === '') return;
+
+	profileManager.renameProfile(name);
+}
+
+function exportCurrentProfile() {
+	const p = profileManager.profile;
+	const txtBlob = new Blob([JSON.stringify(p)], { type: 'text/plain' });
+	const dummya = document.createElement('a');
+	dummya.href = URL.createObjectURL(txtBlob);
+	dummya.download = `${p.name || p.id}.misskeypreferences`;
+	dummya.click();
+}
+
+function importProfile() {
+	const input = document.createElement('input');
+	input.type = 'file';
+	input.accept = '.misskeypreferences';
+	input.onchange = async () => {
+		if (input.files == null || input.files.length === 0) return;
+
+		const file = input.files[0];
+		const txt = await file.text();
+		const profile = JSON.parse(txt) as PreferencesProfile;
+
+		miLocalStorage.setItem('preferences', JSON.stringify(profile));
+		miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+		shouldSuggestRestoreBackup.value = false;
+		unisonReload();
+	};
+
+	input.click();
+}
+
+export async function cloudBackup() {
+	if ($i == null) return;
+	if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') {
+		throw new Error('Profile name is not set');
+	}
+
+	await misskeyApi('i/registry/set', {
+		scope: ['client', 'preferences', 'backups'],
+		key: profileManager.profile.name,
+		value: profileManager.profile,
+	});
+}
+
+export async function restoreFromCloudBackup() {
+	if ($i == null) return;
+
+	// TODO: 更新日時でソートして取得したい
+	const keys = await misskeyApi('i/registry/keys', {
+		scope: ['client', 'preferences', 'backups'],
+	});
+
+	console.log(keys);
+
+	if (keys.length === 0) {
+		os.alert({
+			type: 'warning',
+			title: i18n.ts._preferencesBackup.noBackupsFoundTitle,
+			text: i18n.ts._preferencesBackup.noBackupsFoundDescription,
+		});
+		return;
+	}
+
+	const select = await os.select({
+		title: i18n.ts._preferencesBackup.selectBackupToRestore,
+		items: keys.map(k => ({
+			text: k,
+			value: k,
+		})),
+	});
+	if (select.canceled) return;
+	if (select.result == null) return;
+
+	const profile = await misskeyApi('i/registry/get', {
+		scope: ['client', 'preferences', 'backups'],
+		key: select.result,
+	});
+
+	console.log(profile);
+
+	miLocalStorage.setItem('preferences', JSON.stringify(profile));
+	miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+	store.set('enablePreferencesAutoCloudBackup', true);
+	shouldSuggestRestoreBackup.value = false;
+	unisonReload();
+}
+
+export async function enableAutoBackup() {
+	if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') {
+		await renameProfile();
+	}
+
+	if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') {
+		return;
+	}
+
+	store.set('enablePreferencesAutoCloudBackup', true);
+}
+
+export const shouldSuggestRestoreBackup = ref(false);
+
+if ($i != null) {
+	if (new Date($i.createdAt).getTime() < (Date.now() - 1000 * 60 * 30)) { // アカウント作成直後は意味ないので除外
+		miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+	} else {
+		if (miLocalStorage.getItem('hidePreferencesRestoreSuggestion') !== 'true') {
+			misskeyApi('i/registry/keys', {
+				scope: ['client', 'preferences', 'backups'],
+			}).then(keys => {
+				if (keys.length === 0) {
+					miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+				} else {
+					shouldSuggestRestoreBackup.value = true;
+				}
+			});
+		}
+	}
+}
+
+export function hideRestoreBackupSuggestion() {
+	miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+	shouldSuggestRestoreBackup.value = false;
+}
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index c6ee128f5f..7e0efa2e89 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -165,10 +165,6 @@ const routes: RouteDef[] = [{
 		path: '/deck',
 		name: 'deck',
 		component: page(() => import('@/pages/settings/deck.vue')),
-	}, {
-		path: '/preferences-backups',
-		name: 'preferences-backups',
-		component: page(() => import('@/pages/settings/preferences-backups.vue')),
 	}, {
 		path: '/custom-css',
 		name: 'preferences',
diff --git a/packages/frontend/src/scripts/autogen/settings-search-index.ts b/packages/frontend/src/scripts/autogen/settings-search-index.ts
index c62272b271..983bc07d38 100644
--- a/packages/frontend/src/scripts/autogen/settings-search-index.ts
+++ b/packages/frontend/src/scripts/autogen/settings-search-index.ts
@@ -57,12 +57,12 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['mute'],
 			},
 			{
-				id: 'xy5OOBB4A',
+				id: 'oALW4ja7U',
 				label: i18n.ts.useSoundOnlyWhenActive,
 				keywords: ['active', 'mute'],
 			},
 			{
-				id: '9MxYVIf7k',
+				id: 'BbJK2SKT2',
 				label: i18n.ts.masterVolume,
 				keywords: ['volume', 'master'],
 			},
@@ -267,15 +267,10 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['remember', 'keep', 'note', 'visibility'],
 			},
 			{
-				id: 'rhKwScbVS',
+				id: '1u5HZuujV',
 				label: i18n.ts.defaultNoteVisibility,
 				keywords: ['default', 'note', 'visibility'],
 			},
-			{
-				id: '3EmXVyevo',
-				label: i18n.ts.keepCw,
-				keywords: ['remember', 'keep', 'note', 'cw'],
-			},
 		],
 		label: i18n.ts.privacy,
 		keywords: ['privacy'],
@@ -301,50 +296,50 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['post', 'form', 'timeline'],
 			},
 			{
-				id: '9ra14w32V',
+				id: 'snyCQ5oKE',
 				label: i18n.ts.showFixedPostFormInChannel,
 				keywords: ['post', 'form', 'timeline', 'channel'],
 			},
 			{
-				id: '84MdeDWL1',
+				id: '8j36S4Ev6',
 				label: i18n.ts.pinnedList,
 				keywords: ['pinned', 'list'],
 			},
 			{
-				id: 'fYdWhBbrN',
+				id: 'CWpyT9vLK',
 				label: i18n.ts.enableQuickAddMfmFunction,
 				keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
 			},
 			{
-				id: '4huRldNp5',
+				id: 'puIqj1a8b',
 				children: [
 					{
-						id: 'puIqj1a8b',
+						id: '1x3JNXj8N',
 						label: i18n.ts.collapseRenotes,
 						keywords: ['renote', i18n.ts.collapseRenotesDescription],
 					},
 					{
-						id: 'wqpOC22Zm',
+						id: 'c98gbF9c6',
 						label: i18n.ts.showNoteActionsOnlyHover,
 						keywords: ['hover', 'show', 'footer', 'action'],
 					},
 					{
-						id: 'cjfAtxMzP',
+						id: '4LxdiOMNh',
 						label: i18n.ts.showClipButtonInNoteFooter,
 						keywords: ['footer', 'action', 'clip', 'show'],
 					},
 					{
-						id: 'khzxoCjtp',
+						id: '9gTCaLkIf',
 						label: i18n.ts.enableAdvancedMfm,
 						keywords: ['mfm', 'enable', 'show', 'advanced'],
 					},
 					{
-						id: 'uJkoVjTmF',
+						id: '6kMj4HVOg',
 						label: i18n.ts.showReactionsCount,
 						keywords: ['reaction', 'count', 'show'],
 					},
 					{
-						id: '9gTCaLkIf',
+						id: 'dPersnkzh',
 						label: i18n.ts.loadRawImages,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
 					},
@@ -353,10 +348,10 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['note'],
 			},
 			{
-				id: '5G6O6qdis',
+				id: '5XhS6ukl8',
 				children: [
 					{
-						id: 'sYTvqUbhP',
+						id: '3GcWIaZf8',
 						label: i18n.ts.useGroupedNotifications,
 						keywords: ['group'],
 					},
@@ -365,55 +360,60 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['notification'],
 			},
 			{
-				id: 'c3xhLyXZ5',
+				id: 'dSGDnj2PA',
 				children: [
 					{
-						id: 'FbhoeuRAD',
+						id: '1LHOhDKGW',
 						label: i18n.ts.openImageInNewTab,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
 					},
 					{
-						id: 'qixh85g2N',
+						id: 'DSzwvTp7i',
 						label: i18n.ts.useReactionPickerForContextMenu,
 						keywords: ['reaction', 'picker', 'contextmenu', 'open'],
 					},
 					{
-						id: 'd2H4E5ys6',
+						id: '5QTUzrpT3',
 						label: i18n.ts.enableInfiniteScroll,
 						keywords: ['load', 'auto', 'more'],
 					},
 					{
-						id: 'jC7LtTnmc',
+						id: '7Uf8ksn3q',
 						label: i18n.ts.disableStreamingTimeline,
 						keywords: ['disable', 'streaming', 'timeline'],
 					},
 					{
-						id: '8xazEqlgZ',
+						id: 'whKYKvaQB',
 						label: i18n.ts.alwaysConfirmFollow,
 						keywords: ['follow', 'confirm', 'always'],
 					},
 					{
-						id: 'wZqrDQZar',
+						id: 'nf4kcPeYw',
 						label: i18n.ts.confirmWhenRevealingSensitiveMedia,
 						keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
 					},
 					{
-						id: '5QTUzrpT3',
+						id: 'rRisK1YYQ',
 						label: i18n.ts.confirmOnReact,
 						keywords: ['reaction', 'confirm'],
 					},
 					{
-						id: 'nygexkaUk',
+						id: '6AH0lnjf1',
+						label: i18n.ts.keepCw,
+						keywords: ['remember', 'keep', 'note', 'cw'],
+					},
+					{
+						id: 'uHcTVSGDv',
 						label: i18n.ts.whenServerDisconnected,
 						keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
 					},
 					{
-						id: 'whKYKvaQB',
+						id: 'fzPca1Gk9',
 						label: i18n.ts.numberOfPageCache,
 						keywords: ['cache', 'page'],
 					},
 					{
-						id: 'lBbtAg0Hm',
+						id: 'mNU5IBln7',
 						label: i18n.ts.dataSaver,
 						keywords: ['datasaver'],
 					},
@@ -422,20 +422,20 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['behavior'],
 			},
 			{
-				id: 'y2v7CV9zs',
+				id: 'C3psHYdZn',
 				children: [
 					{
-						id: 'k1qTdyfzM',
+						id: 'iCEiAg4Wg',
 						label: i18n.ts.forceShowAds,
 						keywords: ['ad', 'show'],
 					},
 					{
-						id: 'e9As4Us48',
+						id: 'qj9eChQ5B',
 						label: i18n.ts.hemisphere,
 						keywords: [],
 					},
 					{
-						id: 'zvM13vl26',
+						id: 'uItIge5hw',
 						label: i18n.ts.additionalEmojiDictionary,
 						keywords: ['emoji', 'dictionary', 'additional', 'extra'],
 					},
@@ -449,6 +449,13 @@ export const searchIndexes: SearchIndexItem[] = [
 		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: [
@@ -626,17 +633,17 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription],
 			},
 			{
-				id: 'oqUiI5w0s',
+				id: 'D8HUTGWE1',
 				label: i18n.ts.keepOriginalFilename,
 				keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription],
 			},
 			{
-				id: 'Aszkikq9n',
+				id: '6xAvsWSZi',
 				label: i18n.ts.alwaysMarkSensitive,
 				keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'],
 			},
 			{
-				id: 'iGlVjsfVj',
+				id: 'csNNPF1KX',
 				label: i18n.ts.enableAutoSensitive,
 				keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription],
 			},
@@ -662,80 +669,80 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['blur'],
 			},
 			{
-				id: 'vbZvyLDC1',
+				id: 'C05WQNSIJ',
 				label: i18n.ts.useBlurEffectForModal,
 				keywords: ['blur', 'modal'],
 			},
 			{
-				id: '6fLNMTwNt',
+				id: 'snVKNr7Bw',
 				label: i18n.ts.highlightSensitiveMedia,
 				keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
 			},
 			{
-				id: 'hhvF8Z4pF',
+				id: 'DsS2CwjYE',
 				label: i18n.ts.squareAvatars,
 				keywords: ['avatar', 'icon', 'square'],
 			},
 			{
-				id: 'DsS2CwjYE',
+				id: 'xCcTDl651',
 				label: i18n.ts.showAvatarDecorations,
 				keywords: ['avatar', 'icon', 'decoration', 'show'],
 			},
 			{
-				id: 'pWZ0ypy2g',
+				id: '3dHw723VD',
 				label: i18n.ts.showGapBetweenNotesInTimeline,
 				keywords: ['note', 'timeline', 'gap'],
 			},
 			{
-				id: 'AfRMcC6IM',
-				label: i18n.ts.useSystemFont,
-				keywords: ['font', 'system', 'native'],
-			},
-			{
-				id: 'jD0qbxlzN',
+				id: 'AWi72xbrl',
 				label: i18n.ts.seasonalScreenEffect,
 				keywords: ['effect', 'show'],
 			},
 			{
-				id: 'EdYo3hOK',
+				id: 'Ces8FsJws',
 				label: i18n.ts.menuStyle,
 				keywords: ['menu', 'style', 'popup', 'drawer'],
 			},
 			{
-				id: '9mSlX0EkD',
+				id: 'wDr9xSXCv',
 				label: i18n.ts.emojiStyle,
 				keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
 			},
 			{
-				id: '44UmMwmUe',
+				id: 'vFB0pLzck',
 				label: i18n.ts.fontSize,
 				keywords: ['font', 'size'],
 			},
 			{
-				id: 'vFB0pLzck',
+				id: '23BhvYXPC',
+				label: i18n.ts.useSystemFont,
+				keywords: ['font', 'system', 'native'],
+			},
+			{
+				id: 'EeNLndAOa',
 				children: [
 					{
-						id: 'pc7IpPEU4',
+						id: 'rAAPoaodS',
 						label: i18n.ts.reactionsDisplaySize,
 						keywords: ['reaction', 'size', 'scale', 'display'],
 					},
 					{
-						id: 'siOW5aSwp',
+						id: 'qTLAvNWsc',
 						label: i18n.ts.limitWidthOfReaction,
 						keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
 					},
 					{
-						id: 'dDUvhk13F',
+						id: '2lWgzAm13',
 						label: i18n.ts.mediaListWithOneImageAppearance,
 						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
 					},
 					{
-						id: 'CLxNL1Rp0',
+						id: 'EU7HbxOR5',
 						label: i18n.ts.instanceTicker,
 						keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
 					},
 					{
-						id: 'dP2KWDYzD',
+						id: 'AEtM0FAp1',
 						label: i18n.ts.displayOfSensitiveMedia,
 						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
 					},
@@ -744,15 +751,15 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['note', 'display'],
 			},
 			{
-				id: 'dVOzi22IW',
+				id: 'A1FMC2Zon',
 				children: [
 					{
-						id: 'aoF4ufUwn',
+						id: 'CB37G5ZDo',
 						label: i18n.ts.position,
 						keywords: ['position'],
 					},
 					{
-						id: 'sKK2XSS69',
+						id: 'gGS2i19hS',
 						label: i18n.ts.stackAxis,
 						keywords: ['stack', 'axis', 'direction'],
 					},
@@ -775,32 +782,32 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['animation', 'motion', 'reduce'],
 			},
 			{
-				id: 'RhYwm8At',
+				id: 'cXr3tFdpa',
 				label: i18n.ts.disableShowingAnimatedImages,
 				keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'],
 			},
 			{
-				id: '5mZxz2cru',
+				id: 'Ok1UBwtP',
 				label: i18n.ts.enableAnimatedMfm,
 				keywords: ['mfm', 'enable', 'show', 'animated'],
 			},
 			{
-				id: 'bgjamYEis',
+				id: 'yPEpJigqY',
 				label: i18n.ts.enableHorizontalSwipe,
 				keywords: ['swipe', 'horizontal', 'tab'],
 			},
 			{
-				id: 'yPEpJigqY',
+				id: 'h7iZtdTU3',
 				label: i18n.ts.keepScreenOn,
 				keywords: ['keep', 'screen', 'display', 'on'],
 			},
 			{
-				id: 'oxwiGKMu0',
+				id: 'gP1BY3PDy',
 				label: i18n.ts.useNativeUIForVideoAudioPlayer,
 				keywords: ['native', 'system', 'video', 'audio', 'player', 'media'],
 			},
 			{
-				id: 'n90tffyiU',
+				id: 'jnMK3M6rs',
 				label: i18n.ts._contextMenu.title,
 				keywords: ['contextmenu', 'system', 'native'],
 			},
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index 4d57dcd944..4f2aff9d4c 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -10,18 +10,20 @@ 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 { ColdDeviceStorage } from '@/store.js';
+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(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme'));
+	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);
@@ -77,19 +79,19 @@ async function initHighlighter() {
 		],
 	});
 
-	ColdDeviceStorage.watch('lightTheme', async () => {
-		const newTheme = await getTheme('light');
-		if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
-			highlighter.loadTheme(newTheme);
-		}
-	});
-
-	ColdDeviceStorage.watch('darkTheme', async () => {
-		const newTheme = await getTheme('dark');
-		if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
-			highlighter.loadTheme(newTheme);
-		}
-	});
+	// 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;
 
diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts
index e704b5fd6f..7ff5863243 100644
--- a/packages/frontend/src/scripts/emoji-picker.ts
+++ b/packages/frontend/src/scripts/emoji-picker.ts
@@ -6,7 +6,7 @@
 import { defineAsyncComponent, ref } from 'vue';
 import type { Ref } from 'vue';
 import { popup } from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 /**
  * 絵文字ピッカーを表示する。
@@ -25,7 +25,7 @@ class EmojiPicker {
 	}
 
 	public async init() {
-		const emojisRef = defaultStore.reactiveState.pinnedEmojis;
+		const emojisRef = store.reactiveState.pinnedEmojis;
 		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 			src: this.src,
 			pinnedEmojis: emojisRef,
diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts
index c8ab9238d3..6ac0684170 100644
--- a/packages/frontend/src/scripts/get-drive-file-menu.ts
+++ b/packages/frontend/src/scripts/get-drive-file-menu.ts
@@ -5,12 +5,12 @@
 
 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 type { MenuItem } from '@/types/menu.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 function rename(file: Misskey.entities.DriveFile) {
 	os.inputText({
@@ -148,7 +148,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
 		action: () => deleteFile(file),
 	});
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 84122c0e60..8ce4a81bd4 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -15,7 +15,7 @@ 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 { defaultStore, noteActions } from '@/store.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';
@@ -23,6 +23,7 @@ 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;
@@ -507,7 +508,7 @@ export function getNoteMenu(props: {
 		})));
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyNoteId,
@@ -558,7 +559,7 @@ export function getRenoteMenu(props: {
 			icon: 'ti ti-repeat',
 			action: () => {
 				const el = props.renoteButton.value;
-				if (el && defaultStore.state.animation) {
+				if (el && prefer.s.animation) {
 					const rect = el.getBoundingClientRect();
 					const x = rect.left + (el.offsetWidth / 2);
 					const y = rect.top + (el.offsetHeight / 2);
@@ -596,7 +597,7 @@ export function getRenoteMenu(props: {
 			icon: 'ti ti-repeat',
 			action: () => {
 				const el = props.renoteButton.value;
-				if (el && defaultStore.state.animation) {
+				if (el && prefer.s.animation) {
 					const rect = el.getBoundingClientRect();
 					const x = rect.left + (el.offsetWidth / 2);
 					const y = rect.top + (el.offsetHeight / 2);
@@ -605,8 +606,8 @@ export function getRenoteMenu(props: {
 					});
 				}
 
-				const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
-				const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
+				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);
@@ -647,7 +648,7 @@ export function getRenoteMenu(props: {
 					text: channel.name,
 					action: () => {
 						const el = props.renoteButton.value;
-						if (el && defaultStore.state.animation) {
+						if (el && prefer.s.animation) {
 							const rect = el.getBoundingClientRect();
 							const x = rect.left + (el.offsetWidth / 2);
 							const y = rect.top + (el.offsetHeight / 2);
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 8f7c3ba3be..6892c3a4e4 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -6,19 +6,20 @@
 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 { host, url } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore, userActions } from '@/store.js';
+import { userActions } from '@/store.js';
 import { $i, iAmModerator } from '@/account.js';
 import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
-import type { IRouter } from '@/nirax.js';
 import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
 import { mainRouter } from '@/router/main.js';
 import { genEmbedCode } from '@/scripts/get-embed-code.js';
-import type { MenuItem } from '@/types/menu.js';
+import { prefer } from '@/preferences.js';
 
 export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
 	const meId = $i ? $i.id : null;
@@ -251,7 +252,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 								listId: list.id,
 								userId: user.id,
 							}).then(() => {
-								list.userIds?.splice(list.userIds?.indexOf(user.id), 1);
+								list.userIds?.splice(list.userIds.indexOf(user.id), 1);
 							});
 						}
 					}));
@@ -398,7 +399,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		});
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyUserId,
diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts
index 41e1636aa7..037b0d9567 100644
--- a/packages/frontend/src/scripts/init-chart.ts
+++ b/packages/frontend/src/scripts/init-chart.ts
@@ -24,7 +24,7 @@ import {
 import gradient from 'chartjs-plugin-gradient';
 import zoomPlugin from 'chartjs-plugin-zoom';
 import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import 'chartjs-adapter-date-fns';
 
 export function initChart() {
@@ -52,7 +52,7 @@ export function initChart() {
 	// フォントカラー
 	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
 
-	Chart.defaults.borderColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+	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/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts
deleted file mode 100644
index 37f473b6de..0000000000
--- a/packages/frontend/src/scripts/install-plugin.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { defineAsyncComponent } from 'vue';
-import { compareVersions } from 'compare-versions';
-import { v4 as uuid } from 'uuid';
-import { Interpreter, Parser, utils } from '@syuilo/aiscript';
-import type { Plugin } from '@/store.js';
-import { ColdDeviceStorage } from '@/store.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { i18n } from '@/i18n.js';
-
-export type AiScriptPluginMeta = {
-	name: string;
-	version: string;
-	author: string;
-	description?: string;
-	permissions?: string[];
-	config?: Record<string, any>;
-};
-
-const parser = new Parser();
-
-export function savePlugin({ id, meta, src, token }: {
-	id: string;
-	meta: AiScriptPluginMeta;
-	src: string;
-	token: string;
-}) {
-	ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
-		...meta,
-		id,
-		active: true,
-		configData: {},
-		token: token,
-		src: src,
-	} as Plugin));
-}
-
-export function isSupportedAiScriptVersion(version: string): boolean {
-	try {
-		return (compareVersions(version, '0.12.0') >= 0);
-	} catch (err) {
-		return false;
-	}
-}
-
-export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> {
-	if (!code) {
-		throw new Error('code is required');
-	}
-
-	const lv = utils.getLangVersion(code);
-	if (lv == null) {
-		throw new Error('No language version annotation found');
-	} else if (!isSupportedAiScriptVersion(lv)) {
-		throw new Error(`Aiscript version '${lv}' is not supported`);
-	}
-
-	let ast;
-	try {
-		ast = parser.parse(code);
-	} catch (err) {
-		throw new Error('Aiscript syntax error');
-	}
-
-	const meta = Interpreter.collectMetadata(ast);
-	if (meta == null) {
-		throw new Error('Meta block not found');
-	}
-
-	const metadata = meta.get(null);
-	if (metadata == null) {
-		throw new Error('Metadata not found');
-	}
-
-	const { name, version, author, description, permissions, config } = metadata;
-	if (name == null || version == null || author == null) {
-		throw new Error('Required property not found');
-	}
-
-	return {
-		name,
-		version,
-		author,
-		description,
-		permissions,
-		config,
-	};
-}
-
-export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
-	if (!code) return;
-
-	let realMeta: AiScriptPluginMeta;
-	if (!meta) {
-		realMeta = await parsePluginMeta(code);
-	} else {
-		realMeta = meta;
-	}
-
-	const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => {
-		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
-			title: i18n.ts.tokenRequested,
-			information: i18n.ts.pluginTokenRequestedDescription,
-			initialName: realMeta.name,
-			initialPermissions: realMeta.permissions,
-		}, {
-			done: async result => {
-				const { name, permissions } = result;
-				const { token } = await misskeyApi('miauth/gen-token', {
-					session: null,
-					name: name,
-					permission: permissions,
-				});
-				res(token);
-			},
-			closed: () => dispose(),
-		});
-	});
-
-	savePlugin({
-		id: uuid(),
-		meta: realMeta,
-		token,
-		src: code,
-	});
-}
diff --git a/packages/frontend/src/scripts/install-theme.ts b/packages/frontend/src/scripts/install-theme.ts
deleted file mode 100644
index cc32adcc6a..0000000000
--- a/packages/frontend/src/scripts/install-theme.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import JSON5 from 'json5';
-import { addTheme, getThemes } from '@/theme-store.js';
-import { applyTheme, validateTheme } from '@/scripts/theme.js';
-import type { Theme } from '@/scripts/theme.js';
-
-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/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts
index c142b3ed2a..81f6c02dcf 100644
--- a/packages/frontend/src/scripts/reaction-picker.ts
+++ b/packages/frontend/src/scripts/reaction-picker.ts
@@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js';
 import { defineAsyncComponent, ref } from 'vue';
 import type { Ref } from 'vue';
 import { popup } from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 class ReactionPicker {
 	private src: Ref<HTMLElement | null> = ref(null);
@@ -21,7 +21,7 @@ class ReactionPicker {
 	}
 
 	public async init() {
-		const reactionsRef = defaultStore.reactiveState.reactions;
+		const reactionsRef = store.reactiveState.reactions;
 		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 			src: this.src,
 			pinnedEmojis: reactionsRef,
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index c25b4d73bd..42b34f54f5 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -9,8 +9,8 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
 import { uploadFile } from '@/scripts/upload.js';
+import { prefer } from '@/preferences.js';
 
 export function chooseFileFromPc(
 	multiple: boolean,
@@ -20,8 +20,8 @@ export function chooseFileFromPc(
 		nameConverter?: (file: File) => string | undefined;
 	},
 ): Promise<Misskey.entities.DriveFile[]> {
-	const uploadFolder = options?.uploadFolder ?? defaultStore.state.uploadFolder;
-	const keepOriginal = options?.keepOriginal ?? defaultStore.state.keepOriginalUploading;
+	const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder;
+	const keepOriginal = options?.keepOriginal ?? prefer.s.keepOriginalUploading;
 	const nameConverter = options?.nameConverter ?? (() => undefined);
 
 	return new Promise((res, rej) => {
@@ -82,7 +82,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
 
 			misskeyApi('drive/files/upload-from-url', {
 				url: url,
-				folderId: defaultStore.state.uploadFolder,
+				folderId: prefer.s.uploadFolder,
 				marker,
 			});
 
@@ -96,7 +96,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
 
 function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
 	return new Promise((res, rej) => {
-		const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
+		const keepOriginal = ref(prefer.s.keepOriginalUploading);
 
 		os.popupMenu([label ? {
 			text: label,
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index 2008afe045..436c2b75f0 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -3,8 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import type { SoundStore } from '@/store.js';
-import { defaultStore } from '@/store.js';
+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>();
@@ -127,11 +128,11 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
  * @param type スプライトの種類を指定
  */
 export function playMisskeySfx(operationType: OperationType) {
-	const sound = defaultStore.state[`sound_${operationType}`];
+	const sound = prefer.s[`sound.on.${operationType}`];
 	playMisskeySfxFile(sound).then((succeed) => {
 		if (!succeed && sound.type === '_driveFile_') {
 			// ドライブファイルが存在しない場合はデフォルトのサウンドを再生する
-			const soundName = defaultStore.def[`sound_${operationType}`].default.type as Exclude<SoundType, '_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,
@@ -166,7 +167,7 @@ async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise<boole
 	if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
 		return false;
 	}
-	const masterVolume = defaultStore.state.sound_masterVolume;
+	const masterVolume = prefer.s['sound.masterVolume'];
 	if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
 		return true; // ミュート時は成功として扱う
 	}
@@ -198,10 +199,10 @@ export function createSourceNode(buffer: AudioBuffer, opts: {
 	pan?: number;
 	playbackRate?: number;
 }): {
-	soundSource: AudioBufferSourceNode;
-	panNode: StereoPannerNode;
-	gainNode: GainNode;
-} {
+		soundSource: AudioBufferSourceNode;
+		panNode: StereoPannerNode;
+		gainNode: GainNode;
+	} {
 	const panNode = ctx.createStereoPanner();
 	panNode.pan.value = opts.pan ?? 0;
 
@@ -242,13 +243,13 @@ export async function getSoundDuration(file: string): Promise<number> {
  * ミュートすべきかどうかを判断する
  */
 export function isMute(): boolean {
-	if (defaultStore.state.sound_notUseSound) {
+	if (prefer.s['sound.notUseSound']) {
 		// サウンドを出力しない
 		return true;
 	}
 
 	// noinspection RedundantIfStatementJS
-	if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') {
+	if (prefer.s['sound.useSoundOnlyWhenActive'] && document.visibilityState === 'hidden') {
 		// ブラウザがアクティブな時のみサウンドを出力する
 		return true;
 	}
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index 1a3909c132..851ba41e61 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -7,10 +7,12 @@ 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;
@@ -101,6 +103,7 @@ export function applyTheme(theme: Theme, persist = true) {
 
 	if (persist) {
 		miLocalStorage.setItem('theme', JSON.stringify(props));
+		miLocalStorage.setItem('themeId', theme.id);
 		miLocalStorage.setItem('colorScheme', colorScheme);
 	}
 
@@ -155,3 +158,32 @@ export function validateTheme(theme: Record<string, any>): boolean {
 	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/upload.ts b/packages/frontend/src/scripts/upload.ts
index 713573a377..d105a318a7 100644
--- a/packages/frontend/src/scripts/upload.ts
+++ b/packages/frontend/src/scripts/upload.ts
@@ -7,13 +7,13 @@ 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 { getCompressionConfig } from './upload/compress-config.js';
-import { defaultStore } from '@/store.js';
 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;
@@ -34,7 +34,7 @@ export function uploadFile(
 	file: File,
 	folder?: string | Misskey.entities.DriveFolder,
 	name?: string,
-	keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
+	keepOriginal: boolean = prefer.s.keepOriginalUploading,
 ): Promise<Misskey.entities.DriveFile> {
 	if ($i == null) throw new Error('Not logged in');
 
@@ -59,7 +59,7 @@ export function uploadFile(
 
 			const ctx = reactive<Uploading>({
 				id,
-				name: defaultStore.state.keepOriginalFilename ? filename : id + extension,
+				name: prefer.s.keepOriginalFilename ? filename : id + extension,
 				progressMax: undefined,
 				progressValue: undefined,
 				img: window.URL.createObjectURL(file),
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 128fce315f..6e4b4cd0c1 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -5,15 +5,15 @@
 
 import { markRaw, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { hemisphere } from '@@/js/intl-const.js';
 import lightTheme from '@@/themes/l-light.json5';
 import darkTheme from '@@/themes/d-green-lime.json5';
-import type { SoundType } from '@/scripts/sound.js';
-import type { Ast } from '@syuilo/aiscript';
+import { hemisphere } from '@@/js/intl-const.js';
 import type { DeviceKind } from '@/scripts/device-kind.js';
-import { DEFAULT_DEVICE_KIND } from '@/scripts/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';
 
 interface PostFormAction {
 	title: string,
@@ -42,22 +42,6 @@ interface PageViewInterruptor {
 	handler: (page: Misskey.entities.Page) => unknown;
 }
 
-/** サウンド設定 */
-export type SoundStore = {
-	type: Exclude<SoundType, '_driveFile_'>;
-	volume: number;
-} | {
-	type: '_driveFile_';
-
-	/** ドライブのファイルID */
-	fileId: string;
-
-	/** ファイルURL（こちらが優先される） */
-	fileUrl: string;
-
-	volume: number;
-};
-
 export const postFormActions: PostFormAction[] = [];
 export const userActions: UserAction[] = [];
 export const noteActions: NoteAction[] = [];
@@ -65,9 +49,10 @@ export const noteViewInterruptors: NoteViewInterruptor[] = [];
 export const notePostInterruptors: NotePostInterruptor[] = [];
 export const pageViewInterruptors: PageViewInterruptor[] = [];
 
-// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
-//       あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
-export const defaultStore = markRaw(new Storage('base', {
+/**
+ * 「状態」を管理するストア(not「設定」)
+ */
+export const store = markRaw(new Storage('base', {
 	accountSetupWizard: {
 		where: 'account',
 		default: 0,
@@ -85,38 +70,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'account',
 		default: false,
 	},
-	keepCw: {
-		where: 'account',
-		default: true,
-	},
-	collapseRenotes: {
-		where: 'account',
-		default: true,
-	},
-	rememberNoteVisibility: {
-		where: 'account',
-		default: false,
-	},
-	defaultNoteVisibility: {
-		where: 'account',
-		default: 'public' as (typeof Misskey.noteVisibilities)[number],
-	},
-	defaultNoteLocalOnly: {
-		where: 'account',
-		default: false,
-	},
-	uploadFolder: {
-		where: 'account',
-		default: null as string | null,
-	},
-	pastedFileName: {
-		where: 'account',
-		default: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
-	},
-	keepOriginalUploading: {
-		where: 'account',
-		default: false,
-	},
 	memo: {
 		where: 'account',
 		default: null,
@@ -137,22 +90,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'account',
 		default: [] as string[],
 	},
-
-	menu: {
-		where: 'deviceAccount',
-		default: [
-			'notifications',
-			'clips',
-			'drive',
-			'followRequests',
-			'-',
-			'explore',
-			'announcements',
-			'search',
-			'-',
-			'ui',
-		],
-	},
 	visibility: {
 		where: 'deviceAccount',
 		default: 'public' as (typeof Misskey.noteVisibilities)[number],
@@ -165,17 +102,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
-	statusbars: {
-		where: 'deviceAccount',
-		default: [] as {
-			name: string;
-			id: string;
-			type: string;
-			size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
-			black: boolean;
-			props: Record<string, any>;
-		}[],
-	},
 	widgets: {
 		where: 'account',
 		default: [] as {
@@ -198,14 +124,119 @@ export const defaultStore = markRaw(new Storage('base', {
 			},
 		},
 	},
-	pinnedUserLists: {
+	overridedDeviceKind: {
+		where: 'device',
+		default: null as DeviceKind | null,
+	},
+	darkMode: {
+		where: 'device',
+		default: false,
+	},
+	recentlyUsedEmojis: {
+		where: 'device',
+		default: [] as string[],
+	},
+	recentlyUsedUsers: {
+		where: 'device',
+		default: [] as string[],
+	},
+	menuDisplay: {
+		where: 'device',
+		default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top',
+	},
+	postFormWithHashtags: {
+		where: 'device',
+		default: false,
+	},
+	postFormHashtags: {
+		where: 'device',
+		default: '',
+	},
+	additionalUnicodeEmojiIndexes: {
+		where: 'device',
+		default: {} as Record<string, Record<string, string[]>>,
+	},
+	defaultWithReplies: {
+		where: 'account',
+		default: false,
+	},
+	pluginTokens: {
 		where: 'deviceAccount',
-		default: [] as Misskey.entities.UserList[],
+		default: {} as Record<string, string>, // plugin id, token
+	},
+	'deck.profile': {
+		where: 'deviceAccount',
+		default: 'default',
+	},
+	'deck.columns': {
+		where: 'deviceAccount',
+		default: [] as Column[],
+	},
+	'deck.layout': {
+		where: 'deviceAccount',
+		default: [] as Column['id'][][],
 	},
 
-	overridedDeviceKind: {
+	enablePreferencesAutoCloudBackup: {
 		where: 'device',
-		default: null as DeviceKind | null,
+		default: false,
+	},
+	showPreferencesAutoCloudBackupSuggestion: {
+		where: 'device',
+		default: true,
+	},
+
+	//#region TODO: そのうち消す (preferに移行済み)
+	defaultSideView: {
+		where: 'device',
+		default: false,
+	},
+	defaultNoteVisibility: {
+		where: 'account',
+		default: 'public' as (typeof Misskey.noteVisibilities)[number],
+	},
+	defaultNoteLocalOnly: {
+		where: 'account',
+		default: false,
+	},
+	keepCw: {
+		where: 'account',
+		default: true,
+	},
+	collapseRenotes: {
+		where: 'account',
+		default: true,
+	},
+	rememberNoteVisibility: {
+		where: 'account',
+		default: false,
+	},
+	uploadFolder: {
+		where: 'account',
+		default: null as string | null,
+	},
+	keepOriginalUploading: {
+		where: 'account',
+		default: false,
+	},
+	menu: {
+		where: 'deviceAccount',
+		default: [],
+	},
+	statusbars: {
+		where: 'deviceAccount',
+		default: [] as {
+			name: string;
+			id: string;
+			type: string;
+			size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
+			black: boolean;
+			props: Record<string, any>;
+		}[],
+	},
+	pinnedUserLists: {
+		where: 'deviceAccount',
+		default: [] as Misskey.entities.UserList[],
 	},
 	serverDisconnectedBehavior: {
 		where: 'device',
@@ -287,10 +318,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
-	darkMode: {
-		where: 'device',
-		default: false,
-	},
 	instanceTicker: {
 		where: 'device',
 		default: 'remote' as 'none' | 'remote' | 'always',
@@ -311,22 +338,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: 'auto' as 'auto' | 'popup' | 'drawer',
 	},
-	recentlyUsedEmojis: {
-		where: 'device',
-		default: [] as string[],
-	},
-	recentlyUsedUsers: {
-		where: 'device',
-		default: [] as string[],
-	},
-	defaultSideView: {
-		where: 'device',
-		default: false,
-	},
-	menuDisplay: {
-		where: 'device',
-		default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top',
-	},
 	reportError: {
 		where: 'device',
 		default: false,
@@ -339,18 +350,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
-	postFormWithHashtags: {
-		where: 'device',
-		default: false,
-	},
-	postFormHashtags: {
-		where: 'device',
-		default: '',
-	},
-	themeInitial: {
-		where: 'device',
-		default: true,
-	},
 	numberOfPageCache: {
 		where: 'device',
 		default: 3,
@@ -399,18 +398,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
-	additionalUnicodeEmojiIndexes: {
-		where: 'device',
-		default: {} as Record<string, Record<string, string[]>>,
-	},
 	keepScreenOn: {
 		where: 'device',
 		default: false,
 	},
-	defaultWithReplies: {
-		where: 'account',
-		default: false,
-	},
 	disableStreamingTimeline: {
 		where: 'device',
 		default: false,
@@ -432,17 +423,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
-	dropAndFusion: {
-		where: 'device',
-		default: {
-			bgmVolume: 0.25,
-			sfxVolume: 1,
-		},
-	},
-	hemisphere: {
-		where: 'device',
-		default: hemisphere as 'N' | 'S',
-	},
 	enableHorizontalSwipe: {
 		where: 'device',
 		default: true,
@@ -479,7 +459,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
-
+	hemisphere: {
+		where: 'device',
+		default: hemisphere as 'N' | 'S',
+	},
 	sound_masterVolume: {
 		where: 'device',
 		default: 0.3,
@@ -494,56 +477,49 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	sound_note: {
 		where: 'device',
-		default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
+		default: { type: 'syuilo/n-aec', volume: 1 },
 	},
 	sound_noteMy: {
 		where: 'device',
-		default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
+		default: { type: 'syuilo/n-cea-4va', volume: 1 },
 	},
 	sound_notification: {
 		where: 'device',
-		default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
+		default: { type: 'syuilo/n-ea', volume: 1 },
 	},
 	sound_reaction: {
 		where: 'device',
-		default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
+		default: { type: 'syuilo/bubble2', volume: 1 },
 	},
+	dropAndFusion: {
+		where: 'device',
+		default: {
+			bgmVolume: 0.25,
+			sfxVolume: 1,
+		},
+	},
+	//#endregion
 }));
 
 // TODO: 他のタブと永続化されたstateを同期
 
 const PREFIX = 'miux:' as const;
 
-export type Plugin = {
-	id: string;
-	name: string;
-	active: boolean;
-	config?: Record<string, { default: any }>;
-	configData: Record<string, any>;
-	token: string;
-	src: string | null;
-	version: string;
-	ast: Ast.Node[];
-	author?: string;
-	description?: string;
-	permissions?: string[];
-};
-
 interface Watcher {
 	key: string;
 	callback: (value: unknown) => void;
 }
 
+// TODO: 消す(preferに移行済みのため)
 /**
  * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
  */
-
 export class ColdDeviceStorage {
 	public static default = {
-		lightTheme,
-		darkTheme,
-		syncDeviceDarkMode: true,
-		plugins: [] as Plugin[],
+		lightTheme, // TODO: 消す(preferに移行済みのため)
+		darkTheme, // TODO: 消す(preferに移行済みのため)
+		syncDeviceDarkMode: true, // TODO: 消す(preferに移行済みのため)
+		plugins: [] as Plugin[], // TODO: 消す(preferに移行済みのため)
 	};
 
 	public static watchers: Watcher[] = [];
diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts
index fb010ae426..09c665a2ab 100644
--- a/packages/frontend/src/theme-store.ts
+++ b/packages/frontend/src/theme-store.ts
@@ -3,29 +3,14 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { getBuiltinThemes } from '@/scripts/theme.js';
 import type { Theme } from '@/scripts/theme.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { getBuiltinThemes } from '@/scripts/theme.js';
 import { $i } from '@/account.js';
-
-const lsCacheKey = $i ? `themes:${$i.id}` as const : null;
+import { prefer } from '@/preferences.js';
 
 export function getThemes(): Theme[] {
 	if ($i == null) return [];
-	return JSON.parse(miLocalStorage.getItem(lsCacheKey!) ?? '[]');
-}
-
-export async function fetchThemes(): Promise<void> {
-	if ($i == null) return;
-
-	try {
-		const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' });
-		miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
-	} catch (err) {
-		if (err.code === 'NO_SUCH_KEY') return;
-		throw err;
-	}
+	return prefer.s.themes;
 }
 
 export async function addTheme(theme: Theme): Promise<void> {
@@ -34,15 +19,15 @@ export async function addTheme(theme: Theme): Promise<void> {
 	if (builtinThemes.some(t => t.id === theme.id)) {
 		throw new Error('builtin theme');
 	}
-	await fetchThemes();
-	const themes = getThemes().concat(theme);
-	await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
-	miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
+	const themes = getThemes();
+	if (themes.some(t => t.id === theme.id)) {
+		throw new Error('already exists');
+	}
+	prefer.set('themes', [...themes, theme]);
 }
 
 export async function removeTheme(theme: Theme): Promise<void> {
 	if ($i == null) return;
 	const themes = getThemes().filter(t => t.id !== theme.id);
-	await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
-	miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
+	prefer.set('themes', themes);
 }
diff --git a/packages/frontend/src/ui/_common_/PreferenceRestore.vue b/packages/frontend/src/ui/_common_/PreferenceRestore.vue
new file mode 100644
index 0000000000..0412733350
--- /dev/null
+++ b/packages/frontend/src/ui/_common_/PreferenceRestore.vue
@@ -0,0 +1,73 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+	<span :class="$style.icon">
+		<i class="ti ti-info-circle"></i>
+	</span>
+	<span :class="$style.title">{{ i18n.ts._preferencesBackup.backupFound }}</span>
+	<span :class="$style.body"><button class="_textButton" @click="restore">{{ i18n.ts.restore }}</button> | <button class="_textButton" @click="skip">{{ i18n.ts.skip }}</button></span>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { $i } from '@/account.js';
+import { i18n } from '@/i18n.js';
+import { hideRestoreBackupSuggestion, restoreFromCloudBackup } from '@/preferences/utility.js';
+
+function restore() {
+	restoreFromCloudBackup();
+}
+
+function skip() {
+	hideRestoreBackupSuggestion();
+}
+</script>
+
+<style lang="scss" module>
+.root {
+	--height: 24px;
+	font-size: 0.85em;
+	display: flex;
+	vertical-align: bottom;
+	width: 100%;
+	line-height: var(--height);
+	height: var(--height);
+	overflow: clip;
+	contain: strict;
+	background: var(--MI_THEME-panel);
+
+	@container (max-width: 1000px) {
+		display: block;
+		text-align: center;
+
+		> .body {
+			display: none;
+		}
+	}
+}
+
+.icon {
+	margin-left: 10px;
+}
+
+.title {
+	padding: 0 10px;
+	font-weight: bold;
+
+	&:empty {
+		display: none;
+	}
+}
+
+.body {
+	min-width: 0;
+	flex: 1;
+	overflow: clip;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+</style>
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index d145b9b6c6..51645f9676 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -17,18 +17,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 <TransitionGroup
 	tag="div"
 	:class="[$style.notifications, {
-		[$style.notificationsPosition_leftTop]: defaultStore.state.notificationPosition === 'leftTop',
-		[$style.notificationsPosition_leftBottom]: defaultStore.state.notificationPosition === 'leftBottom',
-		[$style.notificationsPosition_rightTop]: defaultStore.state.notificationPosition === 'rightTop',
-		[$style.notificationsPosition_rightBottom]: defaultStore.state.notificationPosition === 'rightBottom',
-		[$style.notificationsStackAxis_vertical]: defaultStore.state.notificationStackAxis === 'vertical',
-		[$style.notificationsStackAxis_horizontal]: defaultStore.state.notificationStackAxis === 'horizontal',
+		[$style.notificationsPosition_leftTop]: prefer.s.notificationPosition === 'leftTop',
+		[$style.notificationsPosition_leftBottom]: prefer.s.notificationPosition === 'leftBottom',
+		[$style.notificationsPosition_rightTop]: prefer.s.notificationPosition === 'rightTop',
+		[$style.notificationsPosition_rightBottom]: prefer.s.notificationPosition === 'rightBottom',
+		[$style.notificationsStackAxis_vertical]: prefer.s.notificationStackAxis === 'vertical',
+		[$style.notificationsStackAxis_horizontal]: prefer.s.notificationStackAxis === 'horizontal',
 	}]"
-	:moveClass="defaultStore.state.animation ? $style.transition_notification_move : ''"
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_notification_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_notification_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''"
+	:moveClass="prefer.s.animation ? $style.transition_notification_move : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_notification_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_notification_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_notification_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_notification_leaveTo : ''"
 >
 	<div v-for="notification in notifications" :key="notification.id" :class="$style.notification">
 		<XNotification :notification="notification"/>
@@ -56,7 +56,7 @@ import * as sound from '@/scripts/sound.js';
 import { $i } from '@/account.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { globalEvents } from '@/events.js';
 
 const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 44253e93bd..698e9d8d47 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -54,11 +54,11 @@ 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 { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 
-const menu = toRef(defaultStore.state, 'menu');
+const menu = toRef(prefer.s, 'menu');
 const otherMenuItemIndicated = computed(() => {
 	for (const def in navbarItemDef) {
 		if (menu.value.includes(def)) continue;
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index fec8666dc1..1fb99f9f22 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -94,20 +94,21 @@ 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 { defaultStore } from '@/store.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 { useRouter } from '@/router/supplier.js';
+import { prefer } from '@/preferences.js';
 
 const router = useRouter();
 
 const forceIconOnly = ref(window.innerWidth <= 1279);
 const iconOnly = computed(() => {
-	return forceIconOnly.value || (defaultStore.reactiveState.menuDisplay.value === 'sideIcon');
+	return forceIconOnly.value || (store.reactiveState.menuDisplay.value === 'sideIcon');
 });
 
-const menu = computed(() => defaultStore.state.menu);
+const menu = computed(() => prefer.s.menu);
 const otherMenuItemIndicated = computed(() => {
 	for (const def in navbarItemDef) {
 		if (menu.value.includes(def)) continue;
@@ -122,12 +123,12 @@ function calcViewState() {
 
 window.addEventListener('resize', calcViewState);
 
-watch(defaultStore.reactiveState.menuDisplay, () => {
+watch(store.reactiveState.menuDisplay, () => {
 	calcViewState();
 });
 
 function toggleIconOnly() {
-	defaultStore.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
+	store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
 }
 
 function openAccountMenu(ev: MouseEvent) {
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index ed881bef22..a8d87599e6 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<div
-		v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black,
+		v-for="x in prefer.r.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black,
 			[$style.verySmall]: x.size === 'verySmall',
 			[$style.small]: x.size === 'small',
 			[$style.large]: x.size === 'large',
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent } from 'vue';
 import { instance } from '@/instance.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue'));
 const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue'));
 const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue'));
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index cc62a28b14..1eb809d198 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected">
+<div v-if="hasDisconnected && prefer.s.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected">
 	<div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div>
 	<div :class="$style.command" class="_buttons">
 		<MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton>
@@ -19,7 +19,7 @@ import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const zIndex = os.claimZIndex('high');
 
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index f4633314ae..39b40754ff 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -53,15 +53,16 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const WINDOW_THRESHOLD = 1400;
 
 const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
-const menu = ref(defaultStore.state.menu);
-// const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
+const menu = ref(prefer.s.menu);
+// const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 const otherNavItemIndicated = computed<boolean>(() => {
 	for (const def in navbarItemDef) {
 		if (menu.value.includes(def)) continue;
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 5acef0bef8..c11771f028 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -59,14 +59,15 @@ import MkButton from '@/components/MkButton.vue';
 // import { StickySidebar } from '@/scripts/sticky-sidebar.js';
 // import { mainRouter } from '@/router.js';
 //import MisskeyLogo from '@assets/client/misskey.svg';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const WINDOW_THRESHOLD = 1400;
 
-const menu = ref(defaultStore.state.menu);
-const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
+const menu = ref(prefer.s.menu);
+const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 const otherNavItemIndicated = computed<boolean>(() => {
 	for (const def in navbarItemDef) {
 		if (menu.value.includes(def)) continue;
@@ -99,7 +100,7 @@ function openAccountMenu(ev: MouseEvent) {
 	}, ev);
 }
 
-watch(defaultStore.reactiveState.menuDisplay, () => {
+watch(store.reactiveState.menuDisplay, () => {
 	calcViewState();
 });
 
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index da5059bb59..63c60a3d6f 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 
-	<Transition :name="defaultStore.state.animation ? 'tray-back' : ''">
+	<Transition :name="prefer.s.animation ? 'tray-back' : ''">
 		<div
 			v-if="widgetsShowing"
 			class="tray-back _modalBg"
@@ -35,11 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 		></div>
 	</Transition>
 
-	<Transition :name="defaultStore.state.animation ? 'tray' : ''">
+	<Transition :name="prefer.s.animation ? 'tray' : ''">
 		<XWidgets v-if="widgetsShowing" class="tray"/>
 	</Transition>
 
-	<iframe v-if="defaultStore.state.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe>
+	<iframe v-if="prefer.s.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe>
 
 	<XCommon/>
 </div>
@@ -47,18 +47,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue';
+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 { instanceName } from '@@/js/config.js';
+import type { PageMetadata } from '@/scripts/page-metadata.js';
 import { StickySidebar } from '@/scripts/sticky-sidebar.js';
 import * as os from '@/os.js';
 import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { mainRouter } from '@/router/main.js';
-import { isLink } from '@@/js/is-link.js';
+import { prefer } from '@/preferences.js';
 
 const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
@@ -74,7 +75,7 @@ const widgetsShowing = ref(false);
 const fullView = ref(false);
 const globalHeaderHeight = ref(0);
 const wallpaper = miLocalStorage.getItem('wallpaper') != null;
-const showMenuOnTop = computed(() => defaultStore.state.menuDisplay === 'top');
+const showMenuOnTop = computed(() => store.state.menuDisplay === 'top');
 const live2d = shallowRef<HTMLIFrameElement>();
 const widgetsLeft = ref<HTMLElement>();
 const widgetsRight = ref<HTMLElement>();
@@ -96,7 +97,7 @@ provide('shouldHeaderThin', showMenuOnTop.value);
 provide('forceSpacerMin', true);
 
 function attachSticky(el: HTMLElement) {
-	const sticky = new StickySidebar(el, 0, defaultStore.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
+	const sticky = new StickySidebar(el, 0, store.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
 	window.addEventListener('scroll', () => {
 		sticky.calc(window.scrollY);
 	}, { passive: true });
@@ -142,9 +143,9 @@ if (window.innerWidth < 1024) {
 
 document.documentElement.style.overflowY = 'scroll';
 
-defaultStore.loaded.then(() => {
-	if (defaultStore.state.widgets.length === 0) {
-		defaultStore.set('widgets', [{
+store.loaded.then(() => {
+	if (store.state.widgets.length === 0) {
+		store.set('widgets', [{
 			name: 'calendar',
 			id: 'a', place: null, data: {},
 		}, {
@@ -162,7 +163,7 @@ onMounted(() => {
 		isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD);
 	}, { passive: true });
 
-	if (defaultStore.state.aiChanMode) {
+	if (prefer.s.aiChanMode) {
 		const iframeRect = live2d.value.getBoundingClientRect();
 		window.addEventListener('mousemove', ev => {
 			live2d.value.contentWindow.postMessage({
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 8e99c457ad..2107f6f53a 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.main">
 		<XAnnouncements v-if="$i"/>
 		<XStatusBars/>
-		<div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel">
+		<div ref="columnsEl" :class="[$style.sections, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel">
 			<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
 			<section
 				v-for="ids in layout"
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<div :class="$style.sideMenu">
 				<div :class="$style.sideMenuTop">
-					<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
+					<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${store.state['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
 					<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
 				</div>
 				<div :class="$style.sideMenuMiddle">
@@ -62,10 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
 	>
 		<div
 			v-if="drawerMenuShowing"
@@ -77,10 +77,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</Transition>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''"
 	>
 		<div v-if="drawerMenuShowing" :class="$style.menu">
 			<XDrawerMenu/>
@@ -95,8 +95,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue';
 import { v4 as uuid } from 'uuid';
 import XCommon from './_common_/common.vue';
-import { deckStore, columnTypes, addColumn as addColumnToStore, forceSaveDeck, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
-import type { ColumnType } from './deck/deck-store.js';
 import type { MenuItem } from '@/types/menu.js';
 import XSidebar from '@/ui/_common_/navbar.vue';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
@@ -107,7 +105,7 @@ 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 { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import XMainColumn from '@/ui/deck/main-column.vue';
 import XTlColumn from '@/ui/deck/tl-column.vue';
 import XAntennaColumn from '@/ui/deck/antenna-column.vue';
@@ -119,6 +117,8 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
 import XDirectColumn from '@/ui/deck/direct-column.vue';
 import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
 import { mainRouter } from '@/router/main.js';
+import { store } from '@/store.js';
+import { columnTypes, forceSaveDeck, getProfiles, loadDeck, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
 
@@ -137,8 +137,8 @@ const columnComponents = {
 
 mainRouter.navHook = (path, flag): boolean => {
 	if (flag === 'forcePage') return false;
-	const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main');
-	if (deckStore.state.navWindow || noMainColumn) {
+	const noMainColumn = !store.state['deck.columns'].some(x => x.type === 'main');
+	if (prefer.s['deck.navWindow'] || noMainColumn) {
 		os.pageWindow(path);
 		return true;
 	}
@@ -160,8 +160,8 @@ watch(route, () => {
 });
 */
 
-const columns = deckStore.reactiveState.columns;
-const layout = deckStore.reactiveState.layout;
+const columns = store.reactiveState['deck.columns'];
+const layout = store.reactiveState['deck.layout'];
 const menuIndicated = computed(() => {
 	if ($i == null) return false;
 	for (const def in navbarItemDef) {
@@ -214,15 +214,15 @@ loadDeck();
 
 function changeProfile(ev: MouseEvent) {
 	let items: MenuItem[] = [{
-		text: deckStore.state.profile,
+		text: store.state['deck.profile'],
 		active: true,
 		action: () => {},
 	}];
 	getProfiles().then(profiles => {
-		items.push(...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
+		items.push(...(profiles.filter(k => k !== store.state['deck.profile']).map(k => ({
 			text: k,
 			action: () => {
-				deckStore.set('profile', k);
+				store.set('deck.profile', k);
 				unisonReload();
 			},
 		}))), { type: 'divider' as const }, {
@@ -237,7 +237,7 @@ function changeProfile(ev: MouseEvent) {
 				if (canceled || name == null) return;
 
 				os.promiseDialog((async () => {
-					await deckStore.set('profile', name);
+					await store.set('deck.profile', name);
 					await forceSaveDeck();
 				})(), () => {
 					unisonReload();
@@ -252,19 +252,19 @@ function changeProfile(ev: MouseEvent) {
 async function deleteProfile() {
 	const { canceled } = await os.confirm({
 		type: 'warning',
-		text: i18n.tsx.deleteAreYouSure({ x: deckStore.state.profile }),
+		text: i18n.tsx.deleteAreYouSure({ x: store.state['deck.profile'] }),
 	});
 	if (canceled) return;
 
 	os.promiseDialog((async () => {
-		if (deckStore.state.profile === 'default') {
-			await deckStore.set('columns', []);
-			await deckStore.set('layout', []);
+		if (store.state['deck.profile'] === 'default') {
+			await store.set('deck.columns', []);
+			await store.set('deck.layout', []);
 			await forceSaveDeck();
 		} else {
-			await deleteProfile_(deckStore.state.profile);
+			await deleteProfile_(store.state['deck.profile']);
 		}
-		await deckStore.set('profile', 'default');
+		await store.set('deck.profile', 'default');
 	})(), () => {
 		unisonReload();
 	});
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index b79cd8408b..5f0c607edb 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -15,17 +15,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
-import type { entities as MisskeyEntities } from 'misskey-js';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { entities as MisskeyEntities } from 'misskey-js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+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 { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
 import { antennasCache } from '@/cache.js';
-import type { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
 
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index 9e07c06639..f002f655f7 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -22,16 +22,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, ref, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { updateColumn } from '@/deck.js';
 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 { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
-import type { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
 
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index f23e33c748..fc208197a0 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -43,11 +43,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } from 'vue';
-import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from './deck-store.js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from '@/deck.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import type { Column } from './deck-store.js';
-import type { MenuItem } from '@/types/menu.js';
 
 provide('shouldHeaderThin', true);
 provide('shouldOmitHeaderTitle', true);
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index 9055ea6d43..bdca513a7a 100644
--- a/packages/frontend/src/ui/deck/deck-store.ts
+++ b/packages/frontend/src/ui/deck/deck-store.ts
@@ -3,57 +3,11 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { throttle } from 'throttle-debounce';
 import { markRaw } from 'vue';
-import { notificationTypes } from 'misskey-js';
-import type { BasicTimelineType } from '@/timelines.js';
+import type { Column } from '@/deck.js';
 import { Storage } from '@/pizzax.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { deepClone } from '@/scripts/clone.js';
-import type { SoundStore } from '@/store.js';
-
-type ColumnWidget = {
-	name: string;
-	id: string;
-	data: Record<string, any>;
-};
-
-export const columnTypes = [
-	'main',
-	'widgets',
-	'notifications',
-	'tl',
-	'antenna',
-	'list',
-	'channel',
-	'mentions',
-	'direct',
-	'roleTimeline',
-] as const;
-
-export type ColumnType = typeof columnTypes[number];
-
-export type Column = {
-	id: string;
-	type: ColumnType;
-	name: string | null;
-	width: number;
-	widgets?: ColumnWidget[];
-	active?: boolean;
-	flexible?: boolean;
-	antennaId?: string;
-	listId?: string;
-	channelId?: string;
-	roleId?: string;
-	excludeTypes?: typeof notificationTypes[number][];
-	tl?: BasicTimelineType;
-	withRenotes?: boolean;
-	withReplies?: boolean;
-	withSensitive?: boolean;
-	onlyFiles?: boolean;
-	soundSetting?: SoundStore;
-};
 
+// TODO: 消す(移行済みのため)
 export const deckStore = markRaw(new Storage('deck', {
 	profile: {
 		where: 'deviceAccount',
@@ -67,272 +21,4 @@ export const deckStore = markRaw(new Storage('deck', {
 		where: 'deviceAccount',
 		default: [] as Column['id'][][],
 	},
-	columnAlign: {
-		where: 'deviceAccount',
-		default: 'left' as 'left' | 'right' | 'center',
-	},
-	alwaysShowMainColumn: {
-		where: 'deviceAccount',
-		default: true,
-	},
-	navWindow: {
-		where: 'deviceAccount',
-		default: true,
-	},
-	useSimpleUiForNonRootPages: {
-		where: 'deviceAccount',
-		default: true,
-	},
 }));
-
-export const loadDeck = async () => {
-	let deck;
-
-	try {
-		deck = await misskeyApi('i/registry/get', {
-			scope: ['client', 'deck', 'profiles'],
-			key: deckStore.state.profile,
-		});
-	} catch (err) {
-		if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
-			// 後方互換性のため
-			if (deckStore.state.profile === 'default') {
-				saveDeck();
-				return;
-			}
-
-			deckStore.set('columns', []);
-			deckStore.set('layout', []);
-			return;
-		}
-		throw err;
-	}
-
-	deckStore.set('columns', deck.columns);
-	deckStore.set('layout', deck.layout);
-};
-
-export async function forceSaveDeck() {
-	await misskeyApi('i/registry/set', {
-		scope: ['client', 'deck', 'profiles'],
-		key: deckStore.state.profile,
-		value: {
-			columns: deckStore.reactiveState.columns.value,
-			layout: deckStore.reactiveState.layout.value,
-		},
-	});
-}
-
-// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
-export const saveDeck = throttle(1000, () => {
-	forceSaveDeck();
-});
-
-export async function getProfiles(): Promise<string[]> {
-	return await misskeyApi('i/registry/keys', {
-		scope: ['client', 'deck', 'profiles'],
-	});
-}
-
-export async function deleteProfile(key: string): Promise<void> {
-	return await misskeyApi('i/registry/remove', {
-		scope: ['client', 'deck', 'profiles'],
-		key: key,
-	});
-}
-
-export function addColumn(column: Column) {
-	if (column.name === undefined) column.name = null;
-	deckStore.push('columns', column);
-	deckStore.push('layout', [column.id]);
-	saveDeck();
-}
-
-export function removeColumn(id: Column['id']) {
-	deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id));
-	deckStore.set('layout', deckStore.state.layout
-		.map(ids => ids.filter(_id => _id !== id))
-		.filter(ids => ids.length > 0));
-	saveDeck();
-}
-
-export function swapColumn(a: Column['id'], b: Column['id']) {
-	const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1);
-	const aY = deckStore.state.layout[aX].findIndex(id => id === a);
-	const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
-	const bY = deckStore.state.layout[bX].findIndex(id => id === b);
-	const layout = deepClone(deckStore.state.layout);
-	layout[aX][aY] = b;
-	layout[bX][bY] = a;
-	deckStore.set('layout', layout);
-	saveDeck();
-}
-
-export function swapLeftColumn(id: Column['id']) {
-	const layout = deepClone(deckStore.state.layout);
-	deckStore.state.layout.some((ids, i) => {
-		if (ids.includes(id)) {
-			const left = deckStore.state.layout[i - 1];
-			if (left) {
-				layout[i - 1] = deckStore.state.layout[i];
-				layout[i] = left;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-		return false;
-	});
-	saveDeck();
-}
-
-export function swapRightColumn(id: Column['id']) {
-	const layout = deepClone(deckStore.state.layout);
-	deckStore.state.layout.some((ids, i) => {
-		if (ids.includes(id)) {
-			const right = deckStore.state.layout[i + 1];
-			if (right) {
-				layout[i + 1] = deckStore.state.layout[i];
-				layout[i] = right;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-		return false;
-	});
-	saveDeck();
-}
-
-export function swapUpColumn(id: Column['id']) {
-	const layout = deepClone(deckStore.state.layout);
-	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const ids = deepClone(deckStore.state.layout[idsIndex]);
-	ids.some((x, i) => {
-		if (x === id) {
-			const up = ids[i - 1];
-			if (up) {
-				ids[i - 1] = id;
-				ids[i] = up;
-
-				layout[idsIndex] = ids;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-		return false;
-	});
-	saveDeck();
-}
-
-export function swapDownColumn(id: Column['id']) {
-	const layout = deepClone(deckStore.state.layout);
-	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const ids = deepClone(deckStore.state.layout[idsIndex]);
-	ids.some((x, i) => {
-		if (x === id) {
-			const down = ids[i + 1];
-			if (down) {
-				ids[i + 1] = id;
-				ids[i] = down;
-
-				layout[idsIndex] = ids;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-		return false;
-	});
-	saveDeck();
-}
-
-export function stackLeftColumn(id: Column['id']) {
-	let layout = deepClone(deckStore.state.layout);
-	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	layout = layout.map(ids => ids.filter(_id => _id !== id));
-	layout[i - 1].push(id);
-	layout = layout.filter(ids => ids.length > 0);
-	deckStore.set('layout', layout);
-	saveDeck();
-}
-
-export function popRightColumn(id: Column['id']) {
-	let layout = deepClone(deckStore.state.layout);
-	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const affected = layout[i];
-	layout = layout.map(ids => ids.filter(_id => _id !== id));
-	layout.splice(i + 1, 0, [id]);
-	layout = layout.filter(ids => ids.length > 0);
-	deckStore.set('layout', layout);
-
-	const columns = deepClone(deckStore.state.columns);
-	for (const column of columns) {
-		if (affected.includes(column.id)) {
-			column.active = true;
-		}
-	}
-	deckStore.set('columns', columns);
-
-	saveDeck();
-}
-
-export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	if (column.widgets == null) column.widgets = [];
-	column.widgets.unshift(widget);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	if (column.widgets == null) column.widgets = [];
-	column.widgets = column.widgets.filter(w => w.id !== widget.id);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	column.widgets = widgets;
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	if (column.widgets == null) column.widgets = [];
-	column.widgets = column.widgets.map(w => w.id === widgetId ? {
-		...w,
-		data: widgetData,
-	} : w);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function updateColumn(id: Column['id'], column: Partial<Column>) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const currentColumn = deepClone(deckStore.state.columns[columnIndex]);
-	if (currentColumn == null) return;
-	for (const [k, v] of Object.entries(column)) {
-		currentColumn[k] = v;
-	}
-	columns[columnIndex] = currentColumn;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue
index 2cecd6c669..772188d773 100644
--- a/packages/frontend/src/ui/deck/direct-column.vue
+++ b/packages/frontend/src/ui/deck/direct-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import XColumn from './column.vue';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
 import MkNotes from '@/components/MkNotes.vue';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 83961d02bc..f627ab5262 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -15,16 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { watch, shallowRef, ref, onMounted } from 'vue';
-import type { entities as MisskeyEntities } from 'misskey-js';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { entities as MisskeyEntities } from 'misskey-js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+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 { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
-import type { SoundStore } from '@/store.js';
 import { userListsCache } from '@/cache.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 45c39a5cad..906e94f0b3 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked">
+<XColumn v-if="prefer.s['deck.alwaysShowMainColumn'] || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked">
 	<template #header>
 		<template v-if="pageMetadata">
 			<i :class="pageMetadata.icon"></i>
@@ -20,17 +20,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { provide, shallowRef, ref } from 'vue';
+import { getScrollContainer } from '@@/js/scroll.js';
+import { isLink } from '@@/js/is-link.js';
 import XColumn from './column.vue';
-import { deckStore } from '@/ui/deck/deck-store.js';
-import type { Column } from '@/ui/deck/deck-store.js';
+import type { Column } from '@/deck.js';
+import type { PageMetadata } from '@/scripts/page-metadata.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
 import { useScrollPositionManager } from '@/nirax.js';
-import { getScrollContainer } from '@@/js/scroll.js';
-import { isLink } from '@@/js/is-link.js';
 import { mainRouter } from '@/router/main.js';
+import { prefer } from '@/preferences.js';
 
 defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue
index 233fba554b..ffd0307940 100644
--- a/packages/frontend/src/ui/deck/mentions-column.vue
+++ b/packages/frontend/src/ui/deck/mentions-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import XColumn from './column.vue';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
 import MkNotes from '@/components/MkNotes.vue';
 import { i18n } from '../../i18n.js';
 
diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue
index c0303e86dc..0a2c0e9952 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, shallowRef } from 'vue';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import { updateColumn } from '@/deck.js';
+import type { Column } from '@/deck.js';
 import XNotifications from '@/components/MkNotifications.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 5b1420570d..4d1be08706 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -16,14 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, onMounted, ref, shallowRef, watch } from 'vue';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+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 { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
-import type { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
 
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index b9b3746abf..96da944398 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -34,15 +34,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, watch, ref, shallowRef, computed } from 'vue';
 import XColumn from './column.vue';
-import { removeColumn, updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
 import type { MenuItem } from '@/types/menu.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { removeColumn, updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
-import { instance } from '@/instance.js';
-import type { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
 
diff --git a/packages/frontend/src/ui/deck/tl-note-notification.ts b/packages/frontend/src/ui/deck/tl-note-notification.ts
index 03d4b3a580..277ebb0511 100644
--- a/packages/frontend/src/ui/deck/tl-note-notification.ts
+++ b/packages/frontend/src/ui/deck/tl-note-notification.ts
@@ -5,8 +5,8 @@
 
 import * as Misskey from 'misskey-js';
 import type { Ref } from 'vue';
-import type { SoundStore } from '@/store.js';
 import type { SoundType } from '@/scripts/sound.js';
+import type { SoundStore } from '@/preferences/def.js';
 import { getSoundDuration, playMisskeySfxFile, soundsTypes } from '@/scripts/sound.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index 20284d8c9f..4e84ef0ba0 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import XColumn from './column.vue';
-import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from '@/deck.js';
+import type { Column } from '@/deck.js';
 import XWidgets from '@/components/MkWidgets.vue';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 25f47a2d55..bddb62dc60 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu">
 		<template #header>
 			<div>
+				<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
 				<XAnnouncements v-if="$i"/>
 				<XStatusBars :class="$style.statusbars"/>
 			</div>
@@ -38,10 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
 	>
 		<div
 			v-if="drawerMenuShowing"
@@ -53,10 +54,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</Transition>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''"
 	>
 		<div v-if="drawerMenuShowing" :class="$style.menuDrawer">
 			<XDrawerMenu/>
@@ -64,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</Transition>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''"
 	>
 		<div
 			v-if="widgetsShowing"
@@ -79,10 +80,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</Transition>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
 	>
 		<div v-if="widgetsShowing" :class="$style.widgetsDrawer">
 			<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button>
@@ -105,7 +106,7 @@ import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
 import type { PageMetadata } from '@/scripts/page-metadata.js';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { navbarItemDef } from '@/navbar.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
@@ -114,11 +115,14 @@ import { deviceKind } from '@/scripts/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
+import { prefer } from '@/preferences.js';
+import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
 
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
+const XPreferenceRestore = defineAsyncComponent(() => import('@/ui/_common_/PreferenceRestore.vue'));
 
 const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
 
@@ -174,9 +178,9 @@ if (window.innerWidth > 1024) {
 	}
 }
 
-defaultStore.loaded.then(() => {
-	if (defaultStore.state.widgets.length === 0) {
-		defaultStore.set('widgets', [{
+store.loaded.then(() => {
+	if (store.state.widgets.length === 0) {
+		store.set('widgets', [{
 			name: 'calendar',
 			id: 'a', place: 'right', data: {},
 		}, {
diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue
index fc0a4475d2..42b870cbb5 100644
--- a/packages/frontend/src/ui/universal.widgets.vue
+++ b/packages/frontend/src/ui/universal.widgets.vue
@@ -19,7 +19,7 @@ const editMode = ref(false);
 <script lang="ts" setup>
 import XWidgets from '@/components/MkWidgets.vue';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 const props = withDefaults(defineProps<{
 	// null = 全てのウィジェットを表示
@@ -31,24 +31,24 @@ const props = withDefaults(defineProps<{
 });
 
 const widgets = computed(() => {
-	if (props.place === null) return defaultStore.reactiveState.widgets.value;
-	if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left');
-	return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left');
+	if (props.place === null) return store.reactiveState.widgets.value;
+	if (props.place === 'left') return store.reactiveState.widgets.value.filter(w => w.place === 'left');
+	return store.reactiveState.widgets.value.filter(w => w.place !== 'left');
 });
 
 function addWidget(widget) {
-	defaultStore.set('widgets', [{
+	store.set('widgets', [{
 		...widget,
 		place: props.place,
-	}, ...defaultStore.state.widgets]);
+	}, ...store.state.widgets]);
 }
 
 function removeWidget(widget) {
-	defaultStore.set('widgets', defaultStore.state.widgets.filter(w => w.id !== widget.id));
+	store.set('widgets', store.state.widgets.filter(w => w.id !== widget.id));
 }
 
 function updateWidget({ id, data }) {
-	defaultStore.set('widgets', defaultStore.state.widgets.map(w => w.id === id ? {
+	store.set('widgets', store.state.widgets.map(w => w.id === id ? {
 		...w,
 		data,
 		place: props.place,
@@ -57,18 +57,18 @@ function updateWidget({ id, data }) {
 
 function updateWidgets(thisWidgets) {
 	if (props.place === null) {
-		defaultStore.set('widgets', thisWidgets);
+		store.set('widgets', thisWidgets);
 		return;
 	}
 	if (props.place === 'left') {
-		defaultStore.set('widgets', [
+		store.set('widgets', [
 			...thisWidgets.map(w => ({ ...w, place: 'left' })),
-			...defaultStore.state.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)),
+			...store.state.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)),
 		]);
 		return;
 	}
-	defaultStore.set('widgets', [
-		...defaultStore.state.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)),
+	store.set('widgets', [
+		...store.state.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)),
 		...thisWidgets.map(w => ({ ...w, place: 'right' })),
 	]);
 }
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 8bcb260677..9ba9786ca8 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -70,15 +70,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, provide, ref, computed } from 'vue';
-import XCommon from './_common_/common.vue';
 import { instanceName } from '@@/js/config.js';
+import XCommon from './_common_/common.vue';
+import type { PageMetadata } from '@/scripts/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 { ColdDeviceStorage, defaultStore } from '@/store.js';
 import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
 import { mainRouter } from '@/router/main.js';
@@ -108,7 +107,7 @@ const announcements = {
 	limit: 10,
 };
 
-const isTimelineAvailable = ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable);
+const isTimelineAvailable = ref(instance.policies.ltlAvailable || instance.policies.gtlAvailable);
 
 const showMenu = ref(false);
 const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
@@ -116,10 +115,6 @@ const narrow = ref(window.innerWidth < 1280);
 
 const keymap = computed(() => {
 	return {
-		'd': () => {
-			if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
-			defaultStore.set('darkMode', !defaultStore.state.darkMode);
-		},
 		's': () => {
 			mainRouter.push('/search');
 		},
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index 89716575a9..6e56c9115e 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<div class="wbrkwalb">
 		<MkLoading v-if="fetching"/>
-		<TransitionGroup v-else tag="div" :name="defaultStore.state.animation ? 'chart' : ''" class="instances">
+		<TransitionGroup v-else tag="div" :name="prefer.s.animation ? 'chart' : ''" class="instances">
 			<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
 				<img :src="getInstanceIcon(instance)" alt=""/>
 				<div class="body">
@@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import type { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
 import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const name = 'federation';
 
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index 84ba05b5d3..00d0ec7fa5 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -59,7 +59,7 @@ 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 { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const name = 'jobQueue';
 
@@ -104,7 +104,7 @@ const prev = reactive({} as typeof current);
 const jammedAudioBuffer = ref<AudioBuffer | null>(null);
 const jammedSoundNodePlaying = ref<boolean>(false);
 
-if (defaultStore.state.sound_masterVolume) {
+if (prefer.s['sound.masterVolume']) {
 	sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => {
 		if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
 		jammedAudioBuffer.value = buf;
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index 65ab7a7075..4705de2016 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -21,7 +21,7 @@ import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import type { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'memo';
@@ -48,12 +48,12 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const text = ref<string | null>(defaultStore.state.memo);
+const text = ref<string | null>(store.state.memo);
 const changed = ref(false);
 let timeoutId;
 
 const saveMemo = () => {
-	defaultStore.set('memo', text.value);
+	store.set('memo', text.value);
 	changed.value = false;
 };
 
@@ -63,7 +63,7 @@ const onChange = () => {
 	timeoutId = window.setTimeout(saveMemo, 1000);
 };
 
-watch(() => defaultStore.reactiveState.memo, newText => {
+watch(() => store.reactiveState.memo, newText => {
 	text.value = newText.value;
 });
 
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 6d13ba09cc..6c7c82f229 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -31,7 +31,7 @@ import { useStream } from '@/stream.js';
 import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'photos';
@@ -70,7 +70,7 @@ const onDriveFileCreated = (file) => {
 };
 
 const thumbnail = (image: Misskey.entities.DriveFile): string => {
-	return defaultStore.state.disableShowingAnimatedImages
+	return prefer.s.disableShowingAnimatedImages
 		? getStaticImageUrl(image.url)
 		: image.thumbnailUrl ?? image.url;
 };
diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue
index 3354912c07..1e3ca74d40 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<div class="wbrkwala">
 		<MkLoading v-if="fetching"/>
-		<TransitionGroup v-else tag="div" :name="defaultStore.state.animation ? 'chart' : ''" class="tags">
+		<TransitionGroup v-else tag="div" :name="prefer.s.animation ? 'chart' : ''" class="tags">
 			<div v-for="stat in stats" :key="stat.tag">
 				<div class="tag">
 					<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
@@ -26,15 +26,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import type { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const name = 'hashtags';
 
diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts
index cf686efd0d..ffdc858b75 100644
--- a/packages/frontend/test/emoji.test.ts
+++ b/packages/frontend/test/emoji.test.ts
@@ -5,7 +5,7 @@
 
 import { describe, test, assert, afterEach } from 'vitest';
 import { render, cleanup, type RenderResult } from '@testing-library/vue';
-import { defaultStoreState } from './init.js';
+import { preferState } from './init.js';
 import { getEmojiName } from '@@/js/emojilist.js';
 import { components } from '@/components/index.js';
 import { directives } from '@/directives/index.js';
@@ -21,12 +21,12 @@ describe('Emoji', () => {
 
 	afterEach(() => {
 		cleanup();
-		defaultStoreState.emojiStyle = '';
+		preferState.emojiStyle = '';
 	});
 
 	describe('MkEmoji', () => {
 		test('Should render selector-less heart with color in native mode', async () => {
-			defaultStoreState.emojiStyle = 'native';
+			preferState.emojiStyle = 'native';
 			const mkEmoji = await renderEmoji('\u2764'); // monochrome heart
 			assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart
 			assert.ok(!mkEmoji.queryByText('\u2764'));
diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts
index 0cde571dcb..3b6b4d581b 100644
--- a/packages/frontend/test/init.ts
+++ b/packages/frontend/test/init.ts
@@ -17,7 +17,7 @@ updateI18n(locales['en-US']);
 // XXX: misskey-js panics if WebSocket is not defined
 vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
 
-export const defaultStoreState: Record<string, unknown> = {
+export const preferState: Record<string, unknown> = {
 
 	// なんかtestがうまいこと動かないのでここに書く
 	dataSaver: {
@@ -29,11 +29,11 @@ export const defaultStoreState: Record<string, unknown> = {
 
 };
 
-// XXX: defaultStore somehow becomes undefined in vitest?
-vi.mock('@/store.js', () => {
+// XXX: store somehow becomes undefined in vitest?
+vi.mock('@/preferences.js', () => {
 	return {
-		defaultStore: {
-			state: defaultStoreState,
+		prefer: {
+			s: preferState,
 		},
 	};
 });
-- 
cgit v1.2.3-freya


From dca42fd6e67b4b7b4fe97428e792cc299d2afaa8 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 16 Mar 2025 13:59:08 +0900
Subject: enhance(frontend): improve ux for touch devices

---
 packages/frontend-embed/src/pages/not-found.vue    |  4 +--
 packages/frontend/src/components/MkChannelList.vue |  4 +--
 packages/frontend/src/components/MkFormDialog.vue  |  2 +-
 .../frontend/src/components/MkImgWithBlurhash.vue  | 30 ++++++++++++++++++++--
 packages/frontend/src/components/MkNotes.vue       |  4 +--
 .../frontend/src/components/MkNotifications.vue    |  2 +-
 packages/frontend/src/components/MkPagination.vue  |  2 +-
 .../src/components/MkReactionsViewer.reaction.vue  |  2 +-
 packages/frontend/src/components/MkUserList.vue    |  4 +--
 .../frontend/src/components/global/MkAvatar.vue    |  2 ++
 .../src/components/global/MkCustomEmoji.vue        |  4 +++
 .../frontend/src/components/global/MkError.vue     |  2 +-
 packages/frontend/src/pages/_error_.vue            |  2 +-
 packages/frontend/src/pages/admin/roles.role.vue   |  2 +-
 packages/frontend/src/pages/drive.file.info.vue    |  2 +-
 packages/frontend/src/pages/favorites.vue          |  2 +-
 packages/frontend/src/pages/follow-requests.vue    |  2 +-
 packages/frontend/src/pages/invite.vue             |  4 +--
 packages/frontend/src/pages/list.vue               |  2 +-
 packages/frontend/src/pages/my-antennas/index.vue  |  2 +-
 packages/frontend/src/pages/my-lists/index.vue     |  2 +-
 packages/frontend/src/pages/not-found.vue          |  2 +-
 packages/frontend/src/pages/role.vue               |  8 +++---
 packages/frontend/src/pages/settings/apps.vue      |  2 +-
 .../frontend/src/pages/settings/mute-block.vue     |  6 ++---
 packages/frontend/src/style.scss                   |  5 ----
 packages/frontend/src/ui/classic.header.vue        |  2 +-
 packages/frontend/src/ui/classic.sidebar.vue       |  2 +-
 .../src/widgets/WidgetBirthdayFollowings.vue       |  2 +-
 packages/frontend/src/widgets/WidgetRss.vue        |  6 ++---
 30 files changed, 72 insertions(+), 45 deletions(-)

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

diff --git a/packages/frontend-embed/src/pages/not-found.vue b/packages/frontend-embed/src/pages/not-found.vue
index bbb03b4e64..061254a39a 100644
--- a/packages/frontend-embed/src/pages/not-found.vue
+++ b/packages/frontend-embed/src/pages/not-found.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<div class="_fullinfo">
-		<img :src="notFoundImageUrl" class="_ghost"/>
+		<img :src="notFoundImageUrl" draggable="false"/>
 		<div>{{ i18n.ts.notFoundDescription }}</div>
 	</div>
 </div>
@@ -20,5 +20,5 @@ import { i18n } from '@/i18n.js';
 
 const serverMetadata = inject(DI.serverMetadata)!;
 
-const notFoundImageUrl = computed(() => serverMetadata?.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
+const notFoundImageUrl = computed(() => serverMetadata.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
 </script>
diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue
index 23705f6ff8..fdb7d2a1c4 100644
--- a/packages/frontend/src/components/MkChannelList.vue
+++ b/packages/frontend/src/components/MkChannelList.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkPagination :pagination="pagination">
 	<template #empty>
 		<div class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img :src="infoImageUrl" draggable="false"/>
 			<div>{{ i18n.ts.notFound }}</div>
 		</div>
 	</template>
@@ -19,9 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import type { Paging } from '@/components/MkPagination.vue';
 import MkChannelPreview from '@/components/MkChannelPreview.vue';
 import MkPagination from '@/components/MkPagination.vue';
-import type { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
 
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 8fd16e13d4..df39f34816 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 		</div>
 		<div v-else class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img :src="infoImageUrl" draggable="false"/>
 			<div>{{ i18n.ts.nothing }}</div>
 		</div>
 	</MkSpacer>
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 37cbc5d06b..6585784e42 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -14,8 +14,34 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:enterToClass="prefer.s.animation && props.transition?.enterToClass || undefined"
 		:leaveFromClass="prefer.s.animation && props.transition?.leaveFromClass || undefined"
 	>
-		<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
-		<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
+		<canvas
+			v-show="hide"
+			key="canvas"
+			ref="canvas"
+			:class="$style.canvas"
+			:width="canvasWidth"
+			:height="canvasHeight"
+			:title="title ?? undefined"
+			draggable="false"
+			tabindex="-1"
+			style="-webkit-user-drag: none;"
+		/>
+		<img
+			v-show="!hide"
+			key="img"
+			ref="img"
+			:height="imgHeight ?? undefined"
+			:width="imgWidth ?? undefined"
+			:class="$style.img"
+			:src="src ?? undefined"
+			:title="title ?? undefined"
+			:alt="alt ?? undefined"
+			loading="eager"
+			decoding="async"
+			draggable="false"
+			tabindex="-1"
+			style="-webkit-user-drag: none;"
+		/>
 	</TransitionGroup>
 </div>
 </template>
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 344b7c4dd2..62d4350400 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
 	<template #empty>
 		<div class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img :src="infoImageUrl" draggable="false"/>
 			<div>{{ i18n.ts.noNotes }}</div>
 		</div>
 	</template>
@@ -33,10 +33,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { shallowRef } from 'vue';
+import type { Paging } from '@/components/MkPagination.vue';
 import MkNote from '@/components/MkNote.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkPagination from '@/components/MkPagination.vue';
-import type { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
 
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 4275262429..9f75a75fb6 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkPagination ref="pagingComponent" :pagination="pagination">
 		<template #empty>
 			<div class="_fullinfo">
-				<img :src="infoImageUrl" class="_ghost"/>
+				<img :src="infoImageUrl" draggable="false"/>
 				<div>{{ i18n.ts.noNotifications }}</div>
 			</div>
 		</template>
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 1253d7883f..071dbba5eb 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div v-else-if="empty" key="_empty_" class="empty">
 		<slot name="empty">
 			<div class="_fullinfo">
-				<img :src="infoImageUrl" class="_ghost"/>
+				<img :src="infoImageUrl" draggable="false"/>
 				<div>{{ i18n.ts.nothing }}</div>
 			</div>
 		</slot>
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 736bf87508..062894314d 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@click="toggleReaction()"
 	@contextmenu.prevent.stop="menu"
 >
-	<MkReactionIcon :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
+	<MkReactionIcon style="pointer-events: none;" :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
 	<span :class="$style.count">{{ count }}</span>
 </button>
 </template>
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index d1881ec3fc..0d1ffd715f 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkPagination :pagination="pagination">
 	<template #empty>
 		<div class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img :src="infoImageUrl" draggable="false"/>
 			<div>{{ i18n.ts.noUsers }}</div>
 		</div>
 	</template>
@@ -21,9 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import type { Paging } from '@/components/MkPagination.vue';
 import MkUserInfo from '@/components/MkUserInfo.vue';
 import MkPagination from '@/components/MkPagination.vue';
-import type { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
 
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index dc129b4c30..97c2069a2f 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -34,6 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				translate: getDecorationOffset(decoration),
 			}"
 			alt=""
+			draggable="false"
+			style="-webkit-user-drag: none;"
 		>
 	</template>
 </component>
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 20a07e9c28..65d6e9a3f6 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -9,6 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
 	src="/client-assets/dummy.png"
 	:title="alt"
+	draggable="false"
+	style="-webkit-user-drag: none;"
 />
 <span v-else-if="errored">:{{ customEmojiName }}:</span>
 <img
@@ -18,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:alt="alt"
 	:title="alt"
 	decoding="async"
+	draggable="false"
 	@error="errored = true"
 	@load="errored = false"
 	@click="onClick"
@@ -157,6 +160,7 @@ async function edit(name: string) {
 .root {
 	height: 2em;
 	vertical-align: middle;
+	-webkit-user-drag: none;
 	transition: transform 0.2s ease;
 
 	&:hover {
diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue
index b07e0775a3..95ed255189 100644
--- a/packages/frontend/src/components/global/MkError.vue
+++ b/packages/frontend/src/components/global/MkError.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
 	<div :class="$style.root">
-		<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
+		<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/>
 		<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
 		<MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton>
 	</div>
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index 066980db1f..791267f5ca 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkLoading v-if="!loaded"/>
 <Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
 	<div v-show="loaded" :class="$style.root">
-		<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
+		<img :src="serverErrorImageUrl" draggable="false" :class="$style.img"/>
 		<div class="_gaps">
 			<div><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></div>
 			<div v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</div>
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 43c3446b73..e7ebd30a3b 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkPagination :pagination="usersPagination">
 							<template #empty>
 								<div class="_fullinfo">
-									<img :src="infoImageUrl" class="_ghost"/>
+									<img :src="infoImageUrl" draggable="false"/>
 									<div>{{ i18n.ts.noUsers }}</div>
 								</div>
 							</template>
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 3f78935b0a..c2f57cb665 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<div v-else class="_fullinfo">
-		<img :src="infoImageUrl" class="_ghost"/>
+		<img :src="infoImageUrl" draggable="false"/>
 		<div>{{ i18n.ts.nothing }}</div>
 	</div>
 </div>
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index 548ed828f1..364d988924 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPagination :pagination="pagination">
 			<template #empty>
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img :src="infoImageUrl" draggable="false"/>
 					<div>{{ i18n.ts.noNotes }}</div>
 				</div>
 			</template>
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index b4e65303c1..f44d2364af 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkPagination ref="paginationComponent" :pagination="pagination">
 					<template #empty>
 						<div class="_fullinfo">
-							<img :src="infoImageUrl" class="_ghost"/>
+							<img :src="infoImageUrl" draggable="false"/>
 							<div>{{ i18n.ts.noFollowRequests }}</div>
 						</div>
 					</template>
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 352e1d9386..a6f0dfcf4b 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader/></template>
 	<MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
 		<div :class="$style.root">
-			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
+			<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/>
 			<div :class="$style.text">
 				<i class="ti ti-alert-triangle"></i>
 				{{ i18n.ts.nothing }}
@@ -36,12 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 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 { definePage } from '@/page.js';
 import { serverErrorImageUrl, instance } from '@/instance.js';
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 524db0da0b..0505fa7e08 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer v-if="error != null" :contentMax="1200">
 		<div :class="$style.root">
-			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
+			<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/>
 			<p :class="$style.text">
 				<i class="ti ti-alert-triangle"></i>
 				{{ i18n.ts.nothing }}
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index 03f4940e09..7ddaa68b7c 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div>
 			<div v-if="antennas.length === 0" class="empty">
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img :src="infoImageUrl" draggable="false"/>
 					<div>{{ i18n.ts.nothing }}</div>
 				</div>
 			</div>
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index cc701cb16b..69f91ad972 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div class="_gaps">
 			<div v-if="items.length === 0" class="empty">
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img :src="infoImageUrl" draggable="false"/>
 					<div>{{ i18n.ts.nothing }}</div>
 				</div>
 			</div>
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 3bba1159e9..684a3bb5bd 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<div class="_fullinfo">
-		<img :src="notFoundImageUrl" class="_ghost"/>
+		<img :src="notFoundImageUrl" draggable="false"/>
 		<div>{{ i18n.ts.notFoundDescription }}</div>
 	</div>
 </div>
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index 187b675346..38850c29f7 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
 	<MkSpacer v-if="error != null" :contentMax="1200">
 		<div :class="$style.root">
-			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
+			<img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/>
 			<p :class="$style.text">
 				<i class="ti ti-alert-triangle"></i>
 				{{ error }}
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="role">{{ role.description }}</div>
 			<MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/>
 			<div v-else-if="!visible" class="_fullinfo">
-				<img :src="infoImageUrl" class="_ghost"/>
+				<img :src="infoImageUrl" draggable="false"/>
 				<div>{{ i18n.ts.nothing }}</div>
 			</div>
 		</div>
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
 		<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/>
 		<div v-else-if="!visible" class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img :src="infoImageUrl" draggable="false"/>
 			<div>{{ i18n.ts.nothing }}</div>
 		</div>
 	</MkSpacer>
@@ -38,12 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { instanceName } from '@@/js/config.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import MkUserList from '@/components/MkUserList.vue';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import MkTimeline from '@/components/MkTimeline.vue';
-import { instanceName } from '@@/js/config.js';
 import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 626e14b427..c72179b9a1 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<FormPagination ref="list" :pagination="pagination">
 		<template #empty>
 			<div class="_fullinfo">
-				<img :src="infoImageUrl" class="_ghost"/>
+				<img :src="infoImageUrl" draggable="false"/>
 				<div>{{ i18n.ts.nothing }}</div>
 			</div>
 		</template>
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index ce762af071..f3d5e5090a 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkPagination :pagination="renoteMutingPagination">
 						<template #empty>
 							<div class="_fullinfo">
-								<img :src="infoImageUrl" class="_ghost"/>
+								<img :src="infoImageUrl" draggable="false"/>
 								<div>{{ i18n.ts.noUsers }}</div>
 							</div>
 						</template>
@@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkPagination :pagination="mutingPagination">
 						<template #empty>
 							<div class="_fullinfo">
-								<img :src="infoImageUrl" class="_ghost"/>
+								<img :src="infoImageUrl" draggable="false"/>
 								<div>{{ i18n.ts.noUsers }}</div>
 							</div>
 						</template>
@@ -146,7 +146,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkPagination :pagination="blockingPagination">
 						<template #empty>
 							<div class="_fullinfo">
-								<img :src="infoImageUrl" class="_ghost"/>
+								<img :src="infoImageUrl" draggable="false"/>
 								<div>{{ i18n.ts.noUsers }}</div>
 							</div>
 						</template>
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 43aff3012e..43a97d49eb 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -214,11 +214,6 @@ rt {
 	text-overflow: ellipsis;
 }
 
-._ghost {
-	@extend ._noSelect;
-	pointer-events: none;
-}
-
 ._modalBg {
 	position: fixed;
 	top: 0;
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index 1e8a342977..7d4235bd4e 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="body">
 		<div class="left">
 			<button v-click-anime class="item _button instance" @click="openInstanceMenu">
-				<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/>
+				<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" draggable="false"/>
 			</button>
 			<MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact>
 				<i class="ti ti-home ti-fw"></i>
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 096ea0d4cf..8200b6ad38 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="divider"></div>
 	<div class="about">
 		<button v-click-anime class="item _button" @click="openInstanceMenu">
-			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/>
+			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" draggable="false"/>
 		</button>
 	</div>
 	<!--<MisskeyLogo class="misskey"/>-->
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index be11a26917..6fe743aed2 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
 		</div>
 		<div v-else :class="$style.bdayFFallback">
-			<img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/>
+			<img :src="infoImageUrl" draggable="false" :class="$style.bdayFFallbackImage"/>
 			<div>{{ i18n.ts.nothing }}</div>
 		</div>
 	</div>
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 82dfeacff1..a7213f4c21 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="ekmkgxbj">
 		<MkLoading v-if="fetching"/>
 		<div v-else-if="(!items || items.length === 0) && widgetProps.showHeader" class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img :src="infoImageUrl" draggable="false"/>
 			<div>{{ i18n.ts.nothing }}</div>
 		</div>
 		<div v-else :class="$style.feed">
@@ -25,13 +25,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, watch, computed } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url as base } from '@@/js/config.js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { url as base } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { infoImageUrl } from '@/instance.js';
 
 const name = 'rss';
-- 
cgit v1.2.3-freya


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

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

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

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


From 6015254e59ba0526efbfa139c89546458663ccbd Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 20 Mar 2025 19:00:09 +0900
Subject: lint fixes

---
 packages/frontend/.storybook/fake-utils.ts         |  2 +-
 packages/frontend/.storybook/preview.ts            |  6 ++---
 packages/frontend/src/boot/common.ts               | 30 +++++++++++-----------
 packages/frontend/src/boot/main-boot.ts            |  6 ++---
 packages/frontend/src/components/MkAnalogClock.vue |  2 +-
 .../frontend/src/components/MkAutocomplete.vue     |  4 +--
 packages/frontend/src/components/MkButton.vue      |  2 +-
 packages/frontend/src/components/MkCaptcha.vue     |  6 ++---
 packages/frontend/src/components/MkContextMenu.vue |  4 +--
 .../frontend/src/components/MkCropperDialog.vue    |  2 +-
 packages/frontend/src/components/MkFolder.vue      |  2 +-
 .../frontend/src/components/MkImgWithBlurhash.vue  |  4 +--
 .../frontend/src/components/MkInstanceStats.vue    |  2 +-
 packages/frontend/src/components/MkMediaAudio.vue  |  2 +-
 packages/frontend/src/components/MkMediaList.vue   |  6 ++---
 packages/frontend/src/components/MkMediaVideo.vue  |  8 +++---
 packages/frontend/src/components/MkMenu.vue        | 16 ++++++------
 packages/frontend/src/components/MkMiniChart.vue   |  2 +-
 .../frontend/src/components/MkNotifications.vue    |  2 +-
 packages/frontend/src/components/MkPagination.vue  |  2 +-
 packages/frontend/src/components/MkRange.vue       |  8 +++---
 .../src/components/MkReactionsViewer.reaction.vue  |  2 +-
 .../src/components/MkRetentionLineChart.vue        |  2 +-
 packages/frontend/src/components/MkTagCloud.vue    |  4 +--
 .../MkVisitorDashboard.ActiveUsersChart.vue        |  2 +-
 packages/frontend/src/components/MkWindow.vue      |  2 +-
 .../src/components/global/MkPageHeader.tabs.vue    |  2 +-
 .../src/components/global/MkPageHeader.vue         |  4 +--
 .../frontend/src/components/global/RouterView.vue  |  6 ++---
 .../src/components/global/SearchMarker.vue         |  2 +-
 packages/frontend/src/directives/hotkey.ts         |  4 +--
 packages/frontend/src/directives/panel.ts          |  2 +-
 packages/frontend/src/directives/tooltip.ts        |  2 +-
 packages/frontend/src/directives/user-preview.ts   |  4 +--
 packages/frontend/src/instance.ts                  |  2 +-
 packages/frontend/src/os.ts                        |  4 +--
 packages/frontend/src/pages/admin/_header_.vue     |  2 +-
 packages/frontend/src/pages/admin/overview.pie.vue |  2 +-
 packages/frontend/src/pages/auth.form.vue          |  2 +-
 .../frontend/src/pages/drop-and-fusion.game.vue    |  2 +-
 packages/frontend/src/pages/oauth.vue              | 18 ++++++-------
 .../frontend/src/pages/settings/2fa.qrdialog.vue   |  2 +-
 packages/frontend/src/preferences.ts               |  6 ++---
 packages/frontend/src/preferences/utility.ts       |  4 +--
 packages/frontend/src/server-context.ts            |  2 +-
 packages/frontend/src/stream.ts                    |  6 ++---
 packages/frontend/src/theme.ts                     | 12 ++++-----
 packages/frontend/src/ui/_common_/common.vue       |  2 +-
 packages/frontend/src/ui/_common_/navbar.vue       |  4 +--
 packages/frontend/src/ui/classic.vue               |  6 ++---
 packages/frontend/src/ui/deck.vue                  |  4 +--
 packages/frontend/src/ui/minimum.vue               |  4 +--
 packages/frontend/src/ui/universal.vue             | 12 ++++-----
 packages/frontend/src/ui/visitor.vue               |  4 +--
 packages/frontend/src/ui/zen.vue                   |  4 +--
 packages/frontend/src/use/use-note-capture.ts      |  2 +-
 packages/frontend/src/use/use-tooltip.ts           |  4 +--
 packages/frontend/src/utility/focus-trap.ts        |  4 +--
 packages/frontend/src/utility/focus.ts             |  4 +--
 packages/frontend/src/utility/fullscreen.ts        |  4 +--
 packages/frontend/src/utility/hotkey.ts            |  6 ++---
 packages/frontend/src/utility/init-chart.ts        |  2 +-
 packages/frontend/src/utility/physics.ts           |  2 +-
 packages/frontend/src/utility/select-file.ts       |  2 +-
 packages/frontend/src/utility/snowfall-effect.ts   |  6 ++---
 packages/frontend/src/utility/sound.ts             |  4 +--
 packages/frontend/src/utility/sticky-sidebar.ts    |  2 +-
 .../frontend/src/utility/upload/isWebpSupported.ts |  2 +-
 packages/frontend/src/widgets/WidgetRss.vue        |  2 +-
 packages/frontend/src/widgets/WidgetRssTicker.vue  |  2 +-
 packages/frontend/test/scroll.test.ts              | 12 ++++-----
 71 files changed, 160 insertions(+), 160 deletions(-)

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

diff --git a/packages/frontend/.storybook/fake-utils.ts b/packages/frontend/.storybook/fake-utils.ts
index c777cbbe72..44e2263ca0 100644
--- a/packages/frontend/.storybook/fake-utils.ts
+++ b/packages/frontend/.storybook/fake-utils.ts
@@ -131,7 +131,7 @@ export function imageDataUrl(options?: {
 		alpha?: number,
 	}
 }, seed?: string): string {
-	const canvas = document.createElement('canvas');
+	const canvas = window.document.createElement('canvas');
 	canvas.width = options?.size?.width ?? 100;
 	canvas.height = options?.size?.height ?? 100;
 
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index 3f6bfb62ff..269bc4fb9a 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -23,9 +23,9 @@ let misskeyOS = null;
 
 function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
 	unobserve();
-	const theme = themes[document.documentElement.dataset.misskeyTheme];
+	const theme = themes[window.document.documentElement.dataset.misskeyTheme];
 	if (theme) {
-		applyTheme(themes[document.documentElement.dataset.misskeyTheme]);
+		applyTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
 	} else {
 		applyTheme(themes['l-light']);
 	}
@@ -42,7 +42,7 @@ function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
 			}
 		}
 	});
-	observer.observe(document.documentElement, {
+	observer.observe(window.document.documentElement, {
 		attributes: true,
 		attributeFilter: ['data-misskey-theme'],
 	});
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 75b74b41f8..9a505ca9f8 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -95,7 +95,7 @@ export async function common(createVue: () => App<Element>) {
 	//#endregion
 
 	// タッチデバイスでCSSの:hoverを機能させる
-	document.addEventListener('touchend', () => {}, { passive: true });
+	window.document.addEventListener('touchend', () => {}, { passive: true });
 
 	// URLに#pswpを含む場合は取り除く
 	if (window.location.hash === '#pswp') {
@@ -110,13 +110,13 @@ export async function common(createVue: () => App<Element>) {
 
 	// If mobile, insert the viewport meta tag
 	if (['smartphone', 'tablet'].includes(deviceKind)) {
-		const viewport = document.getElementsByName('viewport').item(0);
+		const viewport = window.document.getElementsByName('viewport').item(0);
 		viewport.setAttribute('content',
 			`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
 	}
 
 	//#region Set lang attr
-	const html = document.documentElement;
+	const html = window.document.documentElement;
 	html.setAttribute('lang', lang);
 	//#endregion
 
@@ -155,7 +155,7 @@ export async function common(createVue: () => App<Element>) {
 		);
 	}, { immediate: miLocalStorage.getItem('theme') == null });
 
-	document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
+	window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
 
 	const darkTheme = prefer.model('darkTheme');
 	const lightTheme = prefer.model('lightTheme');
@@ -201,20 +201,20 @@ export async function common(createVue: () => App<Element>) {
 	}, { immediate: true });
 
 	watch(prefer.r.useBlurEffectForModal, v => {
-		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
+		window.document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
 	}, { immediate: true });
 
 	watch(prefer.r.useBlurEffect, v => {
 		if (v) {
-			document.documentElement.style.removeProperty('--MI-blur');
+			window.document.documentElement.style.removeProperty('--MI-blur');
 		} else {
-			document.documentElement.style.setProperty('--MI-blur', 'none');
+			window.document.documentElement.style.setProperty('--MI-blur', 'none');
 		}
 	}, { immediate: true });
 
 	// Keep screen on
-	const onVisibilityChange = () => document.addEventListener('visibilitychange', () => {
-		if (document.visibilityState === 'visible') {
+	const onVisibilityChange = () => window.document.addEventListener('visibilitychange', () => {
+		if (window.document.visibilityState === 'visible') {
 			navigator.wakeLock.request('screen');
 		}
 	});
@@ -224,7 +224,7 @@ export async function common(createVue: () => App<Element>) {
 			.catch(() => {
 				// On WebKit-based browsers, user activation is required to send wake lock request
 				// https://webkit.org/blog/13862/the-user-activation-api/
-				document.addEventListener(
+				window.document.addEventListener(
 					'click',
 					() => navigator.wakeLock.request('screen').then(onVisibilityChange),
 					{ once: true },
@@ -233,7 +233,7 @@ export async function common(createVue: () => App<Element>) {
 	}
 
 	if (prefer.s.makeEveryTextElementsSelectable) {
-		document.documentElement.classList.add('forceSelectableAll');
+		window.document.documentElement.classList.add('forceSelectableAll');
 	}
 
 	//#region Fetch user
@@ -278,16 +278,16 @@ export async function common(createVue: () => App<Element>) {
 	const rootEl = ((): HTMLElement => {
 		const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
 
-		const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
+		const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
 
 		if (currentRoot) {
 			console.warn('multiple import detected');
 			return currentRoot;
 		}
 
-		const root = document.createElement('div');
+		const root = window.document.createElement('div');
 		root.id = MISSKEY_MOUNT_DIV_ID;
-		document.body.appendChild(root);
+		window.document.body.appendChild(root);
 		return root;
 	})();
 
@@ -330,7 +330,7 @@ export async function common(createVue: () => App<Element>) {
 }
 
 function removeSplash() {
-	const splash = document.getElementById('splash');
+	const splash = window.document.getElementById('splash');
 	if (splash) {
 		splash.style.opacity = '0';
 		splash.style.pointerEvents = 'none';
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index f7360a7340..134490e317 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -398,7 +398,7 @@ export async function mainBoot() {
 			let lastVisibilityChangedAt = Date.now();
 
 			function claimPlainLucky() {
-				if (document.visibilityState !== 'visible') {
+				if (window.document.visibilityState !== 'visible') {
 					if (justPlainLuckyTimer != null) window.clearTimeout(justPlainLuckyTimer);
 					return;
 				}
@@ -413,7 +413,7 @@ export async function mainBoot() {
 			window.addEventListener('visibilitychange', () => {
 				const now = Date.now();
 
-				if (document.visibilityState === 'visible') {
+				if (window.document.visibilityState === 'visible') {
 					// タブを高速で切り替えたら取得処理が何度も走るのを防ぐ
 					if ((now - lastVisibilityChangedAt) < 1000 * 10) {
 						justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10);
@@ -554,7 +554,7 @@ export async function mainBoot() {
 			mainRouter.push('/search');
 		},
 	} as const satisfies Keymap;
-	document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
+	window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
 
 	initializeSw();
 }
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index b39bca5b27..eac1ea9534 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -192,7 +192,7 @@ function tick() {
 tick();
 
 function calcColors() {
-	const computedStyle = getComputedStyle(document.documentElement);
+	const computedStyle = getComputedStyle(window.document.documentElement);
 	const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
 	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 	majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 5b747a2a1a..03cf107231 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -359,7 +359,7 @@ onMounted(() => {
 
 	props.textarea.addEventListener('keydown', onKeydown);
 
-	document.body.addEventListener('mousedown', onMousedown);
+	window.document.body.addEventListener('mousedown', onMousedown);
 
 	nextTick(() => {
 		exec();
@@ -375,7 +375,7 @@ onMounted(() => {
 onBeforeUnmount(() => {
 	props.textarea.removeEventListener('keydown', onKeydown);
 
-	document.body.removeEventListener('mousedown', onMousedown);
+	window.document.body.removeEventListener('mousedown', onMousedown);
 });
 </script>
 
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index c81edc2a73..5e89dfba12 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -92,7 +92,7 @@ function onMousedown(evt: MouseEvent): void {
 	const target = evt.target! as HTMLElement;
 	const rect = target.getBoundingClientRect();
 
-	const ripple = document.createElement('div');
+	const ripple = window.document.createElement('div');
 	ripple.classList.add(ripples.value!.dataset.childrenClass!);
 	ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
 	ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index e4f953bda8..30940a34a9 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -112,7 +112,7 @@ watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => {
 if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
 	available.value = true;
 } else if (src.value !== null) {
-	(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
+	(window.document.getElementById(scriptId.value) ?? window.document.head.appendChild(Object.assign(window.document.createElement('script'), {
 		async: true,
 		id: scriptId.value,
 		src: src.value,
@@ -149,7 +149,7 @@ async function requestRender() {
 	if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
 		// reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する.
 		// （同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので）
-		const elem = document.createElement('div');
+		const elem = window.document.createElement('div');
 		captchaEl.value.appendChild(elem);
 
 		captchaWidgetId.value = captcha.value.render(elem, {
@@ -174,7 +174,7 @@ async function requestRender() {
 
 function clearWidget() {
 	if (props.provider === 'mcaptcha') {
-		const container = document.getElementById('mcaptcha__widget-container');
+		const container = window.document.getElementById('mcaptcha__widget-container');
 		if (container) {
 			container.innerHTML = '';
 		}
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index e47dba4bae..9c6397a72c 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -68,11 +68,11 @@ onMounted(() => {
 		rootEl.value.style.left = `${left}px`;
 	}
 
-	document.body.addEventListener('mousedown', onMousedown);
+	window.document.body.addEventListener('mousedown', onMousedown);
 });
 
 onBeforeUnmount(() => {
-	document.body.removeEventListener('mousedown', onMousedown);
+	window.document.body.removeEventListener('mousedown', onMousedown);
 });
 
 function onMousedown(evt: Event) {
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index bd2e6a9cbc..ba21394cbc 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -122,7 +122,7 @@ onMounted(() => {
 	cropper = new Cropper(imgEl.value!, {
 	});
 
-	const computedStyle = getComputedStyle(document.documentElement);
+	const computedStyle = getComputedStyle(window.document.documentElement);
 
 	const selection = cropper.getCropperSelection()!;
 	selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index b13972f66d..afa09e3125 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -116,7 +116,7 @@ function toggle() {
 }
 
 onMounted(() => {
-	const computedStyle = getComputedStyle(document.documentElement);
+	const computedStyle = getComputedStyle(window.document.documentElement);
 	const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
 	const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
 	bgSame.value = parentBg === myBg;
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 420e5e392f..e3a0a371b4 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -55,7 +55,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
 const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
 	// テスト環境で Web Worker インスタンスは作成できない
 	if (import.meta.env.MODE === 'test') {
-		const canvas = document.createElement('canvas');
+		const canvas = window.document.createElement('canvas');
 		canvas.width = 64;
 		canvas.height = 64;
 		resolve(canvas);
@@ -70,7 +70,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
 			);
 			resolve(workers);
 		} else {
-			const canvas = document.createElement('canvas');
+			const canvas = window.document.createElement('canvas');
 			canvas.width = 64;
 			canvas.height = 64;
 			resolve(canvas);
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 76bec9bc66..90391005bc 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -126,7 +126,7 @@ function createDoughnut(chartEl, tooltip, data) {
 			labels: data.map(x => x.name),
 			datasets: [{
 				backgroundColor: data.map(x => x.color),
-				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
+				borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
 				borderWidth: 2,
 				hoverOffset: 0,
 				data: data.map(x => x.value),
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index db6deb161a..b7052ad918 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -148,7 +148,7 @@ const keymap = {
 // PlayerElもしくはその子要素にフォーカスがあるかどうか
 function hasFocus() {
 	if (!playerEl.value) return false;
-	return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
+	return playerEl.value === window.document.activeElement || playerEl.value.contains(window.document.activeElement);
 }
 
 const playerEl = useTemplateRef('playerEl');
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 44a9d14a46..ae15776041 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -48,7 +48,7 @@ const props = defineProps<{
 
 const gallery = useTemplateRef('gallery');
 const pswpZIndex = os.claimZIndex('middle');
-document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
+window.document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
 const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
 let lightbox: PhotoSwipeLightbox | null = null;
 
@@ -166,7 +166,7 @@ onMounted(() => {
 			className: 'pswp__alt-text-container',
 			appendTo: 'wrapper',
 			onInit: (el, pswp) => {
-				const textBox = document.createElement('p');
+				const textBox = window.document.createElement('p');
 				textBox.className = 'pswp__alt-text _acrylic';
 				el.appendChild(textBox);
 
@@ -178,7 +178,7 @@ onMounted(() => {
 	});
 
 	lightbox.on('afterInit', () => {
-		activeEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
+		activeEl = window.document.activeElement instanceof HTMLElement ? window.document.activeElement : null;
 		focusParent(activeEl, true, true);
 		lightbox?.pswp?.element?.focus({
 			preventScroll: true,
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 5475e703a5..629679a971 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -171,7 +171,7 @@ const keymap = {
 // PlayerElもしくはその子要素にフォーカスがあるかどうか
 function hasFocus() {
 	if (!playerEl.value) return false;
-	return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
+	return playerEl.value === window.document.activeElement || playerEl.value.contains(window.document.activeElement);
 }
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
@@ -216,7 +216,7 @@ function showMenu(ev: MouseEvent) {
 				'2.0x': 2,
 			},
 		},
-		...(document.pictureInPictureEnabled ? [{
+		...(window.document.pictureInPictureEnabled ? [{
 			text: i18n.ts._mediaControls.pip,
 			icon: 'ti ti-picture-in-picture',
 			action: togglePictureInPicture,
@@ -384,8 +384,8 @@ function toggleFullscreen() {
 
 function togglePictureInPicture() {
 	if (videoEl.value) {
-		if (document.pictureInPictureElement) {
-			document.exitPictureInPicture();
+		if (window.document.pictureInPictureElement) {
+			window.document.exitPictureInPicture();
 		} else {
 			videoEl.value.requestPictureInPicture();
 		}
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index e380d0bc37..a84bd9b256 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -358,10 +358,10 @@ function switchItem(item: MenuSwitch & { ref: any }) {
 
 function focusUp() {
 	if (disposed) return;
-	if (!itemsEl.value?.contains(document.activeElement)) return;
+	if (!itemsEl.value?.contains(window.document.activeElement)) return;
 
 	const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
-	const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
+	const activeIndex = focusableElements.findIndex(el => el === window.document.activeElement);
 	const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1);
 	const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
 
@@ -370,10 +370,10 @@ function focusUp() {
 
 function focusDown() {
 	if (disposed) return;
-	if (!itemsEl.value?.contains(document.activeElement)) return;
+	if (!itemsEl.value?.contains(window.document.activeElement)) return;
 
 	const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
-	const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
+	const activeIndex = focusableElements.findIndex(el => el === window.document.activeElement);
 	const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0;
 	const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
 
@@ -400,9 +400,9 @@ const onGlobalMousedown = (ev: MouseEvent) => {
 
 const setupHandlers = () => {
 	if (!isNestingMenu) {
-		document.addEventListener('focusin', onGlobalFocusin, { passive: true });
+		window.document.addEventListener('focusin', onGlobalFocusin, { passive: true });
 	}
-	document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
+	window.document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
 };
 
 let disposed = false;
@@ -410,9 +410,9 @@ let disposed = false;
 const disposeHandlers = () => {
 	disposed = true;
 	if (!isNestingMenu) {
-		document.removeEventListener('focusin', onGlobalFocusin);
+		window.document.removeEventListener('focusin', onGlobalFocusin);
 	}
-	document.removeEventListener('mousedown', onGlobalMousedown);
+	window.document.removeEventListener('mousedown', onGlobalMousedown);
 };
 
 onMounted(() => {
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index 7ea585ecc2..98bd471438 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -48,7 +48,7 @@ const polygonPoints = ref('');
 const headX = ref<number | null>(null);
 const headY = ref<number | null>(null);
 const clock = ref<number | null>(null);
-const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
+const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
 const color = accent.toRgbString();
 
 function draw(): void {
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 08fc846327..21f1967bfa 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -59,7 +59,7 @@ const pagination = computed(() => prefer.r.useGroupedNotifications.value ? {
 
 function onNotification(notification) {
 	const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
-	if (isMuted || document.visibilityState === 'visible') {
+	if (isMuted || window.document.visibilityState === 'visible') {
 		useStream().send('readNotification');
 	}
 
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 6a1a91a9f4..ab8bda403b 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -142,7 +142,7 @@ const {
 } = prefer.r;
 
 const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value);
-const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body);
+const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : window.document.body);
 
 const visibility = useDocumentVisibility();
 
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 118dbbe15a..734b624541 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -151,9 +151,9 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 		closed: () => dispose(),
 	});
 
-	const style = document.createElement('style');
-	style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
-	document.head.appendChild(style);
+	const style = window.document.createElement('style');
+	style.appendChild(window.document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
+	window.document.head.appendChild(style);
 
 	const thumbWidth = getThumbWidth();
 
@@ -172,7 +172,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 	let beforeValue = finalValue.value;
 
 	const onMouseup = () => {
-		document.head.removeChild(style);
+		window.document.head.removeChild(style);
 		tooltipForDragShowing.value = false;
 		window.removeEventListener('mousemove', onDrag);
 		window.removeEventListener('touchmove', onDrag);
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 590b0f6f19..9d941a949a 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -136,7 +136,7 @@ async function menu(ev) {
 }
 
 function anime() {
-	if (document.hidden || !prefer.s.animation || buttonEl.value == null) return;
+	if (window.document.hidden || !prefer.s.animation || buttonEl.value == null) return;
 
 	const rect = buttonEl.value.getBoundingClientRect();
 	const x = rect.left + 16;
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index 60b3f17b14..ba66ffecc0 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -44,7 +44,7 @@ onMounted(async () => {
 
 	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
-	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
+	const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
 	const color = accent.toHex();
 
 	if (chartEl.value == null) return;
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index 8ad9e14015..9d541c8acb 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -20,7 +20,7 @@ import tinycolor from 'tinycolor2';
 
 const loaded = !!window.TagCanvas;
 const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
-const computedStyle = getComputedStyle(document.documentElement);
+const computedStyle = getComputedStyle(window.document.documentElement);
 const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
 const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
 const available = ref(false);
@@ -57,7 +57,7 @@ onMounted(() => {
 	if (loaded) {
 		available.value = true;
 	} else {
-		document.head.appendChild(Object.assign(document.createElement('script'), {
+		window.document.head.appendChild(Object.assign(window.document.createElement('script'), {
 			async: true,
 			src: '/client-assets/tagcanvas.min.js',
 		})).addEventListener('load', () => available.value = true);
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index 2c2c515032..79c9e739c4 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -61,7 +61,7 @@ async function renderChart() {
 
 	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
-	const computedStyle = getComputedStyle(document.documentElement);
+	const computedStyle = getComputedStyle(window.document.documentElement);
 	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 
 	const colorRead = accent;
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 98ec448a8e..e5ac791d0b 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -240,7 +240,7 @@ function onHeaderMousedown(evt: MouseEvent | TouchEvent) {
 	const main = rootEl.value;
 	if (main == null) return;
 
-	if (!contains(main, document.activeElement)) main.focus();
+	if (!contains(main, window.document.activeElement)) main.focus();
 
 	const position = main.getBoundingClientRect();
 
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 81adc07f26..358a17d3e8 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -170,7 +170,7 @@ onMounted(() => {
 
 	if (props.rootEl) {
 		ro2 = new ResizeObserver((entries, observer) => {
-			if (document.body.contains(el.value as HTMLElement)) {
+			if (window.document.body.contains(el.value as HTMLElement)) {
 				nextTick(() => renderTab());
 			}
 		});
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 59bf80cfca..d5680b8413 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -108,7 +108,7 @@ function onTabClick(): void {
 
 const calcBg = () => {
 	const rawBg = 'var(--MI_THEME-bg)';
-	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 	tinyBg.setAlpha(0.85);
 	bg.value = tinyBg.toRgbString();
 };
@@ -122,7 +122,7 @@ onMounted(() => {
 	if (el.value && el.value.parentElement) {
 		narrow.value = el.value.parentElement.offsetWidth < 500;
 		ro = new ResizeObserver((entries, observer) => {
-			if (el.value && el.value.parentElement && document.body.contains(el.value as HTMLElement)) {
+			if (el.value && el.value.parentElement && window.document.body.contains(el.value as HTMLElement)) {
 				narrow.value = el.value.parentElement.offsetWidth < 500;
 			}
 		});
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 45cb1e3bd5..1c0c35f34e 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -48,7 +48,7 @@ onMounted(() => {
 });
 
 // view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成
-const viewTransitionStylesTag = document.createElement('style');
+const viewTransitionStylesTag = window.document.createElement('style');
 viewTransitionStylesTag.textContent = `
 @keyframes ${viewId}-old {
 	to { transform: scale(0.95); opacity: 0; }
@@ -89,8 +89,8 @@ router.useListener('change', ({ resolved }) => {
 	}
 
 	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	if (prefer.s.animation && document.startViewTransition) {
-		document.startViewTransition(() => new Promise((res) => {
+	if (prefer.s.animation && window.document.startViewTransition) {
+		window.document.startViewTransition(() => new Promise((res) => {
 			_();
 			nextTick(() => {
 				res();
diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue
index 66a78cb7fd..061ce3f47d 100644
--- a/packages/frontend/src/components/global/SearchMarker.vue
+++ b/packages/frontend/src/components/global/SearchMarker.vue
@@ -42,7 +42,7 @@ const highlighted = ref(props.markerId === searchMarkerId.value);
 
 function checkChildren() {
 	if (props.children?.includes(searchMarkerId.value)) {
-		const el = document.querySelector(`[data-in-app-search-marker-id="${searchMarkerId.value}"]`);
+		const el = window.document.querySelector(`[data-in-app-search-marker-id="${searchMarkerId.value}"]`);
 		highlighted.value = el == null;
 	}
 }
diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts
index 75e022e98f..63637ab2ba 100644
--- a/packages/frontend/src/directives/hotkey.ts
+++ b/packages/frontend/src/directives/hotkey.ts
@@ -13,7 +13,7 @@ export default {
 		el._keyHandler = makeHotkey(binding.value);
 
 		if (el._hotkey_global) {
-			document.addEventListener('keydown', el._keyHandler, { passive: false });
+			window.document.addEventListener('keydown', el._keyHandler, { passive: false });
 		} else {
 			el.addEventListener('keydown', el._keyHandler, { passive: false });
 		}
@@ -21,7 +21,7 @@ export default {
 
 	unmounted(el) {
 		if (el._hotkey_global) {
-			document.removeEventListener('keydown', el._keyHandler);
+			window.document.removeEventListener('keydown', el._keyHandler);
 		} else {
 			el.removeEventListener('keydown', el._keyHandler);
 		}
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
index 17916fb6d3..0af19e6ca3 100644
--- a/packages/frontend/src/directives/panel.ts
+++ b/packages/frontend/src/directives/panel.ts
@@ -10,7 +10,7 @@ export default {
 	mounted(src, binding, vn) {
 		const parentBg = getBgColor(src.parentElement) ?? 'transparent';
 
-		const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');
+		const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel');
 
 		if (parentBg === myBg) {
 			src.style.backgroundColor = 'var(--MI_THEME-bg)';
diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts
index 068186dfa0..750acd0588 100644
--- a/packages/frontend/src/directives/tooltip.ts
+++ b/packages/frontend/src/directives/tooltip.ts
@@ -47,7 +47,7 @@ export default {
 		}
 
 		self.show = () => {
-			if (!document.body.contains(el)) return;
+			if (!window.document.body.contains(el)) return;
 			if (self._close) return;
 			if (self.text == null) return;
 
diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts
index 43a93a0865..94deea82c7 100644
--- a/packages/frontend/src/directives/user-preview.ts
+++ b/packages/frontend/src/directives/user-preview.ts
@@ -31,7 +31,7 @@ export class UserPreview {
 	}
 
 	private show() {
-		if (!document.body.contains(this.el)) return;
+		if (!window.document.body.contains(this.el)) return;
 		if (this.promise) return;
 
 		const showing = ref(true);
@@ -58,7 +58,7 @@ export class UserPreview {
 		};
 
 		this.checkTimer = window.setInterval(() => {
-			if (!document.body.contains(this.el)) {
+			if (!window.document.body.contains(this.el)) {
 				window.clearTimeout(this.showTimer);
 				window.clearTimeout(this.hideTimer);
 				this.close();
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index c694d49c8a..e75e3dfd34 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -12,7 +12,7 @@ import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERR
 // TODO: 他のタブと永続化されたstateを同期
 
 //#region loader
-const providedMetaEl = document.getElementById('misskey_meta');
+const providedMetaEl = window.document.getElementById('misskey_meta');
 
 let cachedMeta = miLocalStorage.getItem('instance') ? JSON.parse(miLocalStorage.getItem('instance')!) : null;
 let cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index eed929432c..7f4f7c5be3 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -675,7 +675,7 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n
 		src = null;
 	}
 
-	let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(document.activeElement);
+	let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(window.document.activeElement);
 	return new Promise(resolve => nextTick(() => {
 		const { dispose } = popup(MkPopupMenu, {
 			items,
@@ -704,7 +704,7 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
 		return Promise.resolve();
 	}
 
-	let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement);
+	let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(window.document.activeElement);
 	ev.preventDefault();
 	return new Promise(resolve => nextTick(() => {
 		const { dispose } = popup(MkContextMenu, {
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index 819ca2c127..0af56e1b15 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -120,7 +120,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
 
 const calcBg = () => {
 	const rawBg = pageMetadata.value.bg ?? 'var(--MI_THEME-bg)';
-	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 	tinyBg.setAlpha(0.85);
 	bg.value = tinyBg.toRgbString();
 };
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index 32dd981ca9..86c5eff4da 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -41,7 +41,7 @@ onMounted(() => {
 			labels: props.data.map(x => x.name),
 			datasets: [{
 				backgroundColor: props.data.map(x => x.color),
-				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
+				borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
 				borderWidth: 2,
 				hoverOffset: 0,
 				data: props.data.map(x => x.value),
diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue
index 1917293c06..5b1fd1a386 100644
--- a/packages/frontend/src/pages/auth.form.vue
+++ b/packages/frontend/src/pages/auth.form.vue
@@ -38,7 +38,7 @@ const emit = defineEmits<{
 const app = computed(() => props.session.app);
 
 const name = computed(() => {
-	const el = document.createElement('div');
+	const el = window.document.createElement('div');
 	el.textContent = app.value.name;
 	return el.innerHTML;
 });
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index 3b1a845f5b..b8b0d6aef6 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -875,7 +875,7 @@ function loadImage(url: string) {
 
 function getGameImageDriveFile() {
 	return new Promise<Misskey.entities.DriveFile | null>(res => {
-		const dcanvas = document.createElement('canvas');
+		const dcanvas = window.document.createElement('canvas');
 		dcanvas.width = game.GAME_WIDTH;
 		dcanvas.height = game.GAME_HEIGHT;
 		const ctx = dcanvas.getContext('2d');
diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue
index 49664c8b08..04bb1661cf 100644
--- a/packages/frontend/src/pages/oauth.vue
+++ b/packages/frontend/src/pages/oauth.vue
@@ -27,42 +27,42 @@ import MkPageWithAnimBg from '@/components/MkPageWithAnimBg.vue';
 import { definePage } from '@/page.js';
 import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
 
-const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
+const transactionIdMeta = window.document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
 if (transactionIdMeta) {
 	transactionIdMeta.remove();
 }
 
-const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content;
-const logo = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-logo"]')?.content;
-const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) ?? [];
+const name = window.document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content;
+const logo = window.document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-logo"]')?.content;
+const permissions = window.document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) ?? [];
 
 function doPost(token: string, decision: 'accept' | 'deny') {
-	const form = document.createElement('form');
+	const form = window.document.createElement('form');
 	form.action = '/oauth/decision';
 	form.method = 'post';
 	form.acceptCharset = 'utf-8';
 
-	const loginToken = document.createElement('input');
+	const loginToken = window.document.createElement('input');
 	loginToken.type = 'hidden';
 	loginToken.name = 'login_token';
 	loginToken.value = token;
 	form.appendChild(loginToken);
 
-	const transactionId = document.createElement('input');
+	const transactionId = window.document.createElement('input');
 	transactionId.type = 'hidden';
 	transactionId.name = 'transaction_id';
 	transactionId.value = transactionIdMeta?.content ?? '';
 	form.appendChild(transactionId);
 
 	if (decision === 'deny') {
-		const cancel = document.createElement('input');
+		const cancel = window.document.createElement('input');
 		cancel.type = 'hidden';
 		cancel.name = 'cancel';
 		cancel.value = 'cancel';
 		form.appendChild(cancel);
 	}
 
-	document.body.appendChild(form);
+	window.document.body.appendChild(form);
 	form.submit();
 }
 
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index a13f1a7813..03f973a33e 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -160,7 +160,7 @@ async function tokenDone() {
 function downloadBackupCodes() {
 	if (backupCodes.value !== undefined) {
 		const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' });
-		const dummya = document.createElement('a');
+		const dummya = window.document.createElement('a');
 		dummya.href = URL.createObjectURL(txtBlob);
 		dummya.download = `${$i.username}@${hostname}` + (port !== '' ? `_${port}` : '') + '-2fa-backup-codes.txt';
 		dummya.click();
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index 7681333910..73c89e23af 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -125,8 +125,8 @@ function syncBetweenTabs() {
 
 window.setInterval(syncBetweenTabs, 5000);
 
-document.addEventListener('visibilitychange', () => {
-	if (document.visibilityState === 'visible') {
+window.document.addEventListener('visibilitychange', () => {
+	if (window.document.visibilityState === 'visible') {
 		syncBetweenTabs();
 	}
 });
@@ -136,7 +136,7 @@ let latestBackupAt = 0;
 window.setInterval(() => {
 	if ($i == null) return;
 	if (!store.s.enablePreferencesAutoCloudBackup) return;
-	if (document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
+	if (window.document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
 	if (prefer.profile.modifiedAt <= latestBackupAt) return;
 
 	cloudBackup().then(() => {
diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
index bf3dfa157f..7229b7348e 100644
--- a/packages/frontend/src/preferences/utility.ts
+++ b/packages/frontend/src/preferences/utility.ts
@@ -106,14 +106,14 @@ async function renameProfile() {
 function exportCurrentProfile() {
 	const p = prefer.profile;
 	const txtBlob = new Blob([JSON.stringify(p)], { type: 'text/plain' });
-	const dummya = document.createElement('a');
+	const dummya = window.document.createElement('a');
 	dummya.href = URL.createObjectURL(txtBlob);
 	dummya.download = `${p.name || p.id}.misskeypreferences`;
 	dummya.click();
 }
 
 function importProfile() {
-	const input = document.createElement('input');
+	const input = window.document.createElement('input');
 	input.type = 'file';
 	input.accept = '.misskeypreferences';
 	input.onchange = async () => {
diff --git a/packages/frontend/src/server-context.ts b/packages/frontend/src/server-context.ts
index e79d3fa314..744bfa4b7b 100644
--- a/packages/frontend/src/server-context.ts
+++ b/packages/frontend/src/server-context.ts
@@ -5,7 +5,7 @@
 
 import * as Misskey from 'misskey-js';
 
-const providedContextEl = document.getElementById('misskey_clientCtx');
+const providedContextEl = window.document.getElementById('misskey_clientCtx');
 
 export type ServerContext = {
 	clip?: Misskey.entities.Clip;
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index c97d7d4071..25544d9d88 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -29,10 +29,10 @@ export function useStream(): Misskey.IStream {
 	timeoutHeartBeat = window.setTimeout(heartbeat, HEART_BEAT_INTERVAL);
 
 	// send heartbeat right now when last send time is over HEART_BEAT_INTERVAL
-	document.addEventListener('visibilitychange', () => {
+	window.document.addEventListener('visibilitychange', () => {
 		if (
 			!stream
-			|| document.visibilityState !== 'visible'
+			|| window.document.visibilityState !== 'visible'
 			|| Date.now() - lastHeartbeatCall < HEART_BEAT_INTERVAL
 		) return;
 		heartbeat();
@@ -42,7 +42,7 @@ export function useStream(): Misskey.IStream {
 }
 
 function heartbeat(): void {
-	if (stream != null && document.visibilityState === 'visible') {
+	if (stream != null && window.document.visibilityState === 'visible') {
 		stream.heartbeat();
 	}
 	lastHeartbeatCall = Date.now();
diff --git a/packages/frontend/src/theme.ts b/packages/frontend/src/theme.ts
index 970d143b97..cd44fff0c4 100644
--- a/packages/frontend/src/theme.ts
+++ b/packages/frontend/src/theme.ts
@@ -68,10 +68,10 @@ let timeout: number | null = null;
 export function applyTheme(theme: Theme, persist = true) {
 	if (timeout) window.clearTimeout(timeout);
 
-	document.documentElement.classList.add('_themeChanging_');
+	window.document.documentElement.classList.add('_themeChanging_');
 
 	timeout = window.setTimeout(() => {
-		document.documentElement.classList.remove('_themeChanging_');
+		window.document.documentElement.classList.remove('_themeChanging_');
 
 		// 色計算など再度行えるようにクライアント全体に通知
 		globalEvents.emit('themeChanged');
@@ -79,7 +79,7 @@ export function applyTheme(theme: Theme, persist = true) {
 
 	const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
 
-	document.documentElement.dataset.colorScheme = colorScheme;
+	window.document.documentElement.dataset.colorScheme = colorScheme;
 
 	// Deep copy
 	const _theme = deepClone(theme);
@@ -91,7 +91,7 @@ export function applyTheme(theme: Theme, persist = true) {
 
 	const props = compile(_theme);
 
-	for (const tag of document.head.children) {
+	for (const tag of window.document.head.children) {
 		if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
 			tag.setAttribute('content', props['htmlThemeColor']);
 			break;
@@ -99,10 +99,10 @@ export function applyTheme(theme: Theme, persist = true) {
 	}
 
 	for (const [k, v] of Object.entries(props)) {
-		document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
+		window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
 	}
 
-	document.documentElement.style.setProperty('color-scheme', colorScheme);
+	window.document.documentElement.style.setProperty('color-scheme', colorScheme);
 
 	if (persist) {
 		miLocalStorage.setItem('theme', JSON.stringify(props));
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index a39a4ee86b..930f633c9f 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -67,7 +67,7 @@ const dev = _DEV_;
 const notifications = ref<Misskey.entities.Notification[]>([]);
 
 function onNotification(notification: Misskey.entities.Notification, isClient = false) {
-	if (document.visibilityState === 'visible') {
+	if (window.document.visibilityState === 'visible') {
 		if (!isClient && notification.type !== 'test') {
 			// サーバーサイドのテスト通知の際は自動で既読をつけない（テストできないので）
 			useStream().send('readNotification');
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 754bf070fa..98d6f329ab 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -129,8 +129,8 @@ watch(store.r.menuDisplay, () => {
 });
 
 function toggleIconOnly() {
-	if (document.startViewTransition && prefer.s.animation) {
-		document.startViewTransition(() => {
+	if (window.document.startViewTransition && prefer.s.animation) {
+		window.document.startViewTransition(() => {
 			store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
 		});
 	} else {
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 8f35ce0c68..f51577fc68 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -87,9 +87,9 @@ provideMetadataReceiver((metadataGetter) => {
 	pageMetadata.value = info;
 	if (pageMetadata.value) {
 		if (isRoot.value && pageMetadata.value.title === instanceName) {
-			document.title = pageMetadata.value.title;
+			window.document.title = pageMetadata.value.title;
 		} else {
-			document.title = `${pageMetadata.value.title} | ${instanceName}`;
+			window.document.title = `${pageMetadata.value.title} | ${instanceName}`;
 		}
 	}
 });
@@ -142,7 +142,7 @@ if (window.innerWidth < 1024) {
 	window.location.reload();
 }
 
-document.documentElement.style.overflowY = 'scroll';
+window.document.documentElement.style.overflowY = 'scroll';
 
 onMounted(() => {
 	window.addEventListener('resize', () => {
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 4c33d7cb27..65aff8455a 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -202,8 +202,8 @@ function onWheel(ev: WheelEvent) {
 	}
 }
 
-document.documentElement.style.overflowY = 'hidden';
-document.documentElement.style.scrollBehavior = 'auto';
+window.document.documentElement.style.overflowY = 'hidden';
+window.document.documentElement.style.scrollBehavior = 'auto';
 
 async function deleteProfile() {
 	if (prefer.s['deck.profile'] == null) return;
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
index 94382b664a..ec20ac1114 100644
--- a/packages/frontend/src/ui/minimum.vue
+++ b/packages/frontend/src/ui/minimum.vue
@@ -30,9 +30,9 @@ provideMetadataReceiver((metadataGetter) => {
 	pageMetadata.value = info;
 	if (pageMetadata.value) {
 		if (isRoot.value && pageMetadata.value.title === instanceName) {
-			document.title = pageMetadata.value.title;
+			window.document.title = pageMetadata.value.title;
 		} else {
-			document.title = `${pageMetadata.value.title} | ${instanceName}`;
+			window.document.title = `${pageMetadata.value.title} | ${instanceName}`;
 		}
 	}
 });
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 92b5d253d3..2742b4cd98 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -143,9 +143,9 @@ provideMetadataReceiver((metadataGetter) => {
 	pageMetadata.value = info;
 	if (pageMetadata.value) {
 		if (isRoot.value && pageMetadata.value.title === instanceName) {
-			document.title = pageMetadata.value.title;
+			window.document.title = pageMetadata.value.title;
 		} else {
-			document.title = `${pageMetadata.value.title} | ${instanceName}`;
+			window.document.title = `${pageMetadata.value.title} | ${instanceName}`;
 		}
 	}
 });
@@ -205,12 +205,12 @@ provide<Ref<number>>(CURRENT_STICKY_BOTTOM, navFooterHeight);
 watch(navFooter, () => {
 	if (navFooter.value) {
 		navFooterHeight.value = navFooter.value.offsetHeight;
-		document.body.style.setProperty('--MI-stickyBottom', `${navFooterHeight.value}px`);
-		document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)');
+		window.document.body.style.setProperty('--MI-stickyBottom', `${navFooterHeight.value}px`);
+		window.document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)');
 	} else {
 		navFooterHeight.value = 0;
-		document.body.style.setProperty('--MI-stickyBottom', '0px');
-		document.body.style.setProperty('--MI-minBottomSpacing', '0px');
+		window.document.body.style.setProperty('--MI-stickyBottom', '0px');
+		window.document.body.style.setProperty('--MI-minBottomSpacing', '0px');
 	}
 }, {
 	immediate: true,
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index ddc3761b04..3e07959458 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -51,9 +51,9 @@ provideMetadataReceiver((metadataGetter) => {
 	pageMetadata.value = info;
 	if (pageMetadata.value) {
 		if (isRoot.value && pageMetadata.value.title === instanceName) {
-			document.title = pageMetadata.value.title;
+			window.document.title = pageMetadata.value.title;
 		} else {
-			document.title = `${pageMetadata.value.title} | ${instanceName}`;
+			window.document.title = `${pageMetadata.value.title} | ${instanceName}`;
 		}
 	}
 });
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index bfb2ef634b..3e4d452281 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -45,9 +45,9 @@ provideMetadataReceiver((metadataGetter) => {
 	pageMetadata.value = info;
 	if (pageMetadata.value) {
 		if (isRoot.value && pageMetadata.value.title === instanceName) {
-			document.title = pageMetadata.value.title;
+			window.document.title = pageMetadata.value.title;
 		} else {
-			document.title = `${pageMetadata.value.title} | ${instanceName}`;
+			window.document.title = `${pageMetadata.value.title} | ${instanceName}`;
 		}
 	}
 });
diff --git a/packages/frontend/src/use/use-note-capture.ts b/packages/frontend/src/use/use-note-capture.ts
index 0de2dbb3c5..97aec4c1f0 100644
--- a/packages/frontend/src/use/use-note-capture.ts
+++ b/packages/frontend/src/use/use-note-capture.ts
@@ -86,7 +86,7 @@ export function useNoteCapture(props: {
 	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 });
+			connection.send(window.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);
 		}
diff --git a/packages/frontend/src/use/use-tooltip.ts b/packages/frontend/src/use/use-tooltip.ts
index d9ddfc8b5d..af76a3a1e8 100644
--- a/packages/frontend/src/use/use-tooltip.ts
+++ b/packages/frontend/src/use/use-tooltip.ts
@@ -29,7 +29,7 @@ export function useTooltip(
 		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から消えている場合があるため
+		if (!window.document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため
 
 		const showing = ref(true);
 		onShow(showing);
@@ -38,7 +38,7 @@ export function useTooltip(
 		};
 
 		autoHidingTimer = window.setInterval(() => {
-			if (elRef.value == null || !document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) {
+			if (elRef.value == null || !window.document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) {
 				if (!isHovering) return;
 				isHovering = false;
 				window.clearTimeout(timeoutId);
diff --git a/packages/frontend/src/utility/focus-trap.ts b/packages/frontend/src/utility/focus-trap.ts
index fd17fa38a0..13d3bc56d2 100644
--- a/packages/frontend/src/utility/focus-trap.ts
+++ b/packages/frontend/src/utility/focus-trap.ts
@@ -50,7 +50,7 @@ function releaseFocusTrap(el: HTMLElement): void {
 
 	const highestZIndexElement = getHighestZIndexElement();
 
-	if (el.parentElement != null && el !== document.body) {
+	if (el.parentElement != null && el !== window.document.body) {
 		el.parentElement.childNodes.forEach((siblingNode) => {
 			const siblingEl = getHTMLElementOrNull(siblingNode);
 			if (!siblingEl) return;
@@ -104,7 +104,7 @@ export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEl
 		el.inert = false;
 	}
 
-	if (el.parentElement != null && el !== document.body) {
+	if (el.parentElement != null && el !== window.document.body) {
 		el.parentElement.childNodes.forEach((siblingNode) => {
 			const siblingEl = getHTMLElementOrNull(siblingNode);
 			if (!siblingEl) return;
diff --git a/packages/frontend/src/utility/focus.ts b/packages/frontend/src/utility/focus.ts
index e3fd928d1d..cbbe8226d7 100644
--- a/packages/frontend/src/utility/focus.ts
+++ b/packages/frontend/src/utility/focus.ts
@@ -58,7 +58,7 @@ export const focusParent = (input: MaybeHTMLElement | null | undefined, self = f
 
 const focusOrScroll = (element: HTMLElement, scroll: boolean) => {
 	if (scroll) {
-		const scrollContainer = getScrollContainer(element) ?? document.documentElement;
+		const scrollContainer = getScrollContainer(element) ?? window.document.documentElement;
 		const scrollContainerTop = getScrollPosition(scrollContainer);
 		const stickyTop = getStickyTop(element, scrollContainer);
 		const stickyBottom = getStickyBottom(element, scrollContainer);
@@ -74,7 +74,7 @@ const focusOrScroll = (element: HTMLElement, scroll: boolean) => {
 		scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' });
 	}
 
-	if (document.activeElement !== element) {
+	if (window.document.activeElement !== element) {
 		element.focus({ preventScroll: true });
 	}
 };
diff --git a/packages/frontend/src/utility/fullscreen.ts b/packages/frontend/src/utility/fullscreen.ts
index 7a0a018ef3..6702393cf1 100644
--- a/packages/frontend/src/utility/fullscreen.ts
+++ b/packages/frontend/src/utility/fullscreen.ts
@@ -35,8 +35,8 @@ export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscr
 
 export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
 	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	if (document.exitFullscreen != null) {
-		document.exitFullscreen();
+	if (window.document.exitFullscreen != null) {
+		window.document.exitFullscreen();
 		return;
 	}
 	if (videoEl.webkitExitFullscreen != null) {
diff --git a/packages/frontend/src/utility/hotkey.ts b/packages/frontend/src/utility/hotkey.ts
index fe62139a74..81fc28d7c8 100644
--- a/packages/frontend/src/utility/hotkey.ts
+++ b/packages/frontend/src/utility/hotkey.ts
@@ -54,9 +54,9 @@ 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;
+		if (window.document.activeElement != null) {
+			if (IGNORE_ELEMENTS.includes(window.document.activeElement.tagName.toLowerCase())) return;
+			if (getHTMLElementOrNull(window.document.activeElement)?.isContentEditable) return;
 		}
 		for (const action of actions) {
 			if (matchPatterns(ev, action)) {
diff --git a/packages/frontend/src/utility/init-chart.ts b/packages/frontend/src/utility/init-chart.ts
index 9775b9fec4..260899c1d7 100644
--- a/packages/frontend/src/utility/init-chart.ts
+++ b/packages/frontend/src/utility/init-chart.ts
@@ -50,7 +50,7 @@ export function initChart() {
 	);
 
 	// フォントカラー
-	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
+	Chart.defaults.color = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-fg');
 
 	Chart.defaults.borderColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 
diff --git a/packages/frontend/src/utility/physics.ts b/packages/frontend/src/utility/physics.ts
index 8a4e9319b3..5de34fd094 100644
--- a/packages/frontend/src/utility/physics.ts
+++ b/packages/frontend/src/utility/physics.ts
@@ -28,7 +28,7 @@ export function physics(container: HTMLElement) {
 	// create renderer
 	const render = Matter.Render.create({
 		engine: engine,
-		//element: document.getElementById('debug'),
+		//element: window.document.getElementById('debug'),
 		options: {
 			width: containerWidth,
 			height: containerHeight,
diff --git a/packages/frontend/src/utility/select-file.ts b/packages/frontend/src/utility/select-file.ts
index 1bee4986f6..b9b3687483 100644
--- a/packages/frontend/src/utility/select-file.ts
+++ b/packages/frontend/src/utility/select-file.ts
@@ -25,7 +25,7 @@ export function chooseFileFromPc(
 	const nameConverter = options?.nameConverter ?? (() => undefined);
 
 	return new Promise((res, rej) => {
-		const input = document.createElement('input');
+		const input = window.document.createElement('input');
 		input.type = 'file';
 		input.multiple = multiple;
 		input.onchange = () => {
diff --git a/packages/frontend/src/utility/snowfall-effect.ts b/packages/frontend/src/utility/snowfall-effect.ts
index d88bdb6660..5c86969876 100644
--- a/packages/frontend/src/utility/snowfall-effect.ts
+++ b/packages/frontend/src/utility/snowfall-effect.ts
@@ -156,7 +156,7 @@ export class SnowfallEffect {
 		easing: 0.0005,
 	};
 	/**
-	 * @throws {Error} - Thrown when it fails to get WebGL context for the canvas 
+	 * @throws {Error} - Thrown when it fails to get WebGL context for the canvas
 	 */
 	constructor(options: {
 		sakura?: boolean;
@@ -172,7 +172,7 @@ export class SnowfallEffect {
 		const gl = canvas.getContext('webgl2', { antialias: true });
 		if (gl == null) throw new Error('Failed to get WebGL context');
 
-		document.body.append(canvas);
+		window.document.body.append(canvas);
 
 		this.canvas = canvas;
 		this.gl = gl;
@@ -190,7 +190,7 @@ export class SnowfallEffect {
 	}
 
 	private initCanvas(): HTMLCanvasElement {
-		const canvas = document.createElement('canvas');
+		const canvas = window.document.createElement('canvas');
 
 		Object.assign(canvas.style, {
 			position: 'fixed',
diff --git a/packages/frontend/src/utility/sound.ts b/packages/frontend/src/utility/sound.ts
index 120f480b63..796af0e5ca 100644
--- a/packages/frontend/src/utility/sound.ts
+++ b/packages/frontend/src/utility/sound.ts
@@ -226,7 +226,7 @@ export function createSourceNode(buffer: AudioBuffer, opts: {
  * @param file ファイルのURL（ドライブIDではない）
  */
 export async function getSoundDuration(file: string): Promise<number> {
-	const audioEl = document.createElement('audio');
+	const audioEl = window.document.createElement('audio');
 	audioEl.src = file;
 	return new Promise((resolve) => {
 		const si = setInterval(() => {
@@ -249,7 +249,7 @@ export function isMute(): boolean {
 	}
 
 	// noinspection RedundantIfStatementJS
-	if (prefer.s['sound.useSoundOnlyWhenActive'] && document.visibilityState === 'hidden') {
+	if (prefer.s['sound.useSoundOnlyWhenActive'] && window.document.visibilityState === 'hidden') {
 		// ブラウザがアクティブな時のみサウンドを出力する
 		return true;
 	}
diff --git a/packages/frontend/src/utility/sticky-sidebar.ts b/packages/frontend/src/utility/sticky-sidebar.ts
index 50f1e6ecc8..867c9b8324 100644
--- a/packages/frontend/src/utility/sticky-sidebar.ts
+++ b/packages/frontend/src/utility/sticky-sidebar.ts
@@ -18,7 +18,7 @@ export class StickySidebar {
 		this.container = container;
 		this.el = this.container.children[0] as HTMLElement;
 		this.el.style.position = 'sticky';
-		this.spacer = document.createElement('div');
+		this.spacer = window.document.createElement('div');
 		this.container.prepend(this.spacer);
 		this.marginTop = marginTop;
 		this.offsetTop = this.container.getBoundingClientRect().top;
diff --git a/packages/frontend/src/utility/upload/isWebpSupported.ts b/packages/frontend/src/utility/upload/isWebpSupported.ts
index 2511236ecc..affd81fd57 100644
--- a/packages/frontend/src/utility/upload/isWebpSupported.ts
+++ b/packages/frontend/src/utility/upload/isWebpSupported.ts
@@ -6,7 +6,7 @@
 let isWebpSupportedCache: boolean | undefined;
 export function isWebpSupported() {
 	if (isWebpSupportedCache === undefined) {
-		const canvas = document.createElement('canvas');
+		const canvas = window.document.createElement('canvas');
 		canvas.width = 1;
 		canvas.height = 1;
 		isWebpSupportedCache = canvas.toDataURL('image/webp').startsWith('data:image/webp');
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index a7213f4c21..132eb0a629 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -77,7 +77,7 @@ const fetchEndpoint = computed(() => {
 const intervalClear = ref<(() => void) | undefined>();
 
 const tick = () => {
-	if (document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
+	if (window.document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
 
 	window.fetch(fetchEndpoint.value, {})
 		.then(res => res.json())
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 13b76533d7..b5be4d35c2 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -108,7 +108,7 @@ const intervalClear = ref<(() => void) | undefined>();
 const key = ref(0);
 
 const tick = () => {
-	if (document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
+	if (window.document.visibilityState === 'hidden' && rawItems.value.length !== 0) return;
 
 	window.fetch(fetchEndpoint.value, {})
 		.then(res => res.json())
diff --git a/packages/frontend/test/scroll.test.ts b/packages/frontend/test/scroll.test.ts
index 32a5a1c558..34e7e64313 100644
--- a/packages/frontend/test/scroll.test.ts
+++ b/packages/frontend/test/scroll.test.ts
@@ -12,10 +12,10 @@ describe('Scroll', () => {
 		/* 動作しない(happy-domのバグ？)
 		test('Initial onScrollTop callback for connected elements', () => {
 			const { document } = new Window();
-			const div = document.createElement('div');
+			const div = window.document.createElement('div');
 			assert.strictEqual(div.scrollTop, 0);
 
-			document.body.append(div);
+			window.document.body.append(div);
 
 			let called = false;
 			onScrollTop(div as any as HTMLElement, () => called = true);
@@ -26,7 +26,7 @@ describe('Scroll', () => {
 
 		test('No onScrollTop callback for disconnected elements', () => {
 			const { document } = new Window();
-			const div = document.createElement('div');
+			const div = window.document.createElement('div');
 			assert.strictEqual(div.scrollTop, 0);
 
 			let called = false;
@@ -40,10 +40,10 @@ describe('Scroll', () => {
 		/* 動作しない(happy-domのバグ？)
 		test('Initial onScrollBottom callback for connected elements', () => {
 			const { document } = new Window();
-			const div = document.createElement('div');
+			const div = window.document.createElement('div');
 			assert.strictEqual(div.scrollTop, 0);
 
-			document.body.append(div);
+			window.document.body.append(div);
 
 			let called = false;
 			onScrollBottom(div as any as HTMLElement, () => called = true);
@@ -54,7 +54,7 @@ describe('Scroll', () => {
 
 		test('No onScrollBottom callback for disconnected elements', () => {
 			const { document } = new Window();
-			const div = document.createElement('div');
+			const div = window.document.createElement('div');
 			assert.strictEqual(div.scrollTop, 0);
 
 			let called = false;
-- 
cgit v1.2.3-freya


From 2b42e8f171e0a030af52ae9b00129282a13479c1 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 11:18:49 +0900
Subject: test

---
 .../frontend/src/components/MkNotifications.vue    | 32 ++++++++++------------
 1 file changed, 15 insertions(+), 17 deletions(-)

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

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 21f1967bfa..80ae5899ec 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -4,23 +4,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkPullToRefresh :refresher="() => reload()">
-	<MkPagination ref="pagingComponent" :pagination="pagination">
-		<template #empty>
-			<div class="_fullinfo">
-				<img :src="infoImageUrl" draggable="false"/>
-				<div>{{ i18n.ts.noNotifications }}</div>
-			</div>
-		</template>
-
-		<template #default="{ items: notifications }">
-			<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
-				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
-			</MkDateSeparatedList>
-		</template>
-	</MkPagination>
-</MkPullToRefresh>
+<MkPagination ref="pagingComponent" :pagination="pagination">
+	<template #empty>
+		<div class="_fullinfo">
+			<img :src="infoImageUrl" draggable="false"/>
+			<div>{{ i18n.ts.noNotifications }}</div>
+		</div>
+	</template>
+
+	<template #default="{ items: notifications }">
+		<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
+			<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
+			<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
+		</MkDateSeparatedList>
+	</template>
+</MkPagination>
 </template>
 
 <script lang="ts" setup>
-- 
cgit v1.2.3-freya


From 3682c0069c1dee698fbd54adce65ec5316841fa1 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 12:27:13 +0900
Subject: Revert "test"

This reverts commit 2b42e8f171e0a030af52ae9b00129282a13479c1.
---
 .../frontend/src/components/MkNotifications.vue    | 32 ++++++++++++----------
 1 file changed, 17 insertions(+), 15 deletions(-)

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

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 80ae5899ec..21f1967bfa 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -4,21 +4,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkPagination ref="pagingComponent" :pagination="pagination">
-	<template #empty>
-		<div class="_fullinfo">
-			<img :src="infoImageUrl" draggable="false"/>
-			<div>{{ i18n.ts.noNotifications }}</div>
-		</div>
-	</template>
-
-	<template #default="{ items: notifications }">
-		<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-			<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
-			<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
-		</MkDateSeparatedList>
-	</template>
-</MkPagination>
+<MkPullToRefresh :refresher="() => reload()">
+	<MkPagination ref="pagingComponent" :pagination="pagination">
+		<template #empty>
+			<div class="_fullinfo">
+				<img :src="infoImageUrl" draggable="false"/>
+				<div>{{ i18n.ts.noNotifications }}</div>
+			</div>
+		</template>
+
+		<template #default="{ items: notifications }">
+			<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
+				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
+				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
+			</MkDateSeparatedList>
+		</template>
+	</MkPagination>
+</MkPullToRefresh>
 </template>
 
 <script lang="ts" setup>
-- 
cgit v1.2.3-freya


From fc88410c0dd9857cc0432832c4e5f6f6bf8db81a Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 13:34:53 +0900
Subject: refactor(frontend): tweak MkNotes and MkNotifications

---
 packages/frontend/src/components/MkNotes.vue       | 49 ++++++++++++----------
 .../frontend/src/components/MkNotifications.vue    | 29 ++++++-------
 2 files changed, 38 insertions(+), 40 deletions(-)

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

diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index cee226ee14..1f0f5db42a 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -13,32 +13,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 
 	<template #default="{ items: notes }">
-		<div :class="[$style.root, { [$style.noGap]: noGap }]">
-			<MkDateSeparatedList
-				ref="notes"
-				v-slot="{ item: note }"
-				:items="notes"
-				:direction="pagination.reversed ? 'up' : 'down'"
-				:reversed="pagination.reversed"
-				:noGap="noGap"
-				:ad="true"
-				:class="$style.notes"
-			>
-				<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
-			</MkDateSeparatedList>
+		<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]">
+			<template v-for="(note, i) in notes" :key="note.id">
+				<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
+				<div v-if="note._shouldInsertAd_" :class="$style.ad">
+					<MkAd :prefer="['horizontal', 'horizontal-big']"/>
+				</div>
+			</template>
 		</div>
 	</template>
 </MkPagination>
 </template>
 
 <script lang="ts" setup>
-import { useTemplateRef } from 'vue';
+import { useTemplateRef, TransitionGroup } from 'vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkNote from '@/components/MkNote.vue';
-import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	pagination: Paging;
@@ -55,20 +49,29 @@ defineExpose({
 
 <style lang="scss" module>
 .root {
+	container-type: inline-size;
+
 	&.noGap {
-		> .notes {
-			background: var(--MI_THEME-panel);
+		background: var(--MI_THEME-panel);
+
+		.note {
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
+		}
+
+		.ad {
+			padding: 8px;
+			background-size: auto auto;
+			background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
 		}
 	}
 
 	&:not(.noGap) {
-		> .notes {
-			background: var(--MI_THEME-bg);
+		background: var(--MI_THEME-bg);
 
-			.note {
-				background: var(--MI_THEME-panel);
-				border-radius: var(--MI-radius);
-			}
+		.note {
+			background: var(--MI_THEME-panel);
+			border-radius: var(--MI-radius);
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 21f1967bfa..a8282fec2a 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -14,10 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 
 		<template #default="{ items: notifications }">
-			<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
-				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
-			</MkDateSeparatedList>
+			<div :class="$style.notifications">
+				<template v-for="(notification, i) in notifications" :key="notification.id">
+					<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/>
+					<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/>
+				</template>
+			</div>
 		</template>
 	</MkPagination>
 </MkPullToRefresh>
@@ -29,7 +31,6 @@ import * as Misskey from 'misskey-js';
 import type { notificationTypes } from '@@/js/const.js';
 import MkPagination from '@/components/MkPagination.vue';
 import XNotification from '@/components/MkNotification.vue';
-import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
@@ -84,28 +85,22 @@ onMounted(() => {
 	connection.on('notificationFlushed', reload);
 });
 
-onActivated(() => {
-	pagingComponent.value?.reload();
-	connection = useStream().useChannel('main');
-	connection.on('notification', onNotification);
-	connection.on('notificationFlushed', reload);
-});
-
 onUnmounted(() => {
 	if (connection) connection.dispose();
 });
 
-onDeactivated(() => {
-	if (connection) connection.dispose();
-});
-
 defineExpose({
 	reload,
 });
 </script>
 
 <style lang="scss" module>
-.list {
+.notifications {
+	container-type: inline-size;
 	background: var(--MI_THEME-panel);
 }
+
+.item {
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
+}
 </style>
-- 
cgit v1.2.3-freya


From c03f9bff0ade3b0b31a59dd6b345434048dfcff4 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 30 Mar 2025 11:52:51 +0900
Subject: 🎨
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkNote.vue        |  2 ++
 packages/frontend/src/components/MkNotes.vue       | 26 ++++++++++++++++++--
 .../frontend/src/components/MkNotifications.vue    | 28 +++++++++++++++++++---
 3 files changed, 51 insertions(+), 5 deletions(-)

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

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 66a70c9be3..ab70a11b9b 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -723,6 +723,8 @@ function emitUpdReaction(emoji: string, delta: number) {
 }
 
 .skipRender {
+	// TODO: これが有効だとTransitionGroupでnoteを追加するときに一瞬がくっとなってしまうのをどうにかしたい
+	// Transitionが完了するのを待ってからskipRenderを付与すれば解決しそうだけどパフォーマンス的な影響が不明
 	content-visibility: auto;
 	contain-intrinsic-size: 0 150px;
 }
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 8605877a93..ad4844fd1b 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -13,14 +13,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 
 	<template #default="{ items: notes }">
-		<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]">
+		<component
+			:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]"
+			:enterActiveClass="$style.transition_x_enterActive"
+			:leaveActiveClass="$style.transition_x_leaveActive"
+			:enterFromClass="$style.transition_x_enterFrom"
+			:leaveToClass="$style.transition_x_leaveTo"
+			:moveClass=" $style.transition_x_move"
+			tag="div"
+		>
 			<template v-for="(note, i) in notes" :key="note.id">
 				<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
 				<div v-if="note._shouldInsertAd_" :class="$style.ad">
 					<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
 				</div>
 			</template>
-		</div>
+		</component>
 	</template>
 </MkPagination>
 </template>
@@ -48,6 +56,20 @@ defineExpose({
 </script>
 
 <style lang="scss" module>
+.transition_x_move,
+.transition_x_enterActive,
+.transition_x_leaveActive {
+	transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_x_enterFrom,
+.transition_x_leaveTo {
+	opacity: 0;
+	transform: translateY(-50%);
+}
+.transition_x_leaveActive {
+	position: absolute;
+}
+
 .root {
 	container-type: inline-size;
 
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index a8282fec2a..99eca35eb7 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -14,19 +14,27 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 
 		<template #default="{ items: notifications }">
-			<div :class="$style.notifications">
+			<component
+				:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]"
+				:enterActiveClass="$style.transition_x_enterActive"
+				:leaveActiveClass="$style.transition_x_leaveActive"
+				:enterFromClass="$style.transition_x_enterFrom"
+				:leaveToClass="$style.transition_x_leaveTo"
+				:moveClass=" $style.transition_x_move"
+				tag="div"
+			>
 				<template v-for="(notification, i) in notifications" :key="notification.id">
 					<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/>
 					<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/>
 				</template>
-			</div>
+			</component>
 		</template>
 	</MkPagination>
 </MkPullToRefresh>
 </template>
 
 <script lang="ts" setup>
-import { onUnmounted, onDeactivated, onMounted, computed, useTemplateRef, onActivated } from 'vue';
+import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { notificationTypes } from '@@/js/const.js';
 import MkPagination from '@/components/MkPagination.vue';
@@ -95,6 +103,20 @@ defineExpose({
 </script>
 
 <style lang="scss" module>
+.transition_x_move,
+.transition_x_enterActive,
+.transition_x_leaveActive {
+	transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_x_enterFrom,
+.transition_x_leaveTo {
+	opacity: 0;
+	transform: translateY(-50%);
+}
+.transition_x_leaveActive {
+	position: absolute;
+}
+
 .notifications {
 	container-type: inline-size;
 	background: var(--MI_THEME-panel);
-- 
cgit v1.2.3-freya


From 6d90e09a587e000fb780f64dae4a1d249568dbc3 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 16 Apr 2025 10:35:05 +0900
Subject: enhance(frontend):
 タイムライン以外でもスクロール位置の保持を試みるように
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../frontend/src/components/MkNotifications.vue    |  4 +--
 .../src/components/global/PageWithHeader.vue       |  3 ++
 .../frontend/src/use/use-scroll-position-keeper.ts | 39 ++++++++++++++++------
 3 files changed, 33 insertions(+), 13 deletions(-)

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

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 99eca35eb7..b8fada1020 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				tag="div"
 			>
 				<template v-for="(notification, i) in notifications" :key="notification.id">
-					<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/>
-					<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/>
+					<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true" :data-scroll-anchor="notification.id"/>
+					<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true" :data-scroll-anchor="notification.id"/>
 				</template>
 			</component>
 		</template>
diff --git a/packages/frontend/src/components/global/PageWithHeader.vue b/packages/frontend/src/components/global/PageWithHeader.vue
index 7ea0b5c97f..99f8df0780 100644
--- a/packages/frontend/src/components/global/PageWithHeader.vue
+++ b/packages/frontend/src/components/global/PageWithHeader.vue
@@ -20,6 +20,7 @@ import { useTemplateRef } from 'vue';
 import { scrollInContainer } from '@@/js/scroll.js';
 import type { PageHeaderItem } from '@/types/page-header.js';
 import type { Tab } from './MkPageHeader.tabs.vue';
+import { useScrollPositionKeeper } from '@/use/use-scroll-position-keeper.js';
 
 const props = withDefaults(defineProps<{
 	tabs?: Tab[];
@@ -35,6 +36,8 @@ const props = withDefaults(defineProps<{
 const tab = defineModel<string>('tab');
 const rootEl = useTemplateRef('rootEl');
 
+useScrollPositionKeeper(rootEl);
+
 defineExpose({
 	scrollToTop: () => {
 		if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' });
diff --git a/packages/frontend/src/use/use-scroll-position-keeper.ts b/packages/frontend/src/use/use-scroll-position-keeper.ts
index 00cc51a459..f7b3015cc9 100644
--- a/packages/frontend/src/use/use-scroll-position-keeper.ts
+++ b/packages/frontend/src/use/use-scroll-position-keeper.ts
@@ -4,19 +4,22 @@
  */
 
 import { throttle } from 'throttle-debounce';
-import { nextTick, onActivated, onUnmounted, watch } from 'vue';
+import { nextTick, onActivated, onDeactivated, onUnmounted, watch } from 'vue';
 import type { Ref } from 'vue';
 
 // note render skippingがオンだとズレるため、遷移直前にスクロール範囲に表示されているdata-scroll-anchor要素を特定して、復元時に当該要素までスクロールするようにする
 
 export function useScrollPositionKeeper(scrollContainerRef: Ref<HTMLElement | null | undefined>): void {
 	let anchorId: string | null = null;
+	let ready = true;
 
 	watch(scrollContainerRef, (el) => {
 		if (!el) return;
 
 		const onScroll = () => {
 			if (!el) return;
+			if (!ready) return;
+
 			const scrollContainerRect = el.getBoundingClientRect();
 			const viewPosition = scrollContainerRect.height / 2;
 
@@ -41,18 +44,32 @@ export function useScrollPositionKeeper(scrollContainerRef: Ref<HTMLElement | nu
 		immediate: true,
 	});
 
+	const restore = () => {
+		if (!anchorId) return;
+		const scrollContainer = scrollContainerRef.value;
+		if (!scrollContainer) return;
+		const scrollAnchorEl = scrollContainer.querySelector(`[data-scroll-anchor="${anchorId}"]`);
+		if (!scrollAnchorEl) return;
+		scrollAnchorEl.scrollIntoView({
+			behavior: 'instant',
+			block: 'center',
+			inline: 'center',
+		});
+	};
+
+	onDeactivated(() => {
+		ready = false;
+	});
+
 	onActivated(() => {
+		restore();
 		nextTick(() => {
-			if (!anchorId) return;
-			const scrollContainer = scrollContainerRef.value;
-			if (!scrollContainer) return;
-			const scrollAnchorEl = scrollContainer.querySelector(`[data-scroll-anchor="${anchorId}"]`);
-			if (!scrollAnchorEl) return;
-			scrollAnchorEl.scrollIntoView({
-				behavior: 'instant',
-				block: 'center',
-				inline: 'center',
-			});
+			restore();
+			window.setTimeout(() => {
+				restore();
+
+				ready = true;
+			}, 100);
 		});
 	});
 }
-- 
cgit v1.2.3-freya


From 1cc9afc555615fdf478c693f526e28054fc2481a Mon Sep 17 00:00:00 2001
From: Hazelnoot <acomputerdog@gmail.com>
Date: Fri, 23 May 2025 21:59:25 -0400
Subject: avoid use of <template> as a child of <TransitionGroup>

---
 packages/frontend/src/components/MkNotifications.vue   | 4 ++--
 packages/frontend/src/components/MkReactionsViewer.vue | 2 +-
 packages/frontend/src/components/MkTimeline.vue        | 4 ++--
 packages/frontend/src/pages/chat/room.vue              | 4 ++--
 4 files changed, 7 insertions(+), 7 deletions(-)

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

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 55ee054285..54edf771ed 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -23,10 +23,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				:moveClass=" $style.transition_x_move"
 				tag="div"
 			>
-				<template v-for="(notification, i) in notifications" :key="notification.id">
+				<div v-for="(notification, i) in notifications" :key="notification.id">
 					<DynamicNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true" :data-scroll-anchor="notification.id"/>
 					<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true" :data-scroll-anchor="notification.id"/>
-				</template>
+				</div>
 			</component>
 		</template>
 	</MkPagination>
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 70d0ddca0a..945640ab41 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	tag="div" :class="$style.root"
 >
 	<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
-	<slot v-if="hasMoreReactions" name="more"/>
+	<slot v-if="hasMoreReactions" :key="'$more'" name="more"/>
 </component>
 </template>
 
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 87e8f0e258..48e8c7f377 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				:moveClass=" $style.transition_x_move"
 				tag="div"
 			>
-				<template v-for="(note, i) in notes" :key="note.id">
+				<div v-for="(note, i) in notes" :key="note.id">
 					<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
 						<DynamicNote :class="$style.note" :note="note" :withHardMute="true"/>
 						<div :class="$style.ad">
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 					</div>
 					<DynamicNote v-else :class="$style.note" :note="note" :withHardMute="true" :data-scroll-anchor="note.id"/>
-				</template>
+				</div>
 			</component>
 		</template>
 	</MkPagination>
diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue
index e05125a3b2..5afda5682f 100644
--- a/packages/frontend/src/pages/chat/room.vue
+++ b/packages/frontend/src/pages/chat/room.vue
@@ -39,14 +39,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 					:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
 					tag="div" class="_gaps"
 				>
-					<template v-for="item in timeline.toReversed()" :key="item.id">
+					<div v-for="item in timeline.toReversed()" :key="item.id">
 						<XMessage v-if="item.type === 'item'" :message="item.data"/>
 						<div v-else-if="item.type === 'date'" :class="$style.dateDivider">
 							<span><i class="ti ti-chevron-up"></i> {{ item.nextText }}</span>
 							<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
 							<span>{{ item.prevText }} <i class="ti ti-chevron-down"></i></span>
 						</div>
-					</template>
+					</div>
 				</TransitionGroup>
 			</div>
 
-- 
cgit v1.2.3-freya

