diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47e13ba5a5..4def20d06c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,16 @@
 
 -->
 
+## 13.x.x (unreleased)
+
+### General
+- タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように
+	- 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります
+
+### Client
+- 開発者モードを追加
+- AiScriptを0.13.3に更新
+
 ## 13.12.2
 
 ## NOTE
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 0b7108fe6d..340698a12b 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -52,6 +52,8 @@ addToList: "リストに追加"
 sendMessage: "メッセージを送信"
 copyRSS: "RSSをコピー"
 copyUsername: "ユーザー名をコピー"
+copyUserId: "ユーザーIDをコピー"
+copyNoteId: "ノートIDをコピー"
 searchUser: "ユーザーを検索"
 reply: "返信"
 loadMore: "もっと見る"
@@ -823,6 +825,7 @@ translatedFrom: "{x}から翻訳"
 accountDeletionInProgress: "アカウントの削除が進行中です"
 usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。"
 aiChanMode: "藍モード"
+devMode: "開発者モード"
 keepCw: "CWを維持する"
 pubSub: "Pub/Subのアカウント"
 lastCommunication: "直近の通信"
diff --git a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js
new file mode 100644
index 0000000000..690653bd7c
--- /dev/null
+++ b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js
@@ -0,0 +1,11 @@
+export class RemoveShowTimelineReplies1684206886988 {
+    name = 'RemoveShowTimelineReplies1684206886988'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showTimelineReplies"`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "showTimelineReplies" boolean NOT NULL DEFAULT false`);
+    }
+}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index c6e1075389..744f999596 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -144,7 +144,7 @@ export function loadConfig() {
 	const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
 	const clientManifest = clientManifestExists ?
 		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
-		: { 'src/init.ts': { file: 'src/init.ts' } };
+		: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
 	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
 
 	const mixin = {} as Mixin;
@@ -165,7 +165,7 @@ export function loadConfig() {
 	mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
 	mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
 	mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
-	mixin.clientEntry = clientManifest['src/init.ts'];
+	mixin.clientEntry = clientManifest['src/_boot_.ts'];
 	mixin.clientManifestExists = clientManifestExists;
 
 	const externalMediaProxy = config.mediaProxy ?
diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts
index 0cee2076bf..bf50a1cded 100644
--- a/packages/backend/src/core/QueryService.ts
+++ b/packages/backend/src/core/QueryService.ts
@@ -208,7 +208,7 @@ export class QueryService {
 	}
 
 	@bindThis
-	public generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null): void {
+	public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<User, 'id'> | null): void {
 		if (me == null) {
 			q.andWhere(new Brackets(qb => { qb
 				.where('note.replyId IS NULL') // 返信ではない
@@ -217,7 +217,7 @@ export class QueryService {
 					.andWhere('note.replyUserId = note.userId');
 				}));
 			}));
-		} else if (!me.showTimelineReplies) {
+		} else if (!withReplies) {
 			q.andWhere(new Brackets(qb => { qb
 				.where('note.replyId IS NULL') // 返信ではない
 				.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index eea1d1b848..f52ebed107 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -32,6 +32,8 @@ import type { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { MetaService } from '@/core/MetaService.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import type { AccountMoveService } from '@/core/AccountMoveService.js';
+import { checkHttps } from '@/misc/check-https.js';
 import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
 import { extractApHashtags } from './tag.js';
 import type { OnModuleInit } from '@nestjs/common';
@@ -42,8 +44,6 @@ import type { ApLoggerService } from '../ApLoggerService.js';
 // eslint-disable-next-line @typescript-eslint/consistent-type-imports
 import type { ApImageService } from './ApImageService.js';
 import type { IActor, IObject } from '../type.js';
-import type { AccountMoveService } from '@/core/AccountMoveService.js';
-import { checkHttps } from '@/misc/check-https.js';
 
 const nameLength = 128;
 const summaryLength = 2048;
@@ -306,7 +306,6 @@ export class ApPersonService implements OnModuleInit {
 					tags,
 					isBot,
 					isCat: (person as any).isCat === true,
-					showTimelineReplies: false,
 				})) as RemoteUser;
 
 				await transactionalEntityManager.save(new UserProfile({
@@ -696,7 +695,7 @@ export class ApPersonService implements OnModuleInit {
 		if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
 			return 'skip: dst.alsoKnownAs is empty';
 		}
-		if (!dst.alsoKnownAs?.includes(src.uri)) {
+		if (!dst.alsoKnownAs.includes(src.uri)) {
 			return 'skip: alsoKnownAs does not include from.uri';
 		}
 
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 7f61e1d6f3..bfd506ea86 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -466,7 +466,6 @@ export class UserEntityService implements OnModuleInit {
 				mutedInstances: profile!.mutedInstances,
 				mutingNotificationTypes: profile!.mutingNotificationTypes,
 				emailNotificationTypes: profile!.emailNotificationTypes,
-				showTimelineReplies: user.showTimelineReplies ?? falsy,
 				achievements: profile!.achievements,
 				loggedInDays: profile!.loggedInDates.length,
 				policies: this.roleService.getUserPolicies(user.id),
diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts
index 8e10f999b6..6669890cf6 100644
--- a/packages/backend/src/models/entities/User.ts
+++ b/packages/backend/src/models/entities/User.ts
@@ -232,12 +232,6 @@ export class User {
 	})
 	public followersUri: string | null;
 
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether to show users replying to other users in the timeline.',
-	})
-	public showTimelineReplies: boolean;
-
 	@Index({ unique: true })
 	@Column('char', {
 		length: 16, nullable: true, unique: true,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 74be00a8b8..d10f690a32 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -141,7 +141,6 @@ export const paramDef = {
 		preventAiLearning: { type: 'boolean' },
 		isBot: { type: 'boolean' },
 		isCat: { type: 'boolean' },
-		showTimelineReplies: { type: 'boolean' },
 		injectFeaturedNote: { type: 'boolean' },
 		receiveAnnouncementEmail: { type: 'boolean' },
 		alwaysMarkNsfw: { type: 'boolean' },
@@ -239,7 +238,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
 			if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
 			if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
-			if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies;
 			if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
 			if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
 			if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index c11c1eac40..88c1ca7f58 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -34,11 +34,8 @@ export const meta = {
 export const paramDef = {
 	type: 'object',
 	properties: {
-		withFiles: {
-			type: 'boolean',
-			default: false,
-			description: 'Only show notes that have attached files.',
-		},
+		withFiles: { type: 'boolean', default: false },
+		withReplies: { type: 'boolean', default: false },
 		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 		sinceId: { type: 'string', format: 'misskey:id' },
 		untilId: { type: 'string', format: 'misskey:id' },
@@ -78,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				.leftJoinAndSelect('reply.user', 'replyUser')
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
-			this.queryService.generateRepliesQuery(query, me);
+			this.queryService.generateRepliesQuery(query, ps.withReplies, me);
 			if (me) {
 				this.queryService.generateMutedUserQuery(query, me);
 				this.queryService.generateMutedNoteQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index 89abd91c7e..7a3581e6e4 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -46,11 +46,8 @@ export const paramDef = {
 		includeMyRenotes: { type: 'boolean', default: true },
 		includeRenotedMyNotes: { type: 'boolean', default: true },
 		includeLocalRenotes: { type: 'boolean', default: true },
-		withFiles: {
-			type: 'boolean',
-			default: false,
-			description: 'Only show notes that have attached files.',
-		},
+		withFiles: { type: 'boolean', default: false },
+		withReplies: { type: 'boolean', default: false },
 	},
 	required: [],
 } as const;
@@ -98,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				.setParameters(followingQuery.getParameters());
 
 			this.queryService.generateChannelQuery(query, me);
-			this.queryService.generateRepliesQuery(query, me);
+			this.queryService.generateRepliesQuery(query, ps.withReplies, me);
 			this.queryService.generateVisibilityQuery(query, me);
 			this.queryService.generateMutedUserQuery(query, me);
 			this.queryService.generateMutedNoteQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index afdafc7c55..2ee549232c 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -36,11 +36,8 @@ export const meta = {
 export const paramDef = {
 	type: 'object',
 	properties: {
-		withFiles: {
-			type: 'boolean',
-			default: false,
-			description: 'Only show notes that have attached files.',
-		},
+		withFiles: { type: 'boolean', default: false },
+		withReplies: { type: 'boolean', default: false },
 		fileType: { type: 'array', items: {
 			type: 'string',
 		} },
@@ -86,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
 			this.queryService.generateChannelQuery(query, me);
-			this.queryService.generateRepliesQuery(query, me);
+			this.queryService.generateRepliesQuery(query, ps.withReplies, me);
 			this.queryService.generateVisibilityQuery(query, me);
 			if (me) this.queryService.generateMutedUserQuery(query, me);
 			if (me) this.queryService.generateMutedNoteQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index c6ee1e5c2b..e1f286439b 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -35,11 +35,8 @@ export const paramDef = {
 		includeMyRenotes: { type: 'boolean', default: true },
 		includeRenotedMyNotes: { type: 'boolean', default: true },
 		includeLocalRenotes: { type: 'boolean', default: true },
-		withFiles: {
-			type: 'boolean',
-			default: false,
-			description: 'Only show notes that have attached files.',
-		},
+		withFiles: { type: 'boolean', default: false },
+		withReplies: { type: 'boolean', default: false },
 	},
 	required: [],
 } as const;
@@ -84,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			}
 
 			this.queryService.generateChannelQuery(query, me);
-			this.queryService.generateRepliesQuery(query, me);
+			this.queryService.generateRepliesQuery(query, ps.withReplies, me);
 			this.queryService.generateVisibilityQuery(query, me);
 			this.queryService.generateMutedUserQuery(query, me);
 			this.queryService.generateMutedNoteQuery(query, me);
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 5454836fe1..d3339072c1 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -13,6 +13,7 @@ class GlobalTimelineChannel extends Channel {
 	public readonly chName = 'globalTimeline';
 	public static shouldShare = true;
 	public static requireCredential = false;
+	private withReplies: boolean;
 
 	constructor(
 		private metaService: MetaService,
@@ -31,6 +32,8 @@ class GlobalTimelineChannel extends Channel {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.gtlAvailable) return;
 
+		this.withReplies = params.withReplies as boolean;
+
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
 	}
@@ -54,7 +57,7 @@ class GlobalTimelineChannel extends Channel {
 		}
 
 		// 関係ない返信は除外
-		if (note.reply && !this.user!.showTimelineReplies) {
+		if (note.reply && !this.withReplies) {
 			const reply = note.reply;
 			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
 			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index ee874ad81e..1755aa94cf 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -11,6 +11,7 @@ class HomeTimelineChannel extends Channel {
 	public readonly chName = 'homeTimeline';
 	public static shouldShare = true;
 	public static requireCredential = true;
+	private withReplies: boolean;
 
 	constructor(
 		private noteEntityService: NoteEntityService,
@@ -24,6 +25,8 @@ class HomeTimelineChannel extends Channel {
 
 	@bindThis
 	public async init(params: any) {
+		this.withReplies = params.withReplies as boolean;
+	
 		this.subscriber.on('notesStream', this.onNote);
 	}
 
@@ -63,7 +66,7 @@ class HomeTimelineChannel extends Channel {
 		}
 
 		// 関係ない返信は除外
-		if (note.reply && !this.user!.showTimelineReplies) {
+		if (note.reply && !this.withReplies) {
 			const reply = note.reply;
 			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
 			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 4f7b4e78b6..5a33e13cf5 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -13,6 +13,7 @@ class HybridTimelineChannel extends Channel {
 	public readonly chName = 'hybridTimeline';
 	public static shouldShare = true;
 	public static requireCredential = true;
+	private withReplies: boolean;
 
 	constructor(
 		private metaService: MetaService,
@@ -31,6 +32,8 @@ class HybridTimelineChannel extends Channel {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.ltlAvailable) return;
 
+		this.withReplies = params.withReplies as boolean;
+
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
 	}
@@ -75,7 +78,7 @@ class HybridTimelineChannel extends Channel {
 		if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
 
 		// 関係ない返信は除外
-		if (note.reply && !this.user!.showTimelineReplies) {
+		if (note.reply && !this.withReplies) {
 			const reply = note.reply;
 			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
 			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 09b0005ac1..9ca4db8ced 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -12,6 +12,7 @@ class LocalTimelineChannel extends Channel {
 	public readonly chName = 'localTimeline';
 	public static shouldShare = true;
 	public static requireCredential = false;
+	private withReplies: boolean;
 
 	constructor(
 		private metaService: MetaService,
@@ -30,6 +31,8 @@ class LocalTimelineChannel extends Channel {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.ltlAvailable) return;
 
+		this.withReplies = params.withReplies as boolean;
+
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
 	}
@@ -54,7 +57,7 @@ class LocalTimelineChannel extends Channel {
 		}
 
 		// 関係ない返信は除外
-		if (note.reply && this.user && !this.user.showTimelineReplies) {
+		if (note.reply && this.user && !this.withReplies) {
 			const reply = note.reply;
 			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
 			if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts
index a6f9145952..fee56e3668 100644
--- a/packages/backend/src/server/api/stream/index.ts
+++ b/packages/backend/src/server/api/stream/index.ts
@@ -246,7 +246,7 @@ export default class Connection {
 
 		const ch: Channel = channelService.create(id, this);
 		this.channels.push(ch);
-		ch.init(params);
+		ch.init(params ?? {});
 
 		if (pong) {
 			this.sendMessageToWs('connected', {
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index a7f8210c8e..02684c93b8 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -43,7 +43,6 @@ describe('ユーザー', () => {
 
 	type MeDetailed = UserDetailedNotMe & 
 		misskey.entities.MeDetailed & {
-		showTimelineReplies: boolean,
 		achievements: object[],
 		loggedInDays: number,
 		policies: object,
@@ -160,7 +159,6 @@ describe('ユーザー', () => {
 			mutedInstances: user.mutedInstances,
 			mutingNotificationTypes: user.mutingNotificationTypes,
 			emailNotificationTypes: user.emailNotificationTypes,
-			showTimelineReplies: user.showTimelineReplies,
 			achievements: user.achievements, 
 			loggedInDays: user.loggedInDays,
 			policies: user.policies,
@@ -406,7 +404,6 @@ describe('ユーザー', () => {
 		assert.deepStrictEqual(response.mutedInstances, []);
 		assert.deepStrictEqual(response.mutingNotificationTypes, []);
 		assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
-		assert.strictEqual(response.showTimelineReplies, false);
 		assert.deepStrictEqual(response.achievements, []);
 		assert.deepStrictEqual(response.loggedInDays, 0);
 		assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); 
@@ -470,8 +467,6 @@ describe('ユーザー', () => {
 		{ parameters: (): object => ({ isBot: false }) },
 		{ parameters: (): object => ({ isCat: true }) },
 		{ parameters: (): object => ({ isCat: false }) },
-		{ parameters: (): object => ({ showTimelineReplies: true }) },
-		{ parameters: (): object => ({ showTimelineReplies: false }) },
 		{ parameters: (): object => ({ injectFeaturedNote: true }) },
 		{ parameters: (): object => ({ injectFeaturedNote: false }) },
 		{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },
diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.js
index e8e0e57d2a..a1e1f57e10 100644
--- a/packages/frontend/.eslintrc.js
+++ b/packages/frontend/.eslintrc.js
@@ -64,6 +64,7 @@ module.exports = {
 		'vue/singleline-html-element-content-newline': 'off',
 		// (vue/vue3-recommended disabled the autofix for Vue 2 compatibility)
 		'vue/v-on-event-hyphenation': ['warn', 'always', { autofix: true }],
+		'vue/attribute-hyphenation': ['warn', 'never'],
 	},
 	globals: {
 		// Node.js
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 2392e510a4..2f754f8aa2 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -19,12 +19,12 @@
 		"@rollup/plugin-json": "6.0.0",
 		"@rollup/plugin-replace": "5.0.2",
 		"@rollup/pluginutils": "5.0.2",
-		"@syuilo/aiscript": "0.13.2",
+		"@syuilo/aiscript": "0.13.3",
 		"@tabler/icons-webfont": "2.17.0",
-		"@vitejs/plugin-vue": "4.2.2",
-		"@vue-macros/reactivity-transform": "0.3.6",
-		"@vue/compiler-sfc": "3.3.1",
-		"autosize": "5.0.2",
+		"@vitejs/plugin-vue": "4.2.3",
+		"@vue-macros/reactivity-transform": "0.3.7",
+		"@vue/compiler-sfc": "3.3.2",
+		"autosize": "6.0.1",
 		"broadcast-channel": "4.20.2",
 		"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
 		"buraha": "github:misskey-dev/buraha",
@@ -53,7 +53,7 @@
 		"punycode": "2.3.0",
 		"querystring": "0.2.1",
 		"rndstr": "1.0.0",
-		"rollup": "3.21.6",
+		"rollup": "3.22.0",
 		"s-age": "1.1.2",
 		"sanitize-html": "2.10.0",
 		"sass": "1.62.1",
@@ -70,31 +70,31 @@
 		"typescript": "5.0.4",
 		"uuid": "9.0.0",
 		"vanilla-tilt": "1.8.0",
-		"vite": "4.3.5",
-		"vue": "3.3.1",
+		"vite": "4.3.7",
+		"vue": "3.3.2",
 		"vue-plyr": "7.0.0",
 		"vue-prism-editor": "2.0.0-alpha.2",
 		"vuedraggable": "next"
 	},
 	"devDependencies": {
-		"@storybook/addon-actions": "7.0.10",
-		"@storybook/addon-essentials": "7.0.10",
-		"@storybook/addon-interactions": "7.0.10",
-		"@storybook/addon-links": "7.0.10",
-		"@storybook/addon-storysource": "7.0.10",
-		"@storybook/addons": "7.0.10",
-		"@storybook/blocks": "7.0.10",
-		"@storybook/core-events": "7.0.10",
+		"@storybook/addon-actions": "7.0.12",
+		"@storybook/addon-essentials": "7.0.12",
+		"@storybook/addon-interactions": "7.0.12",
+		"@storybook/addon-links": "7.0.12",
+		"@storybook/addon-storysource": "7.0.12",
+		"@storybook/addons": "7.0.12",
+		"@storybook/blocks": "7.0.12",
+		"@storybook/core-events": "7.0.12",
 		"@storybook/jest": "0.1.0",
-		"@storybook/manager-api": "7.0.10",
-		"@storybook/preview-api": "7.0.10",
-		"@storybook/react": "7.0.10",
-		"@storybook/react-vite": "7.0.10",
+		"@storybook/manager-api": "7.0.12",
+		"@storybook/preview-api": "7.0.12",
+		"@storybook/react": "7.0.12",
+		"@storybook/react-vite": "7.0.12",
 		"@storybook/testing-library": "0.1.0",
-		"@storybook/theming": "7.0.10",
-		"@storybook/types": "7.0.10",
-		"@storybook/vue3": "7.0.10",
-		"@storybook/vue3-vite": "7.0.10",
+		"@storybook/theming": "7.0.12",
+		"@storybook/types": "7.0.12",
+		"@storybook/vue3": "7.0.12",
+		"@storybook/vue3-vite": "7.0.12",
 		"@testing-library/jest-dom": "5.16.5",
 		"@testing-library/vue": "7.0.0",
 		"@types/escape-regexp": "0.0.1",
@@ -103,7 +103,7 @@
 		"@types/gulp-rename": "2.0.2",
 		"@types/matter-js": "0.18.3",
 		"@types/micromatch": "4.0.2",
-		"@types/node": "20.1.3",
+		"@types/node": "20.1.7",
 		"@types/punycode": "2.1.0",
 		"@types/sanitize-html": "2.9.0",
 		"@types/seedrandom": "3.0.5",
@@ -116,16 +116,16 @@
 		"@typescript-eslint/eslint-plugin": "5.59.5",
 		"@typescript-eslint/parser": "5.59.5",
 		"@vitest/coverage-c8": "0.31.0",
-		"@vue/runtime-core": "3.3.1",
+		"@vue/runtime-core": "3.3.2",
 		"astring": "1.8.4",
 		"chokidar-cli": "3.0.0",
 		"cross-env": "7.0.3",
 		"cypress": "12.12.0",
 		"eslint": "8.40.0",
 		"eslint-plugin-import": "2.27.5",
-		"eslint-plugin-vue": "9.12.0",
+		"eslint-plugin-vue": "9.13.0",
 		"fast-glob": "3.2.12",
-		"happy-dom": "9.16.0",
+		"happy-dom": "9.18.3",
 		"micromatch": "3.1.10",
 		"msw": "1.2.1",
 		"msw-storybook-addon": "1.8.0",
@@ -133,13 +133,13 @@
 		"react": "18.2.0",
 		"react-dom": "18.2.0",
 		"start-server-and-test": "2.0.0",
-		"storybook": "7.0.10",
+		"storybook": "7.0.12",
 		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
 		"summaly": "github:misskey-dev/summaly",
 		"vite-plugin-turbosnap": "1.0.2",
 		"vitest": "0.31.0",
 		"vitest-fetch-mock": "0.2.2",
-		"vue-eslint-parser": "9.2.1",
-		"vue-tsc": "1.6.4"
+		"vue-eslint-parser": "9.3.0",
+		"vue-tsc": "1.6.5"
 	}
 }
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts
new file mode 100644
index 0000000000..a8efafca61
--- /dev/null
+++ b/packages/frontend/src/_boot_.ts
@@ -0,0 +1,12 @@
+// https://vitejs.dev/config/build-options.html#build-modulepreload
+import 'vite/modulepreload-polyfill';
+
+import '@/style.scss';
+import { mainBoot } from './boot/main-boot';
+import { subBoot } from './boot/sub-boot';
+
+if (['/share', '/auth', '/miauth'].includes(location.pathname)) {
+	subBoot();
+} else {
+	mainBoot();
+}
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
new file mode 100644
index 0000000000..e9d6586f85
--- /dev/null
+++ b/packages/frontend/src/boot/common.ts
@@ -0,0 +1,263 @@
+import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue';
+import { compareVersions } from 'compare-versions';
+import JSON5 from 'json5';
+import widgets from '@/widgets';
+import directives from '@/directives';
+import components from '@/components';
+import { version, ui, lang, updateLocale } from '@/config';
+import { applyTheme } from '@/scripts/theme';
+import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
+import { i18n, updateI18n } from '@/i18n';
+import { confirm, alert, post, popup, toast } from '@/os';
+import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
+import { defaultStore, ColdDeviceStorage } from '@/store';
+import { fetchInstance, instance } from '@/instance';
+import { deviceKind } from '@/scripts/device-kind';
+import { reloadChannel } from '@/scripts/unison-reload';
+import { reactionPicker } from '@/scripts/reaction-picker';
+import { getUrlWithoutLoginId } from '@/scripts/login-id';
+import { getAccountFromId } from '@/scripts/get-account-from-id';
+import { deckStore } from '@/ui/deck/deck-store';
+import { miLocalStorage } from '@/local-storage';
+import { fetchCustomEmojis } from '@/custom-emojis';
+import { mainRouter } from '@/router';
+
+export async function common(createVue: () => App<Element>) {
+	console.info(`Misskey v${version}`);
+
+	if (_DEV_) {
+		console.warn('Development mode!!!');
+
+		console.info(`vue ${vueVersion}`);
+
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		(window as any).$i = $i;
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		(window as any).$store = defaultStore;
+
+		window.addEventListener('error', event => {
+			console.error(event);
+			/*
+			alert({
+				type: 'error',
+				title: 'DEV: Unhandled error',
+				text: event.message
+			});
+			*/
+		});
+
+		window.addEventListener('unhandledrejection', event => {
+			console.error(event);
+			/*
+			alert({
+				type: 'error',
+				title: 'DEV: Unhandled promise rejection',
+				text: event.reason
+			});
+			*/
+		});
+	}
+
+	const splash = document.getElementById('splash');
+	// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
+	if (splash) splash.addEventListener('transitionend', () => {
+		splash.remove();
+	});
+
+	let isClientUpdated = false;
+
+	//#region クライアントが更新されたかチェック
+	const lastVersion = miLocalStorage.getItem('lastVersion');
+	if (lastVersion !== version) {
+		miLocalStorage.setItem('lastVersion', version);
+
+		// テーマリビルドするため
+		miLocalStorage.removeItem('theme');
+
+		try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
+			if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
+				isClientUpdated = true;
+			}
+		} catch (err) { /* empty */ }
+	}
+	//#endregion
+
+	//#region Detect language & fetch translations
+	const localeVersion = miLocalStorage.getItem('localeVersion');
+	const localeOutdated = (localeVersion == null || localeVersion !== version);
+	if (localeOutdated) {
+		const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
+		if (res.status === 200) {
+			const newLocale = await res.text();
+			const parsedNewLocale = JSON.parse(newLocale);
+			miLocalStorage.setItem('locale', newLocale);
+			miLocalStorage.setItem('localeVersion', version);
+			updateLocale(parsedNewLocale);
+			updateI18n(parsedNewLocale);
+		}
+	}
+	//#endregion
+
+	// タッチデバイスでCSSの:hoverを機能させる
+	document.addEventListener('touchend', () => {}, { passive: true });
+
+	// 一斉リロード
+	reloadChannel.addEventListener('message', path => {
+		if (path !== null) location.href = path;
+		else location.reload();
+	});
+
+	// If mobile, insert the viewport meta tag
+	if (['smartphone', 'tablet'].includes(deviceKind)) {
+		const viewport = 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;
+	html.setAttribute('lang', lang);
+	//#endregion
+
+	await defaultStore.ready;
+	await deckStore.ready;
+
+	const fetchInstanceMetaPromise = fetchInstance();
+
+	fetchInstanceMetaPromise.then(() => {
+		miLocalStorage.setItem('v', instance.version);
+	});
+
+	//#region loginId
+	const params = new URLSearchParams(location.search);
+	const loginId = params.get('loginId');
+
+	if (loginId) {
+		const target = getUrlWithoutLoginId(location.href);
+
+		if (!$i || $i.id !== loginId) {
+			const account = await getAccountFromId(loginId);
+			if (account) {
+				await login(account.token, target);
+			}
+		}
+
+		history.replaceState({ misskey: 'loginId' }, '', target);
+	}
+	//#endregion
+
+	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
+	watch(defaultStore.reactiveState.darkMode, (darkMode) => {
+		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
+	}, { immediate: miLocalStorage.getItem('theme') == null });
+
+	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
+	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
+
+	watch(darkTheme, (theme) => {
+		if (defaultStore.state.darkMode) {
+			applyTheme(theme);
+		}
+	});
+
+	watch(lightTheme, (theme) => {
+		if (!defaultStore.state.darkMode) {
+			applyTheme(theme);
+		}
+	});
+
+	//#region Sync dark mode
+	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
+		defaultStore.set('darkMode', isDeviceDarkmode());
+	}
+
+	window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
+		if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
+			defaultStore.set('darkMode', mql.matches);
+		}
+	});
+	//#endregion
+
+	fetchInstanceMetaPromise.then(() => {
+		if (defaultStore.state.themeInitial) {
+			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
+			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
+			defaultStore.set('themeInitial', false);
+		}
+	});
+
+	watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
+		document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
+	}, { immediate: true });
+
+	watch(defaultStore.reactiveState.useBlurEffect, v => {
+		if (v) {
+			document.documentElement.style.removeProperty('--blur');
+		} else {
+			document.documentElement.style.setProperty('--blur', 'none');
+		}
+	}, { immediate: true });
+
+	//#region Fetch user
+	if ($i && $i.token) {
+		if (_DEV_) {
+			console.log('account cache found. refreshing...');
+		}
+
+		refreshAccount();
+	}
+	//#endregion
+
+	try {
+		await fetchCustomEmojis();
+	} catch (err) { /* empty */ }
+
+	const app = createVue();
+
+	if (_DEV_) {
+		app.config.performance = true;
+	}
+
+	widgets(app);
+	directives(app);
+	components(app);
+
+	// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
+	// なぜか2回実行されることがあるため、mountするdivを1つに制限する
+	const rootEl = ((): HTMLElement => {
+		const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
+
+		const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
+
+		if (currentRoot) {
+			console.warn('multiple import detected');
+			return currentRoot;
+		}
+
+		const root = document.createElement('div');
+		root.id = MISSKEY_MOUNT_DIV_ID;
+		document.body.appendChild(root);
+		return root;
+	})();
+
+	app.mount(rootEl);
+
+	// boot.jsのやつを解除
+	window.onerror = null;
+	window.onunhandledrejection = null;
+
+	removeSplash();
+
+	return {
+		isClientUpdated,
+		app,
+	};
+}
+
+function removeSplash() {
+	const splash = 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
new file mode 100644
index 0000000000..c0bfa4603e
--- /dev/null
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -0,0 +1,254 @@
+import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
+import { common } from './common';
+import { version, ui, lang, updateLocale } from '@/config';
+import { i18n, updateI18n } from '@/i18n';
+import { confirm, alert, post, popup, toast } from '@/os';
+import { useStream } from '@/stream';
+import * as sound from '@/scripts/sound';
+import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
+import { defaultStore, ColdDeviceStorage } from '@/store';
+import { makeHotkey } from '@/scripts/hotkey';
+import { reactionPicker } from '@/scripts/reaction-picker';
+import { miLocalStorage } from '@/local-storage';
+import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
+import { mainRouter } from '@/router';
+import { initializeSw } from '@/scripts/initialize-sw';
+
+export async function mainBoot() {
+	const { isClientUpdated } = await common(() => createApp(
+		new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
+		!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
+		ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
+		ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
+		defineAsyncComponent(() => import('@/ui/universal.vue')),
+	));
+
+	reactionPicker.init();
+
+	if (isClientUpdated && $i) {
+		popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
+	}
+
+	const stream = useStream();
+
+	let reloadDialogShowing = false;
+	stream.on('_disconnected_', async () => {
+		if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
+			location.reload();
+		} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
+			if (reloadDialogShowing) return;
+			reloadDialogShowing = true;
+			const { canceled } = await confirm({
+				type: 'warning',
+				title: i18n.ts.disconnectedFromServer,
+				text: i18n.ts.reloadConfirm,
+			});
+			reloadDialogShowing = false;
+			if (!canceled) {
+				location.reload();
+			}
+		}
+	});
+
+	for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
+		import('../plugin').then(async ({ install }) => {
+			// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
+			await new Promise(r => setTimeout(r, 0));
+			install(plugin);
+		});
+	}
+
+	const hotkeys = {
+		'd': (): void => {
+			defaultStore.set('darkMode', !defaultStore.state.darkMode);
+		},
+		's': (): void => {
+			mainRouter.push('/search');
+		},
+	};
+
+	if ($i) {
+		// only add post shortcuts if logged in
+		hotkeys['p|n'] = post;
+
+		defaultStore.loaded.then(() => {
+			if (defaultStore.state.accountSetupWizard !== -1) {
+				popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
+			}
+		});
+
+		if ($i.isDeleted) {
+			alert({
+				type: 'warning',
+				text: i18n.ts.accountDeletionInProgress,
+			});
+		}
+
+		const now = new Date();
+		const m = now.getMonth() + 1;
+		const d = now.getDate();
+		
+		if ($i.birthday) {
+			const bm = parseInt($i.birthday.split('-')[1]);
+			const bd = parseInt($i.birthday.split('-')[2]);
+			if (m === bm && d === bd) {
+				claimAchievement('loggedInOnBirthday');
+			}
+		}
+
+		if (m === 1 && d === 1) {
+			claimAchievement('loggedInOnNewYearsDay');
+		}
+
+		if ($i.loggedInDays >= 3) claimAchievement('login3');
+		if ($i.loggedInDays >= 7) claimAchievement('login7');
+		if ($i.loggedInDays >= 15) claimAchievement('login15');
+		if ($i.loggedInDays >= 30) claimAchievement('login30');
+		if ($i.loggedInDays >= 60) claimAchievement('login60');
+		if ($i.loggedInDays >= 100) claimAchievement('login100');
+		if ($i.loggedInDays >= 200) claimAchievement('login200');
+		if ($i.loggedInDays >= 300) claimAchievement('login300');
+		if ($i.loggedInDays >= 400) claimAchievement('login400');
+		if ($i.loggedInDays >= 500) claimAchievement('login500');
+		if ($i.loggedInDays >= 600) claimAchievement('login600');
+		if ($i.loggedInDays >= 700) claimAchievement('login700');
+		if ($i.loggedInDays >= 800) claimAchievement('login800');
+		if ($i.loggedInDays >= 900) claimAchievement('login900');
+		if ($i.loggedInDays >= 1000) claimAchievement('login1000');
+
+		if ($i.notesCount > 0) claimAchievement('notes1');
+		if ($i.notesCount >= 10) claimAchievement('notes10');
+		if ($i.notesCount >= 100) claimAchievement('notes100');
+		if ($i.notesCount >= 500) claimAchievement('notes500');
+		if ($i.notesCount >= 1000) claimAchievement('notes1000');
+		if ($i.notesCount >= 5000) claimAchievement('notes5000');
+		if ($i.notesCount >= 10000) claimAchievement('notes10000');
+		if ($i.notesCount >= 20000) claimAchievement('notes20000');
+		if ($i.notesCount >= 30000) claimAchievement('notes30000');
+		if ($i.notesCount >= 40000) claimAchievement('notes40000');
+		if ($i.notesCount >= 50000) claimAchievement('notes50000');
+		if ($i.notesCount >= 60000) claimAchievement('notes60000');
+		if ($i.notesCount >= 70000) claimAchievement('notes70000');
+		if ($i.notesCount >= 80000) claimAchievement('notes80000');
+		if ($i.notesCount >= 90000) claimAchievement('notes90000');
+		if ($i.notesCount >= 100000) claimAchievement('notes100000');
+
+		if ($i.followersCount > 0) claimAchievement('followers1');
+		if ($i.followersCount >= 10) claimAchievement('followers10');
+		if ($i.followersCount >= 50) claimAchievement('followers50');
+		if ($i.followersCount >= 100) claimAchievement('followers100');
+		if ($i.followersCount >= 300) claimAchievement('followers300');
+		if ($i.followersCount >= 500) claimAchievement('followers500');
+		if ($i.followersCount >= 1000) claimAchievement('followers1000');
+
+		if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
+			claimAchievement('passedSinceAccountCreated1');
+		}
+		if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
+			claimAchievement('passedSinceAccountCreated2');
+		}
+		if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
+			claimAchievement('passedSinceAccountCreated3');
+		}
+
+		if (claimedAchievements.length >= 30) {
+			claimAchievement('collectAchievements30');
+		}
+
+		window.setInterval(() => {
+			if (Math.floor(Math.random() * 20000) === 0) {
+				claimAchievement('justPlainLucky');
+			}
+		}, 1000 * 10);
+
+		window.setTimeout(() => {
+			claimAchievement('client30min');
+		}, 1000 * 60 * 30);
+
+		window.setTimeout(() => {
+			claimAchievement('client60min');
+		}, 1000 * 60 * 60);
+
+		const lastUsed = miLocalStorage.getItem('lastUsed');
+		if (lastUsed) {
+			const lastUsedDate = parseInt(lastUsed, 10);
+			// 二時間以上前なら
+			if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
+				toast(i18n.t('welcomeBackWithName', {
+					name: $i.name || $i.username,
+				}));
+			}
+		}
+		miLocalStorage.setItem('lastUsed', Date.now().toString());
+
+		const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
+		const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
+		if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
+			if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
+				popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
+			}
+		}
+
+		if ('Notification' in window) {
+			// 許可を得ていなかったらリクエスト
+			if (Notification.permission === 'default') {
+				Notification.requestPermission();
+			}
+		}
+
+		const main = markRaw(stream.useChannel('main', null, 'System'));
+
+		// 自分の情報が更新されたとき
+		main.on('meUpdated', i => {
+			updateAccount(i);
+		});
+
+		main.on('readAllNotifications', () => {
+			updateAccount({ hasUnreadNotification: false });
+		});
+
+		main.on('unreadNotification', () => {
+			updateAccount({ hasUnreadNotification: true });
+		});
+
+		main.on('unreadMention', () => {
+			updateAccount({ hasUnreadMentions: true });
+		});
+
+		main.on('readAllUnreadMentions', () => {
+			updateAccount({ hasUnreadMentions: false });
+		});
+
+		main.on('unreadSpecifiedNote', () => {
+			updateAccount({ hasUnreadSpecifiedNotes: true });
+		});
+
+		main.on('readAllUnreadSpecifiedNotes', () => {
+			updateAccount({ hasUnreadSpecifiedNotes: false });
+		});
+
+		main.on('readAllAntennas', () => {
+			updateAccount({ hasUnreadAntenna: false });
+		});
+
+		main.on('unreadAntenna', () => {
+			updateAccount({ hasUnreadAntenna: true });
+			sound.play('antenna');
+		});
+
+		main.on('readAllAnnouncements', () => {
+			updateAccount({ hasUnreadAnnouncement: false });
+		});
+
+		// トークンが再生成されたとき
+		// このままではMisskeyが利用できないので強制的にサインアウトさせる
+		main.on('myTokenRegenerated', () => {
+			signout();
+		});
+	}
+
+	// shortcut
+	document.addEventListener('keydown', makeHotkey(hotkeys));
+
+	initializeSw();
+}
diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts
new file mode 100644
index 0000000000..c2664f6c1d
--- /dev/null
+++ b/packages/frontend/src/boot/sub-boot.ts
@@ -0,0 +1,8 @@
+import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
+import { common } from './common';
+
+export async function subBoot() {
+	const { isClientUpdated } = await common(() => createApp(
+		defineAsyncComponent(() => import('@/ui/minimum.vue')),
+	));
+}
diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index 9f2bf99338..7a1b7d532e 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -9,7 +9,7 @@
 		</I18n>
 	</template>
 	<MkSpacer :margin-min="20" :margin-max="28">
-		<div class="dpvffvvy _gaps_m">
+		<div class="_gaps_m" :class="$style.root">
 			<div class="">
 				<MkTextarea v-model="comment">
 					<template #label>{{ i18n.ts.details }}</template>
@@ -60,8 +60,8 @@ function send() {
 }
 </script>
 
-<style lang="scss" scoped>
-.dpvffvvy {
+<style lang="scss" module>
+.root {
 	--root-margin: 16px;
 }
 </style>
diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue
new file mode 100644
index 0000000000..575ea7c5e3
--- /dev/null
+++ b/packages/frontend/src/components/MkAnimBg.vue
@@ -0,0 +1,243 @@
+<template>
+<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, shallowRef } from 'vue';
+import isChromatic from 'chromatic/isChromatic';
+
+const canvasEl = shallowRef<HTMLCanvasElement>();
+
+const props = withDefaults(defineProps<{
+	scale?: number;
+	focus?: number;
+}>(), {
+	scale: 1.0,
+	focus: 1.0,
+});
+
+function loadShader(gl, type, source) {
+	const shader = gl.createShader(type);
+
+	gl.shaderSource(shader, source);
+	gl.compileShader(shader);
+
+	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+		alert(
+			`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
+		);
+		gl.deleteShader(shader);
+		return null;
+	}
+
+	return shader;
+}
+
+function initShaderProgram(gl, vsSource, fsSource) {
+	const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
+	const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
+
+	const shaderProgram = gl.createProgram();
+	gl.attachShader(shaderProgram, vertexShader);
+	gl.attachShader(shaderProgram, fragmentShader);
+	gl.linkProgram(shaderProgram);
+
+	if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
+		alert(
+			`failed to init shader: ${gl.getProgramInfoLog(
+				shaderProgram,
+			)}`,
+		);
+		return null;
+	}
+
+	return shaderProgram;
+}
+
+let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
+
+onMounted(() => {
+	const canvas = canvasEl.value!;
+	canvas.width = canvas.offsetWidth;
+	canvas.height = canvas.offsetHeight;
+
+	const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
+	if (gl == null) return;
+
+	gl.clearColor(0.0, 0.0, 0.0, 0.0);
+	gl.clear(gl.COLOR_BUFFER_BIT);
+
+	const positionBuffer = gl.createBuffer();
+	gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+
+	const shaderProgram = initShaderProgram(gl, `
+		attribute vec2 vertex;
+
+		uniform vec2 u_scale;
+
+		varying vec2 v_pos;
+
+		void main() {
+			gl_Position = vec4(vertex, 0.0, 1.0);
+			v_pos = vertex / u_scale;
+		}
+	`, `
+		precision mediump float;
+
+		vec3 mod289(vec3 x) {
+			return x - floor(x * (1.0 / 289.0)) * 289.0;
+		}
+
+		vec2 mod289(vec2 x) {
+			return x - floor(x * (1.0 / 289.0)) * 289.0;
+		}
+
+		vec3 permute(vec3 x) {
+			return mod289(((x*34.0)+1.0)*x);
+		}
+
+		float snoise(vec2 v) {
+			const vec4 C = vec4(0.211324865405187,
+													0.366025403784439,
+												-0.577350269189626,
+													0.024390243902439);
+
+			vec2 i  = floor(v + dot(v, C.yy) );
+			vec2 x0 = v -   i + dot(i, C.xx);
+
+			vec2 i1;
+			i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
+			vec4 x12 = x0.xyxy + C.xxzz;
+			x12.xy -= i1;
+
+			i = mod289(i);
+			vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+				+ i.x + vec3(0.0, i1.x, 1.0 ));
+
+			vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
+			m = m*m ;
+			m = m*m ;
+
+			vec3 x = 2.0 * fract(p * C.www) - 1.0;
+			vec3 h = abs(x) - 0.5;
+			vec3 ox = floor(x + 0.5);
+			vec3 a0 = x - ox;
+
+			m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
+
+			vec3 g;
+			g.x  = a0.x  * x0.x  + h.x  * x0.y;
+			g.yz = a0.yz * x12.xz + h.yz * x12.yw;
+			return 130.0 * dot(m, g);
+		}
+
+		uniform float u_time;
+		uniform vec2 u_resolution;
+		uniform float u_spread;
+		uniform float u_speed;
+		uniform float u_warp;
+		uniform float u_focus;
+		uniform float u_itensity;
+
+		varying vec2 v_pos;
+
+		float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
+			float SPREAD = 0.7 * u_spread;
+			float SPEED = 0.00055 * u_speed;
+			float WARP = 1.5 * u_warp;
+			float FOCUS = 1.15 * u_focus;
+
+			vec2 dist = _pos - _origin;
+
+			float distortion = snoise( vec2(
+				_pos.x * 1.587 * WARP + u_time * SPEED * 0.5,
+				_pos.y * 1.192 * WARP + u_time * SPEED * 0.3
+			) ) * 0.5 + 0.5;
+
+			float feather = 0.01 + SPREAD * pow( distortion, FOCUS );
+
+			return 1.0 - smoothstep(
+				_radius - ( _radius * feather ),
+				_radius + ( _radius * feather ),
+				dot( dist, dist ) * 4.0
+			);
+		}
+
+		void main() {
+			vec3 green = vec3( 1.0 ) - vec3( 153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0 );
+			vec3 purple = vec3( 1.0 ) - vec3( 195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0 );
+			vec3 orange = vec3( 1.0 ) - vec3( 255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0 );
+
+			float ratio = u_resolution.x / u_resolution.y;
+
+			vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
+
+			vec3 color = vec3( 0.0 );
+
+			float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
+			float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
+			float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
+
+			float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
+			float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
+			float alphaThree = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 3917.0 ) * 0.00013, uv.x ) ) * 0.5 + 0.5, 1.2 );
+
+			color += vec3( circle( uv, vec2( 0.22 + sin( u_time * 0.000201 ) * 0.06, 0.80 + cos( u_time * 0.000151 ) * 0.06 ), 0.15 ) ) * alphaOne * ( purple * purpleMix + orange * orangeMix );
+			color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
+			color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
+
+			color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
+
+			vec3 inverted = vec3( 1.0 ) - color;
+			gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
+		}
+	`);
+
+	gl.useProgram(shaderProgram);
+	const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution');
+	const u_time = gl.getUniformLocation(shaderProgram, 'u_time');
+	const u_spread = gl.getUniformLocation(shaderProgram, 'u_spread');
+	const u_speed = gl.getUniformLocation(shaderProgram, 'u_speed');
+	const u_warp = gl.getUniformLocation(shaderProgram, 'u_warp');
+	const u_focus = gl.getUniformLocation(shaderProgram, 'u_focus');
+	const u_itensity = gl.getUniformLocation(shaderProgram, 'u_itensity');
+	const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale');
+	gl.uniform2fv(u_resolution, [canvas.width, canvas.height]);
+	gl.uniform1f(u_spread, 1.0);
+	gl.uniform1f(u_speed, 1.0);
+	gl.uniform1f(u_warp, 1.0);
+	gl.uniform1f(u_focus, props.focus);
+	gl.uniform1f(u_itensity, 0.5);
+	gl.uniform2fv(u_scale, [props.scale, props.scale]);
+
+	const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
+	gl.enableVertexAttribArray(vertex);
+	gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
+
+	const vertices = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
+	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);
+
+	if (isChromatic()) {
+		gl!.uniform1f(u_time, 0);
+		gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
+	} else {
+		function render(timeStamp) {
+			gl!.uniform1f(u_time, timeStamp);
+			gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
+
+			handle = window.requestAnimationFrame(render);
+		}
+
+		handle = window.requestAnimationFrame(render);
+	}
+});
+
+onUnmounted(() => {
+	if (handle) {
+		window.cancelAnimationFrame(handle);
+	}
+});
+</script>
+
+<style lang="scss" module>
+</style>
diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue
index 408eab7399..4050520eb9 100644
--- a/packages/frontend/src/components/MkChannelList.vue
+++ b/packages/frontend/src/components/MkChannelList.vue
@@ -26,6 +26,3 @@ const props = withDefaults(defineProps<{
 	extractor: (item) => item,
 });
 </script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index bfec57d6a0..edeaabfba4 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -95,7 +95,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue';
 import XFolder from '@/components/MkDrive.folder.vue';
 import XFile from '@/components/MkDrive.file.vue';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
 import { uploadFile, uploads } from '@/scripts/upload';
@@ -131,7 +131,7 @@ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
 const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
 const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
 const uploadings = uploads;
-const connection = stream.useChannel('drive');
+const connection = useStream().useChannel('drive');
 const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
 
 // ドロップされようとしているか
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index 475e01c8d4..f52c66a8be 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="ssazuxis">
+<div ref="el" class="ssazuxis">
 	<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
 		<div class="title"><div><slot name="header"></slot></div></div>
 		<div class="divider"></div>
@@ -22,80 +22,67 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, ref, shallowRef, watch } from 'vue';
 import tinycolor from 'tinycolor2';
 import { miLocalStorage } from '@/local-storage';
 import { defaultStore } from '@/store';
 
 const miLocalStoragePrefix = 'ui:folder:' as const;
 
-export default defineComponent({
-	props: {
-		expanded: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		persistKey: {
-			type: String,
-			required: false,
-			default: null,
-		},
-	},
-	data() {
-		return {
-			defaultStore,
-			bg: null,
-			showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
-		};
-	},
-	watch: {
-		showBody() {
-			if (this.persistKey) {
-				miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
-			}
-		},
-	},
-	mounted() {
-		function getParentBg(el: Element | null): string {
-			if (el == null || el.tagName === 'BODY') return 'var(--bg)';
-			const bg = el.style.background || el.style.backgroundColor;
-			if (bg) {
-				return bg;
-			} else {
-				return getParentBg(el.parentElement);
-			}
-		}
-		const rawBg = getParentBg(this.$el);
-		const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
-		bg.setAlpha(0.85);
-		this.bg = bg.toRgbString();
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			this.showBody = show;
-		},
+const props = withDefaults(defineProps<{
+	expanded?: boolean;
+	persistKey?: string;
+}>(), {
+	expanded: true,
+});
 
-		enter(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = 0;
-			el.offsetHeight; // reflow
-			el.style.height = elementHeight + 'px';
-		},
-		afterEnter(el) {
-			el.style.height = null;
-		},
-		leave(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = elementHeight + 'px';
-			el.offsetHeight; // reflow
-			el.style.height = 0;
-		},
-		afterLeave(el) {
-			el.style.height = null;
-		},
-	},
+const el = shallowRef<HTMLDivElement>();
+const bg = ref<string | null>(null);
+const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
+
+watch(showBody, () => {
+	if (props.persistKey) {
+		miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f');
+	}
+});
+
+function enter(el: Element) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = 0;
+	el.offsetHeight; // reflow
+	el.style.height = elementHeight + 'px';
+}
+
+function afterEnter(el: Element) {
+	el.style.height = null;
+}
+
+function leave(el: Element) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = elementHeight + 'px';
+	el.offsetHeight; // reflow
+	el.style.height = 0;
+}
+
+function afterLeave(el: Element) {
+	el.style.height = null;
+}
+
+onMounted(() => {
+	function getParentBg(el: HTMLElement | null): string {
+		if (el == null || el.tagName === 'BODY') return 'var(--bg)';
+		const bg = el.style.background || el.style.backgroundColor;
+		if (bg) {
+			return bg;
+		} else {
+			return getParentBg(el.parentElement);
+		}
+	}
+	const rawBg = getParentBg(el.value);
+	const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+	_bg.setAlpha(0.85);
+	bg.value = _bg.toRgbString();
 });
 </script>
 
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 10eee6aab1..8800f31400 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -6,7 +6,7 @@
 				<div :class="$style.headerIcon"><slot name="icon"></slot></div>
 				<div :class="$style.headerText">
 					<div :class="$style.headerTextMain">
-						<slot name="label"></slot>
+						<MkCondensedLine :min-scale="2 / 3"><slot name="label"></slot></MkCondensedLine>
 					</div>
 					<div :class="$style.headerTextSub">
 						<slot name="caption"></slot>
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index beee21c647..7d066fd033 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -33,7 +33,7 @@
 import { onBeforeUnmount, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import { claimAchievement } from '@/scripts/achievements';
 import { $i } from '@/account';
@@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{
 let isFollowing = $ref(props.user.isFollowing);
 let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
 let wait = $ref(false);
-const connection = stream.useChannel('main');
+const connection = useStream().useChannel('main');
 
 if (props.user.isFollowing == null) {
 	os.api('users/show', {
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 979df2e7c1..b4ef54aecd 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -54,8 +54,8 @@
 </MkModalWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { reactive, shallowRef } from 'vue';
 import MkInput from './MkInput.vue';
 import MkTextarea from './MkTextarea.vue';
 import MkSwitch from './MkSwitch.vue';
@@ -66,58 +66,36 @@ import MkRadios from './MkRadios.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkModalWindow,
-		MkInput,
-		MkTextarea,
-		MkSwitch,
-		MkSelect,
-		MkRange,
-		MkButton,
-		MkRadios,
-	},
+const props = defineProps<{
+	title: string;
+	form: any;
+}>();
 
-	props: {
-		title: {
-			type: String,
-			required: true,
-		},
-		form: {
-			type: Object,
-			required: true,
-		},
-	},
+const emit = defineEmits<{
+	(ev: 'done', v: {
+		canceled?: boolean;
+		result?: any;
+	}): void;
+}>();
 
-	emits: ['done'],
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const values = reactive({});
 
-	data() {
-		return {
-			values: {},
-			i18n,
-		};
-	},
+for (const item in props.form) {
+	values[item] = props.form[item].default ?? null;
+}
 
-	created() {
-		for (const item in this.form) {
-			this.values[item] = this.form[item].default ?? null;
-		}
-	},
+function ok() {
+	emit('done', {
+		result: values,
+	});
+	dialog.value.close();
+}
 
-	methods: {
-		ok() {
-			this.$emit('done', {
-				result: this.values,
-			});
-			this.$refs.dialog.close();
-		},
-
-		cancel() {
-			this.$emit('done', {
-				canceled: true,
-			});
-			this.$refs.dialog.close();
-		},
-	},
-});
+function cancel() {
+	emit('done', {
+		canceled: true,
+	});
+	dialog.value.close();
+}
 </script>
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 1aea95fe0e..8cb1b064ba 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -22,7 +22,7 @@ import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import XNotification from '@/components/MkNotification.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { notificationTypes } from '@/const';
@@ -45,7 +45,7 @@ const pagination: Paging = {
 const onNotification = (notification) => {
 	const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
 	if (isMuted || document.visibilityState === 'visible') {
-		stream.send('readNotification');
+		useStream().send('readNotification');
 	}
 
 	if (!isMuted) {
@@ -56,7 +56,7 @@ const onNotification = (notification) => {
 let connection;
 
 onMounted(() => {
-	connection = stream.useChannel('main');
+	connection = useStream().useChannel('main');
 	connection.on('notification', onNotification);
 });
 
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index e7fc73bce3..d48e7886eb 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -28,54 +28,38 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, reactive } from 'vue';
+<script lang="ts" setup>
+import { reactive } from 'vue';
 import number from '@/filters/number';
+import XValue from '@/components/MkObjectView.value.vue';
 
-export default defineComponent({
-	name: 'XValue',
+const props = defineProps<{
+	value: any;
+}>();
 
-	props: {
-		value: {
-			required: true,
-		},
-	},
+const collapsed = reactive({});
 
-	setup(props) {
-		const collapsed = reactive({});
+if (isObject(props.value)) {
+	for (const key in props.value) {
+		collapsed[key] = collapsable(props.value[key]);
+	}
+}
 
-		if (isObject(props.value)) {
-			for (const key in props.value) {
-				collapsed[key] = collapsable(props.value[key]);
-			}
-		}
+function isObject(v): boolean {
+	return typeof v === 'object' && !Array.isArray(v) && v !== null;
+}
 
-		function isObject(v): boolean {
-			return typeof v === 'object' && !Array.isArray(v) && v !== null;
-		}
+function isArray(v): boolean {
+	return Array.isArray(v);
+}
 
-		function isArray(v): boolean {
-			return Array.isArray(v);
-		}
+function isEmpty(v): boolean {
+	return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
+}
 
-		function isEmpty(v): boolean {
-			return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
-		}
-
-		function collapsable(v): boolean {
-			return (isObject(v) || isArray(v)) && !isEmpty(v);
-		}
-
-		return {
-			number,
-			collapsed,
-			isObject,
-			isArray,
-			isEmpty,
-			collapsable,
-		};
-	},
-});
+function collapsable(v): boolean {
+	return (isObject(v) || isArray(v)) && !isEmpty(v);
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkObjectView.vue b/packages/frontend/src/components/MkObjectView.vue
index 55578a37f6..8b1ed74142 100644
--- a/packages/frontend/src/components/MkObjectView.vue
+++ b/packages/frontend/src/components/MkObjectView.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="zhyxdalp">
+<div>
 	<XValue :value="value" :collapsed="false"/>
 </div>
 </template>
@@ -12,9 +12,3 @@ const props = defineProps<{
 	value: Record<string, unknown>;
 }>();
 </script>
-
-<style lang="scss" scoped>
-.zhyxdalp {
-
-}
-</style>
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 02ce58451d..bd842c486a 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -12,12 +12,12 @@
 >
 	<template #header>
 		<template v-if="pageMetadata?.value">
-			<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
+			<i v-if="pageMetadata.value.icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
 			<span>{{ pageMetadata.value.title }}</span>
 		</template>
 	</template>
 
-	<div :class="$style.root" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
+	<div :class="$style.root" style="container-type: inline-size;">
 		<RouterView :key="reloadCount" :router="router"/>
 	</div>
 </MkWindow>
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index e2240fb4e1..84be10078a 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -1,37 +1,27 @@
 <script lang="ts">
-import { VNode, defineComponent, h } from 'vue';
+import { VNode, defineComponent, h, ref, watch } from 'vue';
 import MkRadio from './MkRadio.vue';
 
 export default defineComponent({
-	components: {
-		MkRadio,
-	},
 	props: {
 		modelValue: {
 			required: false,
 		},
 	},
-	data() {
-		return {
-			value: this.modelValue,
-		};
-	},
-	watch: {
-		value() {
-			this.$emit('update:modelValue', this.value);
-		},
-	},
-	render() {
-		console.log(this.$slots, this.$slots.label && this.$slots.label());
-		if (!this.$slots.default) return null;
-		let options = this.$slots.default();
-		const label = this.$slots.label && this.$slots.label();
-		const caption = this.$slots.caption && this.$slots.caption();
+	setup(props, context) {
+		const value = ref(props.modelValue);
+		watch(value, () => {
+			context.emit('update:modelValue', value.value);
+		});
+		if (!context.slots.default) return null;
+		let options = context.slots.default();
+		const label = context.slots.label && context.slots.label();
+		const caption = context.slots.caption && context.slots.caption();
 
 		// なぜかFragmentになることがあるため
 		if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
 
-		return h('div', {
+		return () => h('div', {
 			class: 'novjtcto',
 		}, [
 			...(label ? [h('div', {
@@ -42,8 +32,8 @@ export default defineComponent({
 			}, options.map(option => h(MkRadio, {
 				key: option.key,
 				value: option.props?.value,
-				modelValue: this.value,
-				'onUpdate:modelValue': value => this.value = value,
+				modelValue: value.value,
+				'onUpdate:modelValue': _v => value.value = _v,
 			}, () => option.children)),
 			),
 			...(caption ? [h('div', {
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index 8bd0279806..9f56189f3e 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -124,7 +124,3 @@ onMounted(async () => {
 	});
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue
index 9d93211d5f..60c3a47385 100644
--- a/packages/frontend/src/components/MkRippleEffect.vue
+++ b/packages/frontend/src/components/MkRippleEffect.vue
@@ -1,7 +1,7 @@
 <template>
-<div class="vswabwbm" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
+<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
 	<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
-		<circle fill="none" cx="64" cy="64">
+		<circle fill="none" cx="64" cy="64" style="stroke: var(--accent);">
 			<animate
 				attributeName="r"
 				begin="0s" dur="0.5s"
@@ -22,7 +22,7 @@
 			/>
 		</circle>
 		<g fill="none" fill-rule="evenodd">
-			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
+			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);">
 				<animate
 					attributeName="r"
 					begin="0s" dur="0.8s"
@@ -100,17 +100,11 @@ onMounted(() => {
 });
 </script>
 
-<style lang="scss" scoped>
-.vswabwbm {
+<style lang="scss" module>
+.root {
 	pointer-events: none;
 	position: fixed;
 	width: 128px;
 	height: 128px;
-
-	> svg {
-		> circle {
-			stroke: var(--accent);
-		}
-	}
 }
 </style>
diff --git a/packages/frontend/src/components/MkSample.vue b/packages/frontend/src/components/MkSample.vue
deleted file mode 100644
index 922b862b47..0000000000
--- a/packages/frontend/src/components/MkSample.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-<div class="">
-	<div class="">
-		<MkInput v-model="text">
-			<template #label>Text</template>
-		</MkInput>
-		<MkSwitch v-model="flag">
-			<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
-		</MkSwitch>
-		<div style="margin: 32px 0;">
-			<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
-			<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
-			<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
-		</div>
-		<MkButton inline>This is</MkButton>
-		<MkButton inline primary>the button</MkButton>
-	</div>
-	<div class="" style="pointer-events: none;">
-		<Mfm :text="mfm"/>
-	</div>
-	<div class="">
-		<MkButton inline primary @click="openMenu">Open menu</MkButton>
-		<MkButton inline primary @click="openDialog">Open dialog</MkButton>
-		<MkButton inline primary @click="openForm">Open form</MkButton>
-		<MkButton inline primary @click="openDrive">Open drive</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkSwitch from '@/components/MkSwitch.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
-import MkRadio from '@/components/MkRadio.vue';
-import * as os from '@/os';
-import * as config from '@/config';
-import { $i } from '@/account';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSwitch,
-		MkTextarea,
-		MkRadio,
-	},
-
-	data() {
-		return {
-			text: '',
-			flag: true,
-			radio: 'misskey',
-			$i,
-			mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`,
-		};
-	},
-
-	methods: {
-		async openDialog() {
-			os.alert({
-				type: 'warning',
-				title: 'Oh my Aichan',
-				text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
-			});
-		},
-
-		async openForm() {
-			os.form('Example form', {
-				foo: {
-					type: 'boolean',
-					default: true,
-					label: 'This is a boolean property',
-				},
-				bar: {
-					type: 'number',
-					default: 300,
-					label: 'This is a number property',
-				},
-				baz: {
-					type: 'string',
-					default: 'Misskey makes you happy.',
-					label: 'This is a string property',
-				},
-			});
-		},
-
-		async openDrive() {
-			os.selectDriveFile(false);
-		},
-
-		async selectUser() {
-			os.selectUser();
-		},
-
-		async openMenu(ev) {
-			os.popupMenu([{
-				type: 'label',
-				text: 'Fruits',
-			}, {
-				text: 'Create some apples',
-				action: () => {},
-			}, {
-				text: 'Read some oranges',
-				action: () => {},
-			}, {
-				text: 'Update some melons',
-				action: () => {},
-			}, null, {
-				text: 'Delete some bananas',
-				danger: true,
-				action: () => {},
-			}], ev.currentTarget ?? ev.target);
-		},
-	},
-});
-</script>
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index ffc5e82b56..6eae8ecf84 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -1,7 +1,7 @@
 <template>
-<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
-	<div class="auth _gaps_m">
-		<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
+<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
+	<div class="_gaps_m">
+		<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
 		<MkInfo v-if="message">
 			{{ message }}
 		</MkInfo>
@@ -236,18 +236,14 @@ function resetPassword() {
 }
 </script>
 
-<style lang="scss" scoped>
-.eppvobhk {
-	> .auth {
-		> .avatar {
-			margin: 0 auto 0 auto;
-			width: 64px;
-			height: 64px;
-			background: #ddd;
-			background-position: center;
-			background-size: cover;
-			border-radius: 100%;
-		}
-	}
+<style lang="scss" module>
+.avatar {
+	margin: 0 auto 0 auto;
+	width: 64px;
+	height: 64px;
+	background: #ddd;
+	background-position: center;
+	background-size: cover;
+	border-radius: 100%;
 }
 </style>
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 2a8e43c570..72b70416d9 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -23,22 +23,13 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 
-export default defineComponent({
-	props: {
-		def: {
-			type: Array,
-			required: true,
-		},
-		grid: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-});
+defineProps<{
+	def: any[];
+	grid?: boolean;
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index 6f819bbbd7..7274f9b310 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -7,17 +7,17 @@ export default defineComponent({
 			required: true,
 		},
 	},
-	render() {
-		const options = this.$slots.default();
+	setup(props, { emit, slots }) {
+		const options = slots.default();
 
-		return h('div', {
+		return () => h('div', {
 			class: 'pxhvhrfw',
 		}, options.map(option => withDirectives(h('button', {
-			class: ['_button', { active: this.modelValue === option.props.value }],
+			class: ['_button', { active: props.modelValue === option.props.value }],
 			key: option.key,
-			disabled: this.modelValue === option.props.value,
+			disabled: props.modelValue === option.props.value,
 			onClick: () => {
-				this.$emit('update:modelValue', option.props.value);
+				emit('update:modelValue', option.props.value);
 			},
 		}, option.children), [
 			[resolveDirective('click-anime')],
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 82b631edda..e8f10ab048 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -26,153 +26,88 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+<script lang="ts" setup>
+import { onMounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
 import { debounce } from 'throttle-debounce';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkButton,
-	},
+const props = defineProps<{
+	modelValue: string | null;
+	required?: boolean;
+	readonly?: boolean;
+	disabled?: boolean;
+	pattern?: string;
+	placeholder?: string;
+	autofocus?: boolean;
+	autocomplete?: string;
+	spellcheck?: boolean;
+	debounce?: boolean;
+	manualSave?: boolean;
+	code?: boolean;
+	tall?: boolean;
+	pre?: boolean;
+}>();
 
-	props: {
-		modelValue: {
-			required: true,
-		},
-		type: {
-			type: String,
-			required: false,
-		},
-		required: {
-			type: Boolean,
-			required: false,
-		},
-		readonly: {
-			type: Boolean,
-			required: false,
-		},
-		disabled: {
-			type: Boolean,
-			required: false,
-		},
-		pattern: {
-			type: String,
-			required: false,
-		},
-		placeholder: {
-			type: String,
-			required: false,
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		autocomplete: {
-			required: false,
-		},
-		spellcheck: {
-			required: false,
-		},
-		code: {
-			type: Boolean,
-			required: false,
-		},
-		tall: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		pre: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		debounce: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		manualSave: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
+const emit = defineEmits<{
+	(ev: 'change', _ev: KeyboardEvent): void;
+	(ev: 'keydown', _ev: KeyboardEvent): void;
+	(ev: 'enter'): void;
+	(ev: 'update:modelValue', value: string): void;
+}>();
 
-	emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+const { modelValue, autofocus } = toRefs(props);
+const v = ref<string>(modelValue.value ?? '');
+const focused = ref(false);
+const changed = ref(false);
+const invalid = ref(false);
+const filled = computed(() => v.value !== '' && v.value != null);
+const inputEl = shallowRef<HTMLTextAreaElement>();
 
-	setup(props, context) {
-		const { modelValue, autofocus } = toRefs(props);
-		const v = ref(modelValue.value);
-		const focused = ref(false);
-		const changed = ref(false);
-		const invalid = ref(false);
-		const filled = computed(() => v.value !== '' && v.value != null);
-		const inputEl = ref(null);
+const focus = () => inputEl.value.focus();
+const onInput = (ev) => {
+	changed.value = true;
+	emit('change', ev);
+};
+const onKeydown = (ev: KeyboardEvent) => {
+	if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
 
-		const focus = () => inputEl.value.focus();
-		const onInput = (ev) => {
-			changed.value = true;
-			context.emit('change', ev);
-		};
-		const onKeydown = (ev: KeyboardEvent) => {
-			if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
+	emit('keydown', ev);
 
-			context.emit('keydown', ev);
+	if (ev.code === 'Enter') {
+		emit('enter');
+	}
+};
 
-			if (ev.code === 'Enter') {
-				context.emit('enter');
-			}
-		};
+const updated = () => {
+	changed.value = false;
+	emit('update:modelValue', v.value ?? '');
+};
 
-		const updated = () => {
-			changed.value = false;
-			context.emit('update:modelValue', v.value);
-		};
+const debouncedUpdated = debounce(1000, updated);
 
-		const debouncedUpdated = debounce(1000, updated);
+watch(modelValue, newValue => {
+	v.value = newValue;
+});
 
-		watch(modelValue, newValue => {
-			v.value = newValue;
-		});
+watch(v, newValue => {
+	if (!props.manualSave) {
+		if (props.debounce) {
+			debouncedUpdated();
+		} else {
+			updated();
+		}
+	}
 
-		watch(v, newValue => {
-			if (!props.manualSave) {
-				if (props.debounce) {
-					debouncedUpdated();
-				} else {
-					updated();
-				}
-			}
+	invalid.value = inputEl.value.validity.badInput;
+});
 
-			invalid.value = inputEl.value.validity.badInput;
-		});
-
-		onMounted(() => {
-			nextTick(() => {
-				if (autofocus.value) {
-					focus();
-				}
-			});
-		});
-
-		return {
-			v,
-			focused,
-			invalid,
-			changed,
-			filled,
-			inputEl,
-			focus,
-			onInput,
-			onKeydown,
-			updated,
-			i18n,
-		};
-	},
+onMounted(() => {
+	nextTick(() => {
+		if (autofocus.value) {
+			focus();
+		}
+	});
 });
 </script>
 
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index fb0a3a4b67..25d813e39f 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -5,7 +5,7 @@
 <script lang="ts" setup>
 import { computed, provide, onUnmounted } from 'vue';
 import MkNotes from '@/components/MkNotes.vue';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import * as sound from '@/scripts/sound';
 import { $i } from '@/account';
 import { defaultStore } from '@/store';
@@ -57,6 +57,8 @@ let query;
 let connection;
 let connection2;
 
+const stream = useStream();
+
 if (props.src === 'antenna') {
 	endpoint = 'antennas/notes';
 	query = {
@@ -68,7 +70,12 @@ if (props.src === 'antenna') {
 	connection.on('note', prepend);
 } else if (props.src === 'home') {
 	endpoint = 'notes/timeline';
-	connection = stream.useChannel('homeTimeline');
+	query = {
+		withReplies: defaultStore.state.showTimelineReplies,
+	};
+	connection = stream.useChannel('homeTimeline', {
+		withReplies: defaultStore.state.showTimelineReplies,
+	});
 	connection.on('note', prepend);
 
 	connection2 = stream.useChannel('main');
@@ -76,15 +83,30 @@ if (props.src === 'antenna') {
 	connection2.on('unfollow', onChangeFollowing);
 } else if (props.src === 'local') {
 	endpoint = 'notes/local-timeline';
-	connection = stream.useChannel('localTimeline');
+	query = {
+		withReplies: defaultStore.state.showTimelineReplies,
+	};
+	connection = stream.useChannel('localTimeline', {
+		withReplies: defaultStore.state.showTimelineReplies,
+	});
 	connection.on('note', prepend);
 } else if (props.src === 'social') {
 	endpoint = 'notes/hybrid-timeline';
-	connection = stream.useChannel('hybridTimeline');
+	query = {
+		withReplies: defaultStore.state.showTimelineReplies,
+	};
+	connection = stream.useChannel('hybridTimeline', {
+		withReplies: defaultStore.state.showTimelineReplies,
+	});
 	connection.on('note', prepend);
 } else if (props.src === 'global') {
 	endpoint = 'notes/global-timeline';
-	connection = stream.useChannel('globalTimeline');
+	query = {
+		withReplies: defaultStore.state.showTimelineReplies,
+	};
+	connection = stream.useChannel('globalTimeline', {
+		withReplies: defaultStore.state.showTimelineReplies,
+	});
 	connection.on('note', prepend);
 } else if (props.src === 'mentions') {
 	endpoint = 'notes/mentions';
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index 2d34b090ed..ea39198706 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -41,6 +41,9 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
+// タイミングによっては最初から showing = false な場合があり、その場合に closed 扱いにしないと永久にDOMに残ることになる
+if (!props.showing) emit('closed');
+
 const el = shallowRef<HTMLElement>();
 const zIndex = os.claimZIndex('high');
 
@@ -66,10 +69,8 @@ onMounted(() => {
 		setPosition();
 
 		const loop = () => {
-			loopHandler = window.requestAnimationFrame(() => {
-				setPosition();
-				loop();
-			});
+			setPosition();
+			loopHandler = window.requestAnimationFrame(loop);
 		};
 
 		loop();
diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue
index e244be3e96..30204b91c1 100644
--- a/packages/frontend/src/components/MkUrlPreviewPopup.vue
+++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
+<div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
 	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @after-leave="emit('closed')">
 		<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/>
 	</Transition>
@@ -36,8 +36,8 @@ onMounted(() => {
 });
 </script>
 
-<style lang="scss" scoped>
-.fgmtyycl {
+<style lang="scss" module>
+.root {
 	position: absolute;
 	width: 500px;
 	max-width: calc(90vw - 12px);
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
index e9f4f68df8..5cea67ccf5 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -4,6 +4,7 @@
 
 	<MkFolder>
 		<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
+		<template #icon><i class="ti ti-lock"></i></template>
 		<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
 
 		<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
@@ -11,6 +12,7 @@
 
 	<MkFolder>
 		<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
+		<template #icon><i class="ti ti-eye-off"></i></template>
 		<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
 
 		<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
@@ -18,6 +20,7 @@
 
 	<MkFolder>
 		<template #label>{{ i18n.ts.noCrawle }}</template>
+		<template #icon><i class="ti ti-world-x"></i></template>
 		<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
 
 		<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
@@ -25,6 +28,7 @@
 
 	<MkFolder>
 		<template #label>{{ i18n.ts.preventAiLearning }}</template>
+		<template #icon><i class="ti ti-photo-shield"></i></template>
 		<template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
 
 		<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index f26ea11214..109d26dfaa 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -37,8 +37,8 @@ import { chooseFileFromPc } from '@/scripts/select-file';
 import * as os from '@/os';
 import { $i } from '@/account';
 
-const name = ref('');
-const description = ref('');
+const name = ref($i.name ?? '');
+const description = ref($i.description ?? '');
 
 watch(name, () => {
 	os.apiWithDialog('i/update', {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 4e80a5c0fb..5c04faadca 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -7,10 +7,10 @@
 	@close="close(true)"
 	@closed="emit('closed')"
 >
-	<template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template>
-	<template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template>
-	<template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template>
-	<template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template>
+	<template v-if="page === 1" #header><i class="ti ti-user-edit"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template>
+	<template v-else-if="page === 2" #header><i class="ti ti-lock"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template>
+	<template v-else-if="page === 3" #header><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</template>
+	<template v-else-if="page === 4" #header><i class="ti ti-bell-plus"></i> {{ i18n.ts.pushNotification }}</template>
 	<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
 	<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
 
@@ -27,6 +27,7 @@
 		>
 			<template v-if="page === 0">
 				<div :class="$style.centerPage">
+					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :margin-min="20" :margin-max="28">
 						<div class="_gaps" style="text-align: center;">
 							<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
@@ -41,7 +42,9 @@
 				<div style="height: 100cqh; overflow: auto;">
 					<MkSpacer :margin-min="20" :margin-max="28">
 						<XProfile/>
-						<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+						<div class="_buttonsCenter" style="margin-top: 16px;">
+							<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+						</div>
 					</MkSpacer>
 				</div>
 			</template>
@@ -49,7 +52,9 @@
 				<div style="height: 100cqh; overflow: auto;">
 					<MkSpacer :margin-min="20" :margin-max="28">
 						<XPrivacy/>
-						<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+						<div class="_buttonsCenter" style="margin-top: 16px;">
+							<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+						</div>
 					</MkSpacer>
 				</div>
 			</template>
@@ -78,6 +83,7 @@
 			</template>
 			<template v-else-if="page === 5">
 				<div :class="$style.centerPage">
+					<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
 					<MkSpacer :margin-min="20" :margin-max="28">
 						<div class="_gaps" style="text-align: center;">
 							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
@@ -106,6 +112,7 @@ import MkButton from '@/components/MkButton.vue';
 import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
 import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
 import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
+import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n';
 import { instance } from '@/instance';
 import { host } from '@/config';
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index 3a44c3da3d..9b39858ca1 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -15,70 +15,49 @@
 </Transition>
 </template>
 
-<script lang="ts">
-import { defineComponent, PropType, ref, watch } from 'vue';
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkButton,
-	},
+const props = defineProps<{
+	p: () => Promise<any>;
+}>();
 
-	props: {
-		p: {
-			type: Function as PropType<() => Promise<any>>,
-			required: true,
-		},
-	},
+const pending = ref(true);
+const resolved = ref(false);
+const rejected = ref(false);
+const result = ref(null);
 
-	setup(props, context) {
-		const pending = ref(true);
-		const resolved = ref(false);
-		const rejected = ref(false);
-		const result = ref(null);
+const process = () => {
+	if (props.p == null) {
+		return;
+	}
+	const promise = props.p();
+	pending.value = true;
+	resolved.value = false;
+	rejected.value = false;
+	promise.then((_result) => {
+		pending.value = false;
+		resolved.value = true;
+		result.value = _result;
+	});
+	promise.catch(() => {
+		pending.value = false;
+		rejected.value = true;
+	});
+};
 
-		const process = () => {
-			if (props.p == null) {
-				return;
-			}
-			const promise = props.p();
-			pending.value = true;
-			resolved.value = false;
-			rejected.value = false;
-			promise.then((_result) => {
-				pending.value = false;
-				resolved.value = true;
-				result.value = _result;
-			});
-			promise.catch(() => {
-				pending.value = false;
-				rejected.value = true;
-			});
-		};
-
-		watch(() => props.p, () => {
-			process();
-		}, {
-			immediate: true,
-		});
-
-		const retry = () => {
-			process();
-		};
-
-		return {
-			pending,
-			resolved,
-			rejected,
-			result,
-			retry,
-			defaultStore,
-			i18n,
-		};
-	},
+watch(() => props.p, () => {
+	process();
+}, {
+	immediate: true,
 });
+
+const retry = () => {
+	process();
+};
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
index f6811b6747..685b3b8b8e 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
@@ -1,8 +1,8 @@
 /* eslint-disable @typescript-eslint/explicit-function-return-type */
 import { StoryObj } from '@storybook/vue3';
-import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.vue';
 import { within } from '@storybook/testing-library';
 import { expect } from '@storybook/jest';
+import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.ts';
 export const Default = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
new file mode 100644
index 0000000000..2a50a34390
--- /dev/null
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -0,0 +1,367 @@
+import { VNode, h } from 'vue';
+import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
+import MkUrl from '@/components/global/MkUrl.vue';
+import MkLink from '@/components/MkLink.vue';
+import MkMention from '@/components/MkMention.vue';
+import MkEmoji from '@/components/global/MkEmoji.vue';
+import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
+import MkCode from '@/components/MkCode.vue';
+import MkGoogle from '@/components/MkGoogle.vue';
+import MkSparkle from '@/components/MkSparkle.vue';
+import MkA from '@/components/global/MkA.vue';
+import { host } from '@/config';
+import { defaultStore } from '@/store';
+
+const QUOTE_STYLE = `
+display: block;
+margin: 8px;
+padding: 6px 0 6px 12px;
+color: var(--fg);
+border-left: solid 3px var(--fg);
+opacity: 0.7;
+`.split('\n').join(' ');
+
+export default function(props: {
+	text: string;
+	plain?: boolean;
+	nowrap?: boolean;
+	author?: Misskey.entities.UserLite;
+	i?: Misskey.entities.UserLite;
+	isNote?: boolean;
+	emojiUrls?: string[];
+	rootScale?: number;
+}) {
+	const isNote = props.isNote !== undefined ? props.isNote : true;
+
+	if (props.text == null || props.text === '') return;
+
+	const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
+
+	const validTime = (t: string | null | undefined) => {
+		if (t == null) return null;
+		return t.match(/^[0-9.]+s$/) ? t : null;
+	};
+
+	const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
+
+	/**
+	 * Gen Vue Elements from MFM AST
+	 * @param ast MFM AST
+	 * @param scale How times large the text is
+	 */
+	const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
+		switch (token.type) {
+			case 'text': {
+				const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+
+				if (!props.plain) {
+					const res: (VNode | string)[] = [];
+					for (const t of text.split('\n')) {
+						res.push(h('br'));
+						res.push(t);
+					}
+					res.shift();
+					return res;
+				} else {
+					return [text.replace(/\n/g, ' ')];
+				}
+			}
+
+			case 'bold': {
+				return [h('b', genEl(token.children, scale))];
+			}
+
+			case 'strike': {
+				return [h('del', genEl(token.children, scale))];
+			}
+
+			case 'italic': {
+				return h('i', {
+					style: 'font-style: oblique;',
+				}, genEl(token.children, scale));
+			}
+
+			case 'fn': {
+				// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
+				let style;
+				switch (token.props.name) {
+					case 'tada': {
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
+						break;
+					}
+					case 'jelly': {
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
+						break;
+					}
+					case 'twitch': {
+						const speed = validTime(token.props.args.speed) ?? '0.5s';
+						style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
+						break;
+					}
+					case 'shake': {
+						const speed = validTime(token.props.args.speed) ?? '0.5s';
+						style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
+						break;
+					}
+					case 'spin': {
+						const direction =
+							token.props.args.left ? 'reverse' :
+							token.props.args.alternate ? 'alternate' :
+							'normal';
+						const anime =
+							token.props.args.x ? 'mfm-spinX' :
+							token.props.args.y ? 'mfm-spinY' :
+							'mfm-spin';
+						const speed = validTime(token.props.args.speed) ?? '1.5s';
+						style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
+						break;
+					}
+					case 'jump': {
+						const speed = validTime(token.props.args.speed) ?? '0.75s';
+						style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
+						break;
+					}
+					case 'bounce': {
+						const speed = validTime(token.props.args.speed) ?? '0.75s';
+						style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
+						break;
+					}
+					case 'flip': {
+						const transform =
+							(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
+							token.props.args.v ? 'scaleY(-1)' :
+							'scaleX(-1)';
+						style = `transform: ${transform};`;
+						break;
+					}
+					case 'x2': {
+						return h('span', {
+							class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
+						}, genEl(token.children, scale * 2));
+					}
+					case 'x3': {
+						return h('span', {
+							class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
+						}, genEl(token.children, scale * 3));
+					}
+					case 'x4': {
+						return h('span', {
+							class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
+						}, genEl(token.children, scale * 4));
+					}
+					case 'font': {
+						const family =
+							token.props.args.serif ? 'serif' :
+							token.props.args.monospace ? 'monospace' :
+							token.props.args.cursive ? 'cursive' :
+							token.props.args.fantasy ? 'fantasy' :
+							token.props.args.emoji ? 'emoji' :
+							token.props.args.math ? 'math' :
+							null;
+						if (family) style = `font-family: ${family};`;
+						break;
+					}
+					case 'blur': {
+						return h('span', {
+							class: '_mfm_blur_',
+						}, genEl(token.children, scale));
+					}
+					case 'rainbow': {
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
+						break;
+					}
+					case 'sparkle': {
+						if (!useAnim) {
+							return genEl(token.children, scale);
+						}
+						return h(MkSparkle, {}, genEl(token.children, scale));
+					}
+					case 'rotate': {
+						const degrees = parseFloat(token.props.args.deg ?? '90');
+						style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
+						break;
+					}
+					case 'position': {
+						if (!defaultStore.state.advancedMfm) break;
+						const x = parseFloat(token.props.args.x ?? '0');
+						const y = parseFloat(token.props.args.y ?? '0');
+						style = `transform: translateX(${x}em) translateY(${y}em);`;
+						break;
+					}
+					case 'scale': {
+						if (!defaultStore.state.advancedMfm) {
+							style = '';
+							break;
+						}
+						const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
+						const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
+						style = `transform: scale(${x}, ${y});`; 
+						scale = scale * Math.max(x, y);
+						break;
+					}
+					case 'fg': {
+						let color = token.props.args.color;
+						if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+						style = `color: #${color};`;
+						break;
+					}
+					case 'bg': {
+						let color = token.props.args.color;
+						if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+						style = `background-color: #${color};`;
+						break;
+					}
+				}
+				if (style == null) {
+					return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
+				} else {
+					return h('span', {
+						style: 'display: inline-block; ' + style,
+					}, genEl(token.children, scale));
+				}
+			}
+
+			case 'small': {
+				return [h('small', {
+					style: 'opacity: 0.7;',
+				}, genEl(token.children, scale))];
+			}
+
+			case 'center': {
+				return [h('div', {
+					style: 'text-align:center;',
+				}, genEl(token.children, scale))];
+			}
+
+			case 'url': {
+				return [h(MkUrl, {
+					key: Math.random(),
+					url: token.props.url,
+					rel: 'nofollow noopener',
+				})];
+			}
+
+			case 'link': {
+				return [h(MkLink, {
+					key: Math.random(),
+					url: token.props.url,
+					rel: 'nofollow noopener',
+				}, genEl(token.children, scale))];
+			}
+
+			case 'mention': {
+				return [h(MkMention, {
+					key: Math.random(),
+					host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) || host,
+					username: token.props.username,
+				})];
+			}
+
+			case 'hashtag': {
+				return [h(MkA, {
+					key: Math.random(),
+					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
+					style: 'color:var(--hashtag);',
+				}, `#${token.props.hashtag}`)];
+			}
+
+			case 'blockCode': {
+				return [h(MkCode, {
+					key: Math.random(),
+					code: token.props.code,
+					lang: token.props.lang,
+				})];
+			}
+
+			case 'inlineCode': {
+				return [h(MkCode, {
+					key: Math.random(),
+					code: token.props.code,
+					inline: true,
+				})];
+			}
+
+			case 'quote': {
+				if (!props.nowrap) {
+					return [h('div', {
+						style: QUOTE_STYLE,
+					}, genEl(token.children, scale))];
+				} else {
+					return [h('span', {
+						style: QUOTE_STYLE,
+					}, genEl(token.children, scale))];
+				}
+			}
+
+			case 'emojiCode': {
+				// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+				if (props.author?.host == null) {
+					return [h(MkCustomEmoji, {
+						key: Math.random(),
+						name: token.props.name,
+						normal: props.plain,
+						host: null,
+						useOriginalSize: scale >= 2.5,
+					})];
+				} else {
+					// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+					if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
+						return [h('span', `:${token.props.name}:`)];
+					} else {
+						return [h(MkCustomEmoji, {
+							key: Math.random(),
+							name: token.props.name,
+							// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+							url: props.emojiUrls ? props.emojiUrls[token.props.name] : null,
+							normal: props.plain,
+							host: props.author.host,
+							useOriginalSize: scale >= 2.5,
+						})];
+					}
+				}
+			}
+
+			case 'unicodeEmoji': {
+				return [h(MkEmoji, {
+					key: Math.random(),
+					emoji: token.props.emoji,
+				})];
+			}
+
+			case 'mathInline': {
+				return [h('code', token.props.formula)];
+			}
+
+			case 'mathBlock': {
+				return [h('code', token.props.formula)];
+			}
+
+			case 'search': {
+				return [h(MkGoogle, {
+					key: Math.random(),
+					q: token.props.query,
+				})];
+			}
+
+			case 'plain': {
+				return [h('span', genEl(token.children, scale))];
+			}
+
+			default: {
+				// eslint-disable-next-line @typescript-eslint/no-explicit-any
+				console.error('unrecognized ast type:', (token as any).type);
+
+				return [];
+			}
+		}
+	}).flat(Infinity) as (VNode | string)[];
+
+	return h('span', {
+		// https://codeday.me/jp/qa/20190424/690106.html
+		style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
+	}, genEl(ast, props.rootScale ?? 1));
+}
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue
deleted file mode 100644
index 28a0d1c986..0000000000
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-<template>
-<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import MfmCore from '@/components/mfm';
-
-const props = withDefaults(defineProps<{
-	text: string;
-	plain?: boolean;
-	nowrap?: boolean;
-	author?: any;
-	isNote?: boolean;
-}>(), {
-	plain: false,
-	nowrap: false,
-	author: null,
-	isNote: true,
-});
-</script>
-
-<style lang="scss">
-._mfm_blur_ {
-	filter: blur(6px);
-	transition: filter 0.3s;
-
-	&:hover {
-		filter: blur(0px);
-	}
-}
-
-.mfm-x2 {
-	--mfm-zoom-size: 200%;
-}
-
-.mfm-x3 {
-	--mfm-zoom-size: 400%;
-}
-
-.mfm-x4 {
-	--mfm-zoom-size: 600%;
-}
-
-.mfm-x2, .mfm-x3, .mfm-x4 {
-	font-size: var(--mfm-zoom-size);
-
-	.mfm-x2, .mfm-x3, .mfm-x4 {
-		/* only half effective */
-		font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
-
-		.mfm-x2, .mfm-x3, .mfm-x4 {
-			/* disabled */
-			font-size: 100%;
-		}
-	}
-}
-
-@keyframes mfm-spin {
-	0% { transform: rotate(0deg); }
-	100% { transform: rotate(360deg); }
-}
-
-@keyframes mfm-spinX {
-	0% { transform: perspective(128px) rotateX(0deg); }
-	100% { transform: perspective(128px) rotateX(360deg); }
-}
-
-@keyframes mfm-spinY {
-	0% { transform: perspective(128px) rotateY(0deg); }
-	100% { transform: perspective(128px) rotateY(360deg); }
-}
-
-@keyframes mfm-jump {
-	0% { transform: translateY(0); }
-	25% { transform: translateY(-16px); }
-	50% { transform: translateY(0); }
-	75% { transform: translateY(-8px); }
-	100% { transform: translateY(0); }
-}
-
-@keyframes mfm-bounce {
-	0% { transform: translateY(0) scale(1, 1); }
-	25% { transform: translateY(-16px) scale(1, 1); }
-	50% { transform: translateY(0) scale(1, 1); }
-	75% { transform: translateY(0) scale(1.5, 0.75); }
-	100% { transform: translateY(0) scale(1, 1); }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-twitch {
-	0% { transform: translate(7px, -2px) }
-	5% { transform: translate(-3px, 1px) }
-	10% { transform: translate(-7px, -1px) }
-	15% { transform: translate(0px, -1px) }
-	20% { transform: translate(-8px, 6px) }
-	25% { transform: translate(-4px, -3px) }
-	30% { transform: translate(-4px, -6px) }
-	35% { transform: translate(-8px, -8px) }
-	40% { transform: translate(4px, 6px) }
-	45% { transform: translate(-3px, 1px) }
-	50% { transform: translate(2px, -10px) }
-	55% { transform: translate(-7px, 0px) }
-	60% { transform: translate(-2px, 4px) }
-	65% { transform: translate(3px, -8px) }
-	70% { transform: translate(6px, 7px) }
-	75% { transform: translate(-7px, -2px) }
-	80% { transform: translate(-7px, -8px) }
-	85% { transform: translate(9px, 3px) }
-	90% { transform: translate(-3px, -2px) }
-	95% { transform: translate(-10px, 2px) }
-	100% { transform: translate(-2px, -6px) }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-shake {
-	0% { transform: translate(-3px, -1px) rotate(-8deg) }
-	5% { transform: translate(0px, -1px) rotate(-10deg) }
-	10% { transform: translate(1px, -3px) rotate(0deg) }
-	15% { transform: translate(1px, 1px) rotate(11deg) }
-	20% { transform: translate(-2px, 1px) rotate(1deg) }
-	25% { transform: translate(-1px, -2px) rotate(-2deg) }
-	30% { transform: translate(-1px, 2px) rotate(-3deg) }
-	35% { transform: translate(2px, 1px) rotate(6deg) }
-	40% { transform: translate(-2px, -3px) rotate(-9deg) }
-	45% { transform: translate(0px, -1px) rotate(-12deg) }
-	50% { transform: translate(1px, 2px) rotate(10deg) }
-	55% { transform: translate(0px, -3px) rotate(8deg) }
-	60% { transform: translate(1px, -1px) rotate(8deg) }
-	65% { transform: translate(0px, -1px) rotate(-7deg) }
-	70% { transform: translate(-1px, -3px) rotate(6deg) }
-	75% { transform: translate(0px, -2px) rotate(4deg) }
-	80% { transform: translate(-2px, -1px) rotate(3deg) }
-	85% { transform: translate(1px, -3px) rotate(-10deg) }
-	90% { transform: translate(1px, 0px) rotate(3deg) }
-	95% { transform: translate(-2px, 0px) rotate(-3deg) }
-	100% { transform: translate(2px, 1px) rotate(2deg) }
-}
-
-@keyframes mfm-rubberBand {
-	from { transform: scale3d(1, 1, 1); }
-	30% { transform: scale3d(1.25, 0.75, 1); }
-	40% { transform: scale3d(0.75, 1.25, 1); }
-	50% { transform: scale3d(1.15, 0.85, 1); }
-	65% { transform: scale3d(0.95, 1.05, 1); }
-	75% { transform: scale3d(1.05, 0.95, 1); }
-	to { transform: scale3d(1, 1, 1); }
-}
-
-@keyframes mfm-rainbow {
-	0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
-	100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
-}
-</style>
-
-<style lang="scss" module>
-.root {
-	white-space: pre-wrap;
-
-	&.nowrap {
-		white-space: pre;
-		word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
-}
-</style>
diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts
index 1fd293ba10..2708b759aa 100644
--- a/packages/frontend/src/components/global/i18n.ts
+++ b/packages/frontend/src/components/global/i18n.ts
@@ -1,42 +1,24 @@
-import { h, defineComponent } from 'vue';
+import { h } from 'vue';
 
-export default defineComponent({
-	props: {
-		src: {
-			type: String,
-			required: true,
-		},
-		tag: {
-			type: String,
-			required: false,
-			default: 'span',
-		},
-		textTag: {
-			type: String,
-			required: false,
-			default: null,
-		},
-	},
-	render() {
-		let str = this.src;
-		const parsed = [] as (string | { arg: string; })[];
-		while (true) {
-			const nextBracketOpen = str.indexOf('{');
-			const nextBracketClose = str.indexOf('}');
+export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
+	let str = props.src;
+	const parsed = [] as (string | { arg: string; })[];
+	while (true) {
+		const nextBracketOpen = str.indexOf('{');
+		const nextBracketClose = str.indexOf('}');
 
-			if (nextBracketOpen === -1) {
-				parsed.push(str);
-				break;
-			} else {
-				if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
-				parsed.push({
-					arg: str.substring(nextBracketOpen + 1, nextBracketClose),
-				});
-			}
-
-			str = str.substr(nextBracketClose + 1);
+		if (nextBracketOpen === -1) {
+			parsed.push(str);
+			break;
+		} else {
+			if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
+			parsed.push({
+				arg: str.substring(nextBracketOpen + 1, nextBracketClose),
+			});
 		}
 
-		return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]()));
-	},
-});
+		str = str.substr(nextBracketClose + 1);
+	}
+
+	return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
+}
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index 4ef8111da9..ee2a2bc7bd 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -1,6 +1,6 @@
 import { App } from 'vue';
 
-import Mfm from './global/MkMisskeyFlavoredMarkdown.vue';
+import Mfm from './global/MkMisskeyFlavoredMarkdown.ts';
 import MkA from './global/MkA.vue';
 import MkAcct from './global/MkAcct.vue';
 import MkAvatar from './global/MkAvatar.vue';
diff --git a/packages/frontend/src/components/mfm.ts b/packages/frontend/src/components/mfm.ts
deleted file mode 100644
index c3c07b5834..0000000000
--- a/packages/frontend/src/components/mfm.ts
+++ /dev/null
@@ -1,390 +0,0 @@
-import { VNode, defineComponent, h } from 'vue';
-import * as mfm from 'mfm-js';
-import MkUrl from '@/components/global/MkUrl.vue';
-import MkLink from '@/components/MkLink.vue';
-import MkMention from '@/components/MkMention.vue';
-import MkEmoji from '@/components/global/MkEmoji.vue';
-import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
-import MkCode from '@/components/MkCode.vue';
-import MkGoogle from '@/components/MkGoogle.vue';
-import MkSparkle from '@/components/MkSparkle.vue';
-import MkA from '@/components/global/MkA.vue';
-import { host } from '@/config';
-import { defaultStore } from '@/store';
-
-const QUOTE_STYLE = `
-display: block;
-margin: 8px;
-padding: 6px 0 6px 12px;
-color: var(--fg);
-border-left: solid 3px var(--fg);
-opacity: 0.7;
-`.split('\n').join(' ');
-
-export default defineComponent({
-	props: {
-		text: {
-			type: String,
-			required: true,
-		},
-		plain: {
-			type: Boolean,
-			default: false,
-		},
-		nowrap: {
-			type: Boolean,
-			default: false,
-		},
-		author: {
-			type: Object,
-			default: null,
-		},
-		i: {
-			type: Object,
-			default: null,
-		},
-		isNote: {
-			type: Boolean,
-			default: true,
-		},
-		emojiUrls: {
-			type: Object,
-			default: null,
-		},
-		rootScale: {
-			type: Number,
-			default: 1,
-		}
-	},
-
-	render() {
-		if (this.text == null || this.text === '') return;
-
-		const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text);
-
-		const validTime = (t: string | null | undefined) => {
-			if (t == null) return null;
-			return t.match(/^[0-9.]+s$/) ? t : null;
-		};
-
-		const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
-
-		/**
-		 * Gen Vue Elements from MFM AST
-		 * @param ast MFM AST
-		 * @param scale How times large the text is
-		 */
-		const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
-			switch (token.type) {
-				case 'text': {
-					const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
-
-					if (!this.plain) {
-						const res: (VNode | string)[] = [];
-						for (const t of text.split('\n')) {
-							res.push(h('br'));
-							res.push(t);
-						}
-						res.shift();
-						return res;
-					} else {
-						return [text.replace(/\n/g, ' ')];
-					}
-				}
-
-				case 'bold': {
-					return [h('b', genEl(token.children, scale))];
-				}
-
-				case 'strike': {
-					return [h('del', genEl(token.children, scale))];
-				}
-
-				case 'italic': {
-					return h('i', {
-						style: 'font-style: oblique;',
-					}, genEl(token.children, scale));
-				}
-
-				case 'fn': {
-					// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
-					let style;
-					switch (token.props.name) {
-						case 'tada': {
-							const speed = validTime(token.props.args.speed) ?? '1s';
-							style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
-							break;
-						}
-						case 'jelly': {
-							const speed = validTime(token.props.args.speed) ?? '1s';
-							style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
-							break;
-						}
-						case 'twitch': {
-							const speed = validTime(token.props.args.speed) ?? '0.5s';
-							style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
-							break;
-						}
-						case 'shake': {
-							const speed = validTime(token.props.args.speed) ?? '0.5s';
-							style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
-							break;
-						}
-						case 'spin': {
-							const direction =
-								token.props.args.left ? 'reverse' :
-								token.props.args.alternate ? 'alternate' :
-								'normal';
-							const anime =
-								token.props.args.x ? 'mfm-spinX' :
-								token.props.args.y ? 'mfm-spinY' :
-								'mfm-spin';
-							const speed = validTime(token.props.args.speed) ?? '1.5s';
-							style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
-							break;
-						}
-						case 'jump': {
-							const speed = validTime(token.props.args.speed) ?? '0.75s';
-							style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
-							break;
-						}
-						case 'bounce': {
-							const speed = validTime(token.props.args.speed) ?? '0.75s';
-							style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
-							break;
-						}
-						case 'flip': {
-							const transform =
-								(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
-								token.props.args.v ? 'scaleY(-1)' :
-								'scaleX(-1)';
-							style = `transform: ${transform};`;
-							break;
-						}
-						case 'x2': {
-							return h('span', {
-								class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
-							}, genEl(token.children, scale * 2));
-						}
-						case 'x3': {
-							return h('span', {
-								class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
-							}, genEl(token.children, scale * 3));
-						}
-						case 'x4': {
-							return h('span', {
-								class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
-							}, genEl(token.children, scale * 4));
-						}
-						case 'font': {
-							const family =
-								token.props.args.serif ? 'serif' :
-								token.props.args.monospace ? 'monospace' :
-								token.props.args.cursive ? 'cursive' :
-								token.props.args.fantasy ? 'fantasy' :
-								token.props.args.emoji ? 'emoji' :
-								token.props.args.math ? 'math' :
-								null;
-							if (family) style = `font-family: ${family};`;
-							break;
-						}
-						case 'blur': {
-							return h('span', {
-								class: '_mfm_blur_',
-							}, genEl(token.children, scale));
-						}
-						case 'rainbow': {
-							const speed = validTime(token.props.args.speed) ?? '1s';
-							style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
-							break;
-						}
-						case 'sparkle': {
-							if (!useAnim) {
-								return genEl(token.children, scale);
-							}
-							return h(MkSparkle, {}, genEl(token.children, scale));
-						}
-						case 'rotate': {
-							const degrees = parseFloat(token.props.args.deg ?? '90');
-							style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
-							break;
-						}
-						case 'position': {
-							if (!defaultStore.state.advancedMfm) break;
-							const x = parseFloat(token.props.args.x ?? '0');
-							const y = parseFloat(token.props.args.y ?? '0');
-							style = `transform: translateX(${x}em) translateY(${y}em);`;
-							break;
-						}
-						case 'scale': {
-							if (!defaultStore.state.advancedMfm) {
-								style = '';
-								break;
-							}
-							const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
-							const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
-							style = `transform: scale(${x}, ${y});`; 
-							scale = scale * Math.max(x, y);
-							break;
-						}
-						case 'fg': {
-							let color = token.props.args.color;
-							if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
-							style = `color: #${color};`;
-							break;
-						}
-						case 'bg': {
-							let color = token.props.args.color;
-							if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
-							style = `background-color: #${color};`;
-							break;
-						}
-					}
-					if (style == null) {
-						return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
-					} else {
-						return h('span', {
-							style: 'display: inline-block; ' + style,
-						}, genEl(token.children, scale));
-					}
-				}
-
-				case 'small': {
-					return [h('small', {
-						style: 'opacity: 0.7;',
-					}, genEl(token.children, scale))];
-				}
-
-				case 'center': {
-					return [h('div', {
-						style: 'text-align:center;',
-					}, genEl(token.children, scale))];
-				}
-
-				case 'url': {
-					return [h(MkUrl, {
-						key: Math.random(),
-						url: token.props.url,
-						rel: 'nofollow noopener',
-					})];
-				}
-
-				case 'link': {
-					return [h(MkLink, {
-						key: Math.random(),
-						url: token.props.url,
-						rel: 'nofollow noopener',
-					}, genEl(token.children, scale))];
-				}
-
-				case 'mention': {
-					return [h(MkMention, {
-						key: Math.random(),
-						host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
-						username: token.props.username,
-					})];
-				}
-
-				case 'hashtag': {
-					return [h(MkA, {
-						key: Math.random(),
-						to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
-						style: 'color:var(--hashtag);',
-					}, `#${token.props.hashtag}`)];
-				}
-
-				case 'blockCode': {
-					return [h(MkCode, {
-						key: Math.random(),
-						code: token.props.code,
-						lang: token.props.lang,
-					})];
-				}
-
-				case 'inlineCode': {
-					return [h(MkCode, {
-						key: Math.random(),
-						code: token.props.code,
-						inline: true,
-					})];
-				}
-
-				case 'quote': {
-					if (!this.nowrap) {
-						return [h('div', {
-							style: QUOTE_STYLE,
-						}, genEl(token.children, scale))];
-					} else {
-						return [h('span', {
-							style: QUOTE_STYLE,
-						}, genEl(token.children, scale))];
-					}
-				}
-
-				case 'emojiCode': {
-					// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-					if (this.author?.host == null) {
-						return [h(MkCustomEmoji, {
-							key: Math.random(),
-							name: token.props.name,
-							normal: this.plain,
-							host: null,
-							useOriginalSize: scale >= 2.5,
-						})];
-					} else {
-						// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-						if (this.emojiUrls && (this.emojiUrls[token.props.name] == null)) {
-							return [h('span', `:${token.props.name}:`)];
-						} else {
-							return [h(MkCustomEmoji, {
-								key: Math.random(),
-								name: token.props.name,
-								// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-								url: this.emojiUrls ? this.emojiUrls[token.props.name] : null,
-								normal: this.plain,
-								host: this.author.host,
-								useOriginalSize: scale >= 2.5,
-							})];
-						}
-					}
-				}
-
-				case 'unicodeEmoji': {
-					return [h(MkEmoji, {
-						key: Math.random(),
-						emoji: token.props.emoji,
-					})];
-				}
-
-				case 'mathInline': {
-					return [h('code', token.props.formula)];
-				}
-
-				case 'mathBlock': {
-					return [h('code', token.props.formula)];
-				}
-
-				case 'search': {
-					return [h(MkGoogle, {
-						key: Math.random(),
-						q: token.props.query,
-					})];
-				}
-
-				case 'plain': {
-					return [h('span', genEl(token.children, scale))];
-				}
-
-				default: {
-					// eslint-disable-next-line @typescript-eslint/no-explicit-any
-					console.error('unrecognized ast type:', (token as any).type);
-
-					return [];
-				}
-			}
-		}).flat(Infinity) as (VNode | string)[];
-
-		// Parse ast to DOM
-		return h('span', genEl(ast, this.rootScale));
-	},
-});
diff --git a/packages/frontend/src/components/page/block.type.ts b/packages/frontend/src/components/page/block.type.ts
new file mode 100644
index 0000000000..71249a8aff
--- /dev/null
+++ b/packages/frontend/src/components/page/block.type.ts
@@ -0,0 +1,29 @@
+export type BlockBase = {
+	id: string;
+	type: string;
+};
+
+export type TextBlock = BlockBase & {
+	type: 'text';
+	text: string;
+};
+
+export type SectionBlock = BlockBase & {
+	type: 'section';
+	title: string;
+	children: Block[];
+};
+
+export type ImageBlock = BlockBase & {
+	type: 'image';
+	fileId: string | null;
+};
+
+export type NoteBlock = BlockBase & {
+	type: 'note';
+	detailed: boolean;
+	note: string | null;
+};
+
+export type Block =
+	TextBlock | SectionBlock | ImageBlock | NoteBlock;
diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue
index f3e7764604..dddb9d76bc 100644
--- a/packages/frontend/src/components/page/page.block.vue
+++ b/packages/frontend/src/components/page/page.block.vue
@@ -1,44 +1,19 @@
 <template>
-<component :is="'x-' + block.type" :key="block.id" :block="block" :hpml="hpml" :h="h"/>
+<component :is="'x-' + block.type" :key="block.id" :page="page" :block="block" :h="h"/>
 </template>
 
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
 import XText from './page.text.vue';
 import XSection from './page.section.vue';
 import XImage from './page.image.vue';
-import XButton from './page.button.vue';
-import XNumberInput from './page.number-input.vue';
-import XTextInput from './page.text-input.vue';
-import XTextareaInput from './page.textarea-input.vue';
-import XSwitch from './page.switch.vue';
-import XIf from './page.if.vue';
-import XTextarea from './page.textarea.vue';
-import XPost from './page.post.vue';
-import XCounter from './page.counter.vue';
-import XRadioButton from './page.radio-button.vue';
-import XCanvas from './page.canvas.vue';
 import XNote from './page.note.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { Block } from '@/scripts/hpml/block';
+import { Block } from './block.type';
 
-export default defineComponent({
-	components: {
-		XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote,
-	},
-	props: {
-		block: {
-			type: Object as PropType<Block>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-		h: {
-			type: Number,
-			required: true,
-		},
-	},
-});
+defineProps<{
+	block: Block,
+	h: number,
+	page: Misskey.entities.Page,
+}>();
 </script>
diff --git a/packages/frontend/src/components/page/page.button.vue b/packages/frontend/src/components/page/page.button.vue
deleted file mode 100644
index 83931021d8..0000000000
--- a/packages/frontend/src/components/page/page.button.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<template>
-<div>
-	<MkButton class="kudkigyw" :primary="block.primary" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType, unref } from 'vue';
-import MkButton from '../MkButton.vue';
-import * as os from '@/os';
-import { ButtonBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-	props: {
-		block: {
-			type: Object as PropType<ButtonBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	methods: {
-		click() {
-			if (this.block.action === 'dialog') {
-				this.hpml.eval();
-				os.alert({
-					text: this.hpml.interpolate(this.block.content),
-				});
-			} else if (this.block.action === 'resetRandom') {
-				this.hpml.updateRandomSeed(Math.random());
-				this.hpml.eval();
-			} else if (this.block.action === 'pushEvent') {
-				os.api('page-push', {
-					pageId: this.hpml.page.id,
-					event: this.block.event,
-					...(this.block.var ? {
-						var: unref(this.hpml.vars)[this.block.var],
-					} : {}),
-				});
-
-				os.alert({
-					type: 'success',
-					text: this.hpml.interpolate(this.block.message),
-				});
-			} else if (this.block.action === 'callAiScript') {
-				this.hpml.callAiScript(this.block.fn);
-			}
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
-	display: inline-block;
-	min-width: 200px;
-	max-width: 450px;
-	margin: 8px 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.canvas.vue b/packages/frontend/src/components/page/page.canvas.vue
deleted file mode 100644
index 82ff36ec36..0000000000
--- a/packages/frontend/src/components/page/page.canvas.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<div class="ysrxegms">
-	<canvas ref="canvas" :width="block.width" :height="block.height"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
-import { CanvasBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-
-export default defineComponent({
-	props: {
-		block: {
-			type: Object as PropType<CanvasBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const canvas: Ref<any> = ref(null);
-
-		onMounted(() => {
-			props.hpml.registerCanvas(props.block.name, canvas.value);
-		});
-
-		return {
-			canvas,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ysrxegms {
-	display: inline-block;
-	vertical-align: bottom;
-	overflow: auto;
-	max-width: 100%;
-
-	> canvas {
-		display: block;
-	}
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.counter.vue b/packages/frontend/src/components/page/page.counter.vue
deleted file mode 100644
index 63fde6a120..0000000000
--- a/packages/frontend/src/components/page/page.counter.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-<div>
-	<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkButton from '../MkButton.vue';
-import { CounterVarBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-	props: {
-		block: {
-			type: Object as PropType<CounterVarBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function click() {
-			props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1));
-			props.hpml.eval();
-		}
-
-		return {
-			click,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.llumlmnx {
-	display: inline-block;
-	min-width: 300px;
-	max-width: 450px;
-	margin: 8px 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.if.vue b/packages/frontend/src/components/page/page.if.vue
deleted file mode 100644
index 372a15f0c6..0000000000
--- a/packages/frontend/src/components/page/page.if.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<div v-show="hpml.vars.value[block.var]">
-	<XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h"/>
-</div>
-</template>
-
-<script lang="ts">
-import { IfBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { defineComponent, defineAsyncComponent, PropType } from 'vue';
-
-export default defineComponent({
-	components: {
-		XBlock: defineAsyncComponent(() => import('./page.block.vue')),
-	},
-	props: {
-		block: {
-			type: Object as PropType<IfBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-		h: {
-			type: Number,
-			required: true,
-		},
-	},
-});
-</script>
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index 6ea81d257f..2edcfb8b1a 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -5,15 +5,15 @@
 </template>
 
 <script lang="ts" setup>
-import { PropType } from 'vue';
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
+import { ImageBlock } from './block.type';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
-import { ImageBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
 
 const props = defineProps<{
-	block: PropType<ImageBlock>,
-	hpml: PropType<Hpml>,
+	block: ImageBlock,
+	page: Misskey.entities.Page,
 }>();
 
-const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
+const image = props.page.attachedFiles.find(x => x.id === props.block.fileId);
 </script>
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index 8c65dabf08..7133a7f5a1 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -1,47 +1,29 @@
 <template>
-<div class="voxdxuby">
+<div style="margin: 1em 0;">
 	<MkNote v-if="note && !block.detailed" :key="note.id + ':normal'" v-model:note="note"/>
 	<MkNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" v-model:note="note"/>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
+<script lang="ts" setup>
+import { onMounted, Ref, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { NoteBlock } from './block.type';
 import MkNote from '@/components/MkNote.vue';
 import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
 import * as os from '@/os';
-import { NoteBlock } from '@/scripts/hpml/block';
 
-export default defineComponent({
-	components: {
-		MkNote,
-		MkNoteDetailed,
-	},
-	props: {
-		block: {
-			type: Object as PropType<NoteBlock>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const note: Ref<Record<string, any> | null> = ref(null);
+const props = defineProps<{
+	block: NoteBlock,
+	page: Misskey.entities.Page,
+}>();
 
-		onMounted(() => {
-			os.api('notes/show', { noteId: props.block.note })
-			.then(result => {
-				note.value = result;
-			});
+const note: Ref<Misskey.entities.Note | null> = ref(null);
+
+onMounted(() => {
+	os.api('notes/show', { noteId: props.block.note })
+		.then(result => {
+			note.value = result;
 		});
-
-		return {
-			note,
-		};
-	},
 });
 </script>
-
-<style lang="scss" scoped>
-.voxdxuby {
-	margin: 1em 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.number-input.vue b/packages/frontend/src/components/page/page.number-input.vue
deleted file mode 100644
index 72c1b6deb0..0000000000
--- a/packages/frontend/src/components/page/page.number-input.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<div>
-	<MkInput class="kudkigyw" :model-value="value" type="number" @update:model-value="updateValue($event)">
-		<template #label>{{ hpml.interpolate(block.text) }}</template>
-	</MkInput>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkInput from '../MkInput.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { NumberInputVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkInput,
-	},
-	props: {
-		block: {
-			type: Object as PropType<NumberInputVarBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
-	display: inline-block;
-	min-width: 300px;
-	max-width: 450px;
-	margin: 8px 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.post.vue b/packages/frontend/src/components/page/page.post.vue
deleted file mode 100644
index 55da610cb6..0000000000
--- a/packages/frontend/src/components/page/page.post.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-<template>
-<div class="ngbfujlo">
-	<MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea>
-	<MkButton class="button" primary :disabled="posting || posted" @click="post()">
-		<i v-if="posted" class="ti ti-check"></i>
-		<i v-else class="ti ti-send"></i>
-	</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-import MkTextarea from '../MkTextarea.vue';
-import MkButton from '../MkButton.vue';
-import { apiUrl } from '@/config';
-import * as os from '@/os';
-import { PostBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { defaultStore } from '@/store';
-import { $i } from '@/account';
-
-export default defineComponent({
-	components: {
-		MkTextarea,
-		MkButton,
-	},
-	props: {
-		block: {
-			type: Object as PropType<PostBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	data() {
-		return {
-			text: this.hpml.interpolate(this.block.text),
-			posted: false,
-			posting: false,
-		};
-	},
-	watch: {
-		'hpml.vars': {
-			handler() {
-				this.text = this.hpml.interpolate(this.block.text);
-			},
-			deep: true,
-		},
-	},
-	methods: {
-		upload() {
-			const promise = new Promise((ok) => {
-				const canvas = this.hpml.canvases[this.block.canvasId];
-				canvas.toBlob(blob => {
-					const formData = new FormData();
-					formData.append('file', blob);
-					formData.append('i', $i.token);
-					if (defaultStore.state.uploadFolder) {
-						formData.append('folderId', defaultStore.state.uploadFolder);
-					}
-
-					window.fetch(apiUrl + '/drive/files/create', {
-						method: 'POST',
-						body: formData,
-					})
-						.then(response => response.json())
-						.then(f => {
-							ok(f);
-						});
-				});
-			});
-			os.promiseDialog(promise);
-			return promise;
-		},
-		async post() {
-			this.posting = true;
-			const file = this.block.attachCanvasImage ? await this.upload() : null;
-			os.apiWithDialog('notes/create', {
-				text: this.text === '' ? null : this.text,
-				fileIds: file ? [file.id] : undefined,
-			}).then(() => {
-				this.posted = true;
-			});
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ngbfujlo {
-	position: relative;
-	padding: 32px;
-	border-radius: 6px;
-	box-shadow: 0 2px 8px var(--shadow);
-	z-index: 1;
-
-	> .button {
-		margin-top: 32px;
-	}
-
-	@media (max-width: 600px) {
-		padding: 16px;
-
-		> .button {
-			margin-top: 16px;
-		}
-	}
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.radio-button.vue b/packages/frontend/src/components/page/page.radio-button.vue
deleted file mode 100644
index ce8f252e44..0000000000
--- a/packages/frontend/src/components/page/page.radio-button.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<div>
-	<div>{{ hpml.interpolate(block.title) }}</div>
-	<MkRadio v-for="item in block.values" :key="item" :modelValue="value" :value="item" @update:model-value="updateValue($event)">{{ item }}</MkRadio>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkRadio from '../MkRadio.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { RadioButtonVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkRadio,
-	},
-	props: {
-		block: {
-			type: Object as PropType<RadioButtonVarBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue: string) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue,
-		};
-	},
-});
-</script>
diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue
index 50181b3905..dc06a231f9 100644
--- a/packages/frontend/src/components/page/page.section.vue
+++ b/packages/frontend/src/components/page/page.section.vue
@@ -3,34 +3,23 @@
 	<component :is="'h' + h">{{ block.title }}</component>
 
 	<div class="children">
-		<XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h + 1"/>
+		<XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/>
 	</div>
 </section>
 </template>
 
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, PropType } from 'vue';
-import { SectionBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
+import { SectionBlock } from './block.type';
 
-export default defineComponent({
-	components: {
-		XBlock: defineAsyncComponent(() => import('./page.block.vue')),
-	},
-	props: {
-		block: {
-			type: Object as PropType<SectionBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-		h: {
-			required: true,
-		},
-	},
-});
+const XBlock = defineAsyncComponent(() => import('./page.block.vue'));
+
+defineProps<{
+	block: SectionBlock,
+	h: number,
+	page: Misskey.entities.Page,
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/page/page.switch.vue b/packages/frontend/src/components/page/page.switch.vue
deleted file mode 100644
index b5f3464512..0000000000
--- a/packages/frontend/src/components/page/page.switch.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<div class="hkcxmtwj">
-	<MkSwitch :model-value="value" @update:model-value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkSwitch from '../MkSwitch.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { SwitchVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkSwitch,
-	},
-	props: {
-		block: {
-			type: Object as PropType<SwitchVarBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue: boolean) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.hkcxmtwj {
-	display: inline-block;
-	margin: 16px auto;
-
-	& + .hkcxmtwj {
-		margin-left: 16px;
-	}
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.text-input.vue b/packages/frontend/src/components/page/page.text-input.vue
deleted file mode 100644
index d020a99de8..0000000000
--- a/packages/frontend/src/components/page/page.text-input.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<div>
-	<MkInput class="kudkigyw" :model-value="value" type="text" @update:model-value="updateValue($event)">
-		<template #label>{{ hpml.interpolate(block.text) }}</template>
-	</MkInput>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkInput from '../MkInput.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { TextInputVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkInput,
-	},
-	props: {
-		block: {
-			type: Object as PropType<TextInputVarBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
-	display: inline-block;
-	min-width: 300px;
-	max-width: 450px;
-	margin: 8px 0;
-}
-</style>
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index e0e4959efa..308948b45c 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -1,56 +1,26 @@
 <template>
 <div class="mrdgzndn">
-	<Mfm :key="text" :text="text" :is-note="false" :i="$i"/>
+	<Mfm :text="block.text" :isNote="false" :i="$i"/>
 	<MkUrlPreview v-for="url in urls" :key="url" :url="url" class="url"/>
 </div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, PropType } from 'vue';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
 import * as mfm from 'mfm-js';
-import { TextBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
+import * as Misskey from 'misskey-js';
+import { TextBlock } from './block.type';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 import { $i } from '@/account';
 
-export default defineComponent({
-	components: {
-		MkUrlPreview: defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')),
-	},
-	props: {
-		block: {
-			type: Object as PropType<TextBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	data() {
-		return {
-			text: this.hpml.interpolate(this.block.text),
-			$i,
-		};
-	},
-	computed: {
-		urls(): string[] {
-			if (this.text) {
-				return extractUrlFromMfm(mfm.parse(this.text));
-			} else {
-				return [];
-			}
-		},
-	},
-	watch: {
-		'hpml.vars': {
-			handler() {
-				this.text = this.hpml.interpolate(this.block.text);
-			},
-			deep: true,
-		},
-	},
-});
+const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
+
+const props = defineProps<{
+	block: TextBlock,
+	page: Misskey.entities.Page,
+}>();
+
+const urls = props.block.text ? extractUrlFromMfm(mfm.parse(props.block.text)) : [];
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/page/page.textarea-input.vue b/packages/frontend/src/components/page/page.textarea-input.vue
deleted file mode 100644
index db3a96dd1b..0000000000
--- a/packages/frontend/src/components/page/page.textarea-input.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-<div>
-	<MkTextarea :model-value="value" @update:model-value="updateValue($event)">
-		<template #label>{{ hpml.interpolate(block.text) }}</template>
-	</MkTextarea>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkTextarea from '../MkTextarea.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { TextInputVarBlock } from '@/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkTextarea,
-	},
-	props: {
-		block: {
-			type: Object as PropType<TextInputVarBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue,
-		};
-	},
-});
-</script>
diff --git a/packages/frontend/src/components/page/page.textarea.vue b/packages/frontend/src/components/page/page.textarea.vue
deleted file mode 100644
index 9b82412e8a..0000000000
--- a/packages/frontend/src/components/page/page.textarea.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-<MkTextarea :model-value="text" readonly></MkTextarea>
-</template>
-
-<script lang="ts">
-import { TextBlock } from '@/scripts/hpml/block';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { defineComponent, PropType } from 'vue';
-import MkTextarea from '../MkTextarea.vue';
-
-export default defineComponent({
-	components: {
-		MkTextarea,
-	},
-	props: {
-		block: {
-			type: Object as PropType<TextBlock>,
-			required: true,
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true,
-		},
-	},
-	data() {
-		return {
-			text: this.hpml.interpolate(this.block.text),
-		};
-	},
-	watch: {
-		'hpml.vars': {
-			handler() {
-				this.text = this.hpml.interpolate(this.block.text);
-			},
-			deep: true,
-		},
-	},
-});
-</script>
diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue
index 5f1f62581e..f9291c4d2d 100644
--- a/packages/frontend/src/components/page/page.vue
+++ b/packages/frontend/src/components/page/page.vue
@@ -1,44 +1,17 @@
 <template>
-<div v-if="hpml" class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }">
-	<XBlock v-for="child in page.content" :key="child.id" :block="child" :hpml="hpml" :h="2"/>
+<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }">
+	<XBlock v-for="child in page.content" :key="child.id" :block="child" :h="2"/>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, onMounted, nextTick, PropType } from 'vue';
+<script lang="ts" setup>
+import { onMounted, nextTick } from 'vue';
+import * as Misskey from 'misskey-js';
 import XBlock from './page.block.vue';
-import { Hpml } from '@/scripts/hpml/evaluator';
-import { url } from '@/config';
-import { $i } from '@/account';
 
-export default defineComponent({
-	components: {
-		XBlock,
-	},
-	props: {
-		page: {
-			type: Object as PropType<Record<string, any>>,
-			required: true,
-		},
-	},
-	setup(props, ctx) {
-		const hpml = new Hpml(props.page, {
-			randomSeed: Math.random(),
-			visitor: $i,
-			url: url,
-		});
-
-		onMounted(() => {
-			nextTick(() => {
-				hpml.eval();
-			});
-		});
-
-		return {
-			hpml,
-		};
-	},
-});
+defineProps<{
+	page: Misskey.entities.Page,
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index a89a420d77..de1b5b8a63 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -1,8 +1,7 @@
 import { shallowRef, computed, markRaw } from 'vue';
 import * as Misskey from 'misskey-js';
 import { api, apiGet } from './os';
-import { miLocalStorage } from './local-storage';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { get, set } from '@/scripts/idb-proxy';
 
 const storageCache = await get('emojis');
@@ -17,6 +16,9 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
 	return markRaw([...Array.from(categories), null]);
 });
 
+// TODO: ここら辺副作用なのでいい感じにする
+const stream = useStream();
+
 stream.on('emojiAdded', emojiData => {
 	customEmojis.value = [emojiData.emoji, ...customEmojis.value];
 	set('emojis', customEmojis.value);
@@ -34,10 +36,9 @@ stream.on('emojiDeleted', emojiData => {
 
 export async function fetchCustomEmojis(force = false) {
 	const now = Date.now();
-	const needsMigration = miLocalStorage.getItem('emojis') != null;
 
 	let res;
-	if (force || needsMigration) {
+	if (force) {
 		res = await api('emojis', {});
 	} else {
 		const lastFetchedAt = await get('lastEmojisFetchedAt');
@@ -48,10 +49,6 @@ export async function fetchCustomEmojis(force = false) {
 	customEmojis.value = res.emojis;
 	set('emojis', res.emojis);
 	set('lastEmojisFetchedAt', now);
-	if (needsMigration) {
-		miLocalStorage.removeItem('emojis');
-		miLocalStorage.removeItem('lastEmojisFetchedAt');
-	}
 }
 
 let cachedTags;
diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts
index 5d13497b5f..373141fa35 100644
--- a/packages/frontend/src/directives/tooltip.ts
+++ b/packages/frontend/src/directives/tooltip.ts
@@ -5,7 +5,7 @@ import { defineAsyncComponent, Directive, ref } from 'vue';
 import { isTouchUsing } from '@/scripts/touch';
 import { popup, alert } from '@/os';
 
-const start = isTouchUsing ? 'touchstart' : 'mouseover';
+const start = isTouchUsing ? 'touchstart' : 'mouseenter';
 const end = isTouchUsing ? 'touchend' : 'mouseleave';
 
 export default {
@@ -63,16 +63,24 @@ export default {
 			ev.preventDefault();
 		});
 
-		el.addEventListener(start, () => {
+		el.addEventListener(start, (ev) => {
 			window.clearTimeout(self.showTimer);
 			window.clearTimeout(self.hideTimer);
-			self.showTimer = window.setTimeout(self.show, delay);
+			if (delay === 0) {
+				self.show();
+			} else {
+				self.showTimer = window.setTimeout(self.show, delay);
+			}
 		}, { passive: true });
 
 		el.addEventListener(end, () => {
 			window.clearTimeout(self.showTimer);
 			window.clearTimeout(self.hideTimer);
-			self.hideTimer = window.setTimeout(self.close, delay);
+			if (delay === 0) {
+				self.close();
+			} else {
+				self.hideTimer = window.setTimeout(self.close, delay);
+			}
 		}, { passive: true });
 
 		el.addEventListener('click', () => {
diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts
deleted file mode 100644
index 49e7bb4008..0000000000
--- a/packages/frontend/src/init.ts
+++ /dev/null
@@ -1,527 +0,0 @@
-/**
- * Client entry point
- */
-// https://vitejs.dev/config/build-options.html#build-modulepreload
-import 'vite/modulepreload-polyfill';
-
-import '@/style.scss';
-
-import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
-import { compareVersions } from 'compare-versions';
-import JSON5 from 'json5';
-
-import widgets from '@/widgets';
-import directives from '@/directives';
-import components from '@/components';
-import { version, ui, lang, updateLocale } from '@/config';
-import { applyTheme } from '@/scripts/theme';
-import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
-import { i18n, updateI18n } from '@/i18n';
-import { confirm, alert, post, popup, toast } from '@/os';
-import { stream } from '@/stream';
-import * as sound from '@/scripts/sound';
-import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
-import { defaultStore, ColdDeviceStorage } from '@/store';
-import { fetchInstance, instance } from '@/instance';
-import { makeHotkey } from '@/scripts/hotkey';
-import { deviceKind } from '@/scripts/device-kind';
-import { initializeSw } from '@/scripts/initialize-sw';
-import { reloadChannel } from '@/scripts/unison-reload';
-import { reactionPicker } from '@/scripts/reaction-picker';
-import { getUrlWithoutLoginId } from '@/scripts/login-id';
-import { getAccountFromId } from '@/scripts/get-account-from-id';
-import { deckStore } from '@/ui/deck/deck-store';
-import { miLocalStorage } from '@/local-storage';
-import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
-import { fetchCustomEmojis } from '@/custom-emojis';
-import { mainRouter } from '@/router';
-
-console.info(`Misskey v${version}`);
-
-if (_DEV_) {
-	console.warn('Development mode!!!');
-
-	console.info(`vue ${vueVersion}`);
-
-	// eslint-disable-next-line @typescript-eslint/no-explicit-any
-	(window as any).$i = $i;
-	// eslint-disable-next-line @typescript-eslint/no-explicit-any
-	(window as any).$store = defaultStore;
-
-	window.addEventListener('error', event => {
-		console.error(event);
-		/*
-		alert({
-			type: 'error',
-			title: 'DEV: Unhandled error',
-			text: event.message
-		});
-		*/
-	});
-
-	window.addEventListener('unhandledrejection', event => {
-		console.error(event);
-		/*
-		alert({
-			type: 'error',
-			title: 'DEV: Unhandled promise rejection',
-			text: event.reason
-		});
-		*/
-	});
-}
-
-//#region Detect language & fetch translations
-const localeVersion = miLocalStorage.getItem('localeVersion');
-const localeOutdated = (localeVersion == null || localeVersion !== version);
-if (localeOutdated) {
-	const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
-	if (res.status === 200) {
-		const newLocale = await res.text();
-		const parsedNewLocale = JSON.parse(newLocale);
-		miLocalStorage.setItem('locale', newLocale);
-		miLocalStorage.setItem('localeVersion', version);
-		updateLocale(parsedNewLocale);
-		updateI18n(parsedNewLocale);
-	}
-}
-//#endregion
-
-// タッチデバイスでCSSの:hoverを機能させる
-document.addEventListener('touchend', () => {}, { passive: true });
-
-// 一斉リロード
-reloadChannel.addEventListener('message', path => {
-	if (path !== null) location.href = path;
-	else location.reload();
-});
-
-// If mobile, insert the viewport meta tag
-if (['smartphone', 'tablet'].includes(deviceKind)) {
-	const viewport = 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;
-html.setAttribute('lang', lang);
-//#endregion
-
-//#region loginId
-const params = new URLSearchParams(location.search);
-const loginId = params.get('loginId');
-
-if (loginId) {
-	const target = getUrlWithoutLoginId(location.href);
-
-	if (!$i || $i.id !== loginId) {
-		const account = await getAccountFromId(loginId);
-		if (account) {
-			await login(account.token, target);
-		}
-	}
-
-	history.replaceState({ misskey: 'loginId' }, '', target);
-}
-
-//#endregion
-
-//#region Fetch user
-if ($i && $i.token) {
-	if (_DEV_) {
-		console.log('account cache found. refreshing...');
-	}
-
-	refreshAccount();
-} else {
-	if (_DEV_) {
-		console.log('no account cache found.');
-	}
-
-	// 連携ログインの場合用にCookieを参照する
-	const i = (document.cookie.match(/igi=(\w+)/) ?? [null, null])[1];
-
-	if (i != null && i !== 'null') {
-		if (_DEV_) {
-			console.log('signing...');
-		}
-
-		try {
-			document.body.innerHTML = '<div>Please wait...</div>';
-			await login(i);
-		} catch (err) {
-			// Render the error screen
-			// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
-			document.body.innerHTML = '<div id="err">Oops!</div>';
-		}
-	} else {
-		if (_DEV_) {
-			console.log('not signed in');
-		}
-	}
-}
-//#endregion
-
-const fetchInstanceMetaPromise = fetchInstance();
-
-fetchInstanceMetaPromise.then(() => {
-	miLocalStorage.setItem('v', instance.version);
-
-	// Init service worker
-	initializeSw();
-});
-
-try {
-	await fetchCustomEmojis();
-} catch (err) { /* empty */ }
-
-const app = createApp(
-	new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
-	!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
-	ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
-	ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
-	defineAsyncComponent(() => import('@/ui/universal.vue')),
-);
-
-if (_DEV_) {
-	app.config.performance = true;
-}
-
-widgets(app);
-directives(app);
-components(app);
-
-const splash = document.getElementById('splash');
-// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
-if (splash) splash.addEventListener('transitionend', () => {
-	splash.remove();
-});
-
-await deckStore.ready;
-
-// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
-// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する
-const rootEl = ((): HTMLElement => {
-	const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
-
-	const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
-
-	if (currentRoot) {
-		console.warn('multiple import detected');
-		return currentRoot;
-	}
-
-	const root = document.createElement('div');
-	root.id = MISSKEY_MOUNT_DIV_ID;
-	document.body.appendChild(root);
-	return root;
-})();
-
-app.mount(rootEl);
-
-// boot.jsのやつを解除
-window.onerror = null;
-window.onunhandledrejection = null;
-
-reactionPicker.init();
-
-if (splash) {
-	splash.style.opacity = '0';
-	splash.style.pointerEvents = 'none';
-}
-
-// クライアントが更新されたか?
-const lastVersion = miLocalStorage.getItem('lastVersion');
-if (lastVersion !== version) {
-	miLocalStorage.setItem('lastVersion', version);
-
-	// テーマリビルドするため
-	miLocalStorage.removeItem('theme');
-
-	try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
-		if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
-			// ログインしてる場合だけ
-			if ($i) {
-				popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
-			}
-		}
-	} catch (err) { /* empty */ }
-}
-
-await defaultStore.ready;
-
-// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
-watch(defaultStore.reactiveState.darkMode, (darkMode) => {
-	applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
-}, { immediate: miLocalStorage.getItem('theme') == null });
-
-const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
-const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
-
-watch(darkTheme, (theme) => {
-	if (defaultStore.state.darkMode) {
-		applyTheme(theme);
-	}
-});
-
-watch(lightTheme, (theme) => {
-	if (!defaultStore.state.darkMode) {
-		applyTheme(theme);
-	}
-});
-
-//#region Sync dark mode
-if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-	defaultStore.set('darkMode', isDeviceDarkmode());
-}
-
-window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
-	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-		defaultStore.set('darkMode', mql.matches);
-	}
-});
-//#endregion
-
-fetchInstanceMetaPromise.then(() => {
-	if (defaultStore.state.themeInitial) {
-		if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
-		if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
-		defaultStore.set('themeInitial', false);
-	}
-});
-
-watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
-	document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
-}, { immediate: true });
-
-watch(defaultStore.reactiveState.useBlurEffect, v => {
-	if (v) {
-		document.documentElement.style.removeProperty('--blur');
-	} else {
-		document.documentElement.style.setProperty('--blur', 'none');
-	}
-}, { immediate: true });
-
-let reloadDialogShowing = false;
-stream.on('_disconnected_', async () => {
-	if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
-		location.reload();
-	} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
-		if (reloadDialogShowing) return;
-		reloadDialogShowing = true;
-		const { canceled } = await confirm({
-			type: 'warning',
-			title: i18n.ts.disconnectedFromServer,
-			text: i18n.ts.reloadConfirm,
-		});
-		reloadDialogShowing = false;
-		if (!canceled) {
-			location.reload();
-		}
-	}
-});
-
-for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
-	import('./plugin').then(async ({ install }) => {
-		// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
-		await new Promise(r => setTimeout(r, 0));
-		install(plugin);
-	});
-}
-
-const hotkeys = {
-	'd': (): void => {
-		defaultStore.set('darkMode', !defaultStore.state.darkMode);
-	},
-	's': (): void => {
-		mainRouter.push('/search');
-	},
-};
-
-if ($i) {
-	// only add post shortcuts if logged in
-	hotkeys['p|n'] = post;
-
-	if (defaultStore.state.accountSetupWizard !== -1) {
-		// このウィザードが実装される前に登録したユーザーには表示させないため
-		// TODO: そのうち消す
-		if (Date.now() - new Date($i.createdAt).getTime() < 1000 * 60 * 60 * 24) {
-			popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
-		} else {
-			defaultStore.set('accountSetupWizard', -1);
-		}
-	}
-
-	if ($i.isDeleted) {
-		alert({
-			type: 'warning',
-			text: i18n.ts.accountDeletionInProgress,
-		});
-	}
-
-	const now = new Date();
-	const m = now.getMonth() + 1;
-	const d = now.getDate();
-	
-	if ($i.birthday) {
-		const bm = parseInt($i.birthday.split('-')[1]);
-		const bd = parseInt($i.birthday.split('-')[2]);
-		if (m === bm && d === bd) {
-			claimAchievement('loggedInOnBirthday');
-		}
-	}
-
-	if (m === 1 && d === 1) {
-		claimAchievement('loggedInOnNewYearsDay');
-	}
-
-	if ($i.loggedInDays >= 3) claimAchievement('login3');
-	if ($i.loggedInDays >= 7) claimAchievement('login7');
-	if ($i.loggedInDays >= 15) claimAchievement('login15');
-	if ($i.loggedInDays >= 30) claimAchievement('login30');
-	if ($i.loggedInDays >= 60) claimAchievement('login60');
-	if ($i.loggedInDays >= 100) claimAchievement('login100');
-	if ($i.loggedInDays >= 200) claimAchievement('login200');
-	if ($i.loggedInDays >= 300) claimAchievement('login300');
-	if ($i.loggedInDays >= 400) claimAchievement('login400');
-	if ($i.loggedInDays >= 500) claimAchievement('login500');
-	if ($i.loggedInDays >= 600) claimAchievement('login600');
-	if ($i.loggedInDays >= 700) claimAchievement('login700');
-	if ($i.loggedInDays >= 800) claimAchievement('login800');
-	if ($i.loggedInDays >= 900) claimAchievement('login900');
-	if ($i.loggedInDays >= 1000) claimAchievement('login1000');
-
-	if ($i.notesCount > 0) claimAchievement('notes1');
-	if ($i.notesCount >= 10) claimAchievement('notes10');
-	if ($i.notesCount >= 100) claimAchievement('notes100');
-	if ($i.notesCount >= 500) claimAchievement('notes500');
-	if ($i.notesCount >= 1000) claimAchievement('notes1000');
-	if ($i.notesCount >= 5000) claimAchievement('notes5000');
-	if ($i.notesCount >= 10000) claimAchievement('notes10000');
-	if ($i.notesCount >= 20000) claimAchievement('notes20000');
-	if ($i.notesCount >= 30000) claimAchievement('notes30000');
-	if ($i.notesCount >= 40000) claimAchievement('notes40000');
-	if ($i.notesCount >= 50000) claimAchievement('notes50000');
-	if ($i.notesCount >= 60000) claimAchievement('notes60000');
-	if ($i.notesCount >= 70000) claimAchievement('notes70000');
-	if ($i.notesCount >= 80000) claimAchievement('notes80000');
-	if ($i.notesCount >= 90000) claimAchievement('notes90000');
-	if ($i.notesCount >= 100000) claimAchievement('notes100000');
-
-	if ($i.followersCount > 0) claimAchievement('followers1');
-	if ($i.followersCount >= 10) claimAchievement('followers10');
-	if ($i.followersCount >= 50) claimAchievement('followers50');
-	if ($i.followersCount >= 100) claimAchievement('followers100');
-	if ($i.followersCount >= 300) claimAchievement('followers300');
-	if ($i.followersCount >= 500) claimAchievement('followers500');
-	if ($i.followersCount >= 1000) claimAchievement('followers1000');
-
-	if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
-		claimAchievement('passedSinceAccountCreated1');
-	}
-	if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
-		claimAchievement('passedSinceAccountCreated2');
-	}
-	if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
-		claimAchievement('passedSinceAccountCreated3');
-	}
-
-	if (claimedAchievements.length >= 30) {
-		claimAchievement('collectAchievements30');
-	}
-
-	window.setInterval(() => {
-		if (Math.floor(Math.random() * 20000) === 0) {
-			claimAchievement('justPlainLucky');
-		}
-	}, 1000 * 10);
-
-	window.setTimeout(() => {
-		claimAchievement('client30min');
-	}, 1000 * 60 * 30);
-
-	window.setTimeout(() => {
-		claimAchievement('client60min');
-	}, 1000 * 60 * 60);
-
-	const lastUsed = miLocalStorage.getItem('lastUsed');
-	if (lastUsed) {
-		const lastUsedDate = parseInt(lastUsed, 10);
-		// 二時間以上前なら
-		if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
-			toast(i18n.t('welcomeBackWithName', {
-				name: $i.name || $i.username,
-			}));
-		}
-	}
-	miLocalStorage.setItem('lastUsed', Date.now().toString());
-
-	const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
-	const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
-	if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
-		if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
-			popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
-		}
-	}
-
-	if ('Notification' in window) {
-		// 許可を得ていなかったらリクエスト
-		if (Notification.permission === 'default') {
-			Notification.requestPermission();
-		}
-	}
-
-	const main = markRaw(stream.useChannel('main', null, 'System'));
-
-	// 自分の情報が更新されたとき
-	main.on('meUpdated', i => {
-		updateAccount(i);
-	});
-
-	main.on('readAllNotifications', () => {
-		updateAccount({ hasUnreadNotification: false });
-	});
-
-	main.on('unreadNotification', () => {
-		updateAccount({ hasUnreadNotification: true });
-	});
-
-	main.on('unreadMention', () => {
-		updateAccount({ hasUnreadMentions: true });
-	});
-
-	main.on('readAllUnreadMentions', () => {
-		updateAccount({ hasUnreadMentions: false });
-	});
-
-	main.on('unreadSpecifiedNote', () => {
-		updateAccount({ hasUnreadSpecifiedNotes: true });
-	});
-
-	main.on('readAllUnreadSpecifiedNotes', () => {
-		updateAccount({ hasUnreadSpecifiedNotes: false });
-	});
-
-	main.on('readAllAntennas', () => {
-		updateAccount({ hasUnreadAntenna: false });
-	});
-
-	main.on('unreadAntenna', () => {
-		updateAccount({ hasUnreadAntenna: true });
-		sound.play('antenna');
-	});
-
-	main.on('readAllAnnouncements', () => {
-		updateAccount({ hasUnreadAnnouncement: false });
-	});
-
-	// トークンが再生成されたとき
-	// このままではMisskeyが利用できないので強制的にサインアウトさせる
-	main.on('myTokenRegenerated', () => {
-		signout();
-	});
-}
-
-// shortcut
-document.addEventListener('keydown', makeHotkey(hotkeys));
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 9e0594db3c..b487916ff6 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -86,8 +86,13 @@
 				</FormSection>
 				<FormSection>
 					<template #label>Special thanks</template>
-					<div style="text-align: center;">
-						<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
+					<div class="_gaps" style="text-align: center;">
+						<div>
+							<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
+						</div>
+						<div>
+							<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
+						</div>
 					</div>
 				</FormSection>
 			</div>
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index 9e8af43024..f8200570f9 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -2,7 +2,7 @@
 <MkStickyContainer>
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="900">
-		<div class="lcixvhis">
+		<div>
 			<div class="reports">
 				<div class="">
 					<div class="inputs" style="display: flex;">
@@ -87,9 +87,3 @@ definePageMetadata({
 	icon: 'ti ti-exclamation-circle',
 });
 </script>
-
-<style lang="scss" scoped>
-.lcixvhis {
-	margin: var(--margin);
-}
-</style>
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index b76e4b9114..638b193c11 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -2,7 +2,7 @@
 <MkStickyContainer>
 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :content-max="900">
-		<div class="ztgjmzrw _gaps_m">
+		<div class="_gaps_m">
 			<section v-for="announcement in announcements" class="">
 				<div class="_panel _gaps_m" style="padding: 24px;">
 					<MkInput v-model="announcement.title">
@@ -113,9 +113,3 @@ definePageMetadata({
 	icon: 'ti ti-speakerphone',
 });
 </script>
-
-<style lang="scss" scoped>
-.ztgjmzrw {
-	margin: var(--margin);
-}
-</style>
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index c189437246..2b13a7c80c 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -3,7 +3,7 @@
 	<MkStickyContainer>
 		<template #header><XHeader :actions="headerActions"/></template>
 		<MkSpacer :content-max="900">
-			<div class="xrmjdkdw">
+			<div>
 				<div>
 					<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
 						<MkSelect v-model="origin" style="margin: 0; flex: 1;">
@@ -109,9 +109,3 @@ definePageMetadata(computed(() => ({
 	icon: 'ti ti-cloud',
 })));
 </script>
-
-<style lang="scss" scoped>
-.xrmjdkdw {
-	margin: var(--margin);
-}
-</style>
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index 08a29bf550..af7bc70551 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -67,7 +67,3 @@ onMounted(() => {
 	});
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue
index 6a11e8b768..a3c8659ce5 100644
--- a/packages/frontend/src/pages/admin/overview.queue.chart.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue
@@ -132,7 +132,3 @@ defineExpose({
 	pushData,
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index 1f56a2826a..69ca89e226 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -33,9 +33,9 @@
 import { markRaw, onMounted, onUnmounted, ref } from 'vue';
 import XChart from './overview.queue.chart.vue';
 import number from '@/filters/number';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 
-const connection = markRaw(stream.useChannel('queueStats'));
+const connection = markRaw(useStream().useChannel('queueStats'));
 
 const activeSincePrevTick = ref(0);
 const active = ref(0);
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index 5c96c07bfb..46a93ac5d8 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -1,6 +1,6 @@
 <template>
 <MkSpacer :content-max="1000">
-	<div ref="rootEl" class="edbbcaef">
+	<div ref="rootEl" :class="$style.root">
 		<MkFoldableSection class="item">
 			<template #header>Stats</template>
 			<XStats/>
@@ -72,7 +72,7 @@ import XRetention from './overview.retention.vue';
 import XModerators from './overview.moderators.vue';
 import XHeatmap from './overview.heatmap.vue';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
@@ -87,7 +87,7 @@ let federationSubActive = $ref<number | null>(null);
 let federationSubActiveDiff = $ref<number | null>(null);
 let newUsers = $ref(null);
 let activeInstances = $shallowRef(null);
-const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
+const queueStatsConnection = markRaw(useStream().useChannel('queueStats'));
 const now = new Date();
 const filesPagination = {
 	endpoint: 'admin/drive/files' as const,
@@ -176,8 +176,8 @@ definePageMetadata({
 });
 </script>
 
-<style lang="scss" scoped>
-.edbbcaef {
+<style lang="scss" module>
+.root {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
 	grid-gap: 16px;
diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue
index 1a1f6a9db4..9bc0eee212 100644
--- a/packages/frontend/src/pages/admin/queue.chart.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue
@@ -132,7 +132,3 @@ defineExpose({
 	pushData,
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 100d1ea545..728f3b2c80 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -47,11 +47,11 @@ import { markRaw, onMounted, onUnmounted, ref } from 'vue';
 import XChart from './queue.chart.chart.vue';
 import number from '@/filters/number';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import MkFolder from '@/components/MkFolder.vue';
 
-const connection = markRaw(stream.useChannel('queueStats'));
+const connection = markRaw(useStream().useChannel('queueStats'));
 
 const activeSincePrevTick = ref(0);
 const active = ref(0);
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 9aa564a7da..59ca6c83bd 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -5,6 +5,8 @@
 		<div v-if="channel && tab === 'overview'" class="_gaps">
 			<div class="_panel" :class="$style.bannerContainer">
 				<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
+				<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
+				<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton>
 				<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" :class="$style.banner">
 					<div :class="$style.bannerStatus">
 						<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
@@ -13,13 +15,10 @@
 					<div :class="$style.bannerFade"></div>
 				</div>
 				<div v-if="channel.description" :class="$style.description">
-					<Mfm :text="channel.description" :is-note="false" :i="$i"/>
+					<Mfm :text="channel.description" :isNote="false" :i="$i"/>
 				</div>
 			</div>
 
-			<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
-			<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-star"></i></MkButton>
-
 			<MkFoldableSection>
 				<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
 				<div v-if="channel.pinnedNotes.length > 0" class="_gaps">
@@ -229,6 +228,13 @@ definePageMetadata(computed(() => channel ? {
 	left: 16px;
 }
 
+.favorite {
+	position: absolute;
+	z-index: 1;
+	top: 16px;
+	right: 16px;
+}
+
 .banner {
 	position: relative;
 	height: 200px;
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index e3ac3f4c9b..9207a9fa9b 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -5,7 +5,7 @@
 		<div v-if="clip">
 			<div class="okzinsic _panel">
 				<div v-if="clip.description" class="description">
-					<Mfm :text="clip.description" :is-note="false" :i="$i"/>
+					<Mfm :text="clip.description" :isNote="false" :i="$i"/>
 				</div>
 				<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
 				<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 84bc153b71..3c829d6a8e 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -10,8 +10,8 @@
 	<template #header>:{{ emoji.name }}:</template>
 
 	<MkSpacer :margin-min="20" :margin-max="28">
-		<div class="yigymqpb _gaps_m">
-			<img :src="`/emoji/${emoji.name}.webp`" class="img"/>
+		<div class="_gaps_m">
+			<img :src="`/emoji/${emoji.name}.webp`" :class="$style.img"/>
 			<MkInput v-model="name">
 				<template #label>{{ i18n.ts.name }}</template>
 			</MkInput>
@@ -99,12 +99,10 @@ async function del() {
 }
 </script>
 
-<style lang="scss" scoped>
-.yigymqpb {
-	> .img {
-		display: block;
-		height: 64px;
-		margin: 0 auto;
-	}
+<style lang="scss" module>
+.img {
+	display: block;
+	height: 64px;
+	margin: 0 auto;
 }
 </style>
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index 816825e5b6..a3daa8ea46 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkInput from '@/components/MkInput.vue';
 import { useRouter } from '@/router';
 
-const PRESET_DEFAULT = `/// @ 0.13.2
+const PRESET_DEFAULT = `/// @ 0.13.3
 
 var name = ""
 
@@ -51,7 +51,7 @@ Ui:render([
 ])
 `;
 
-const PRESET_OMIKUJI = `/// @ 0.13.2
+const PRESET_OMIKUJI = `/// @ 0.13.3
 // ユーザーごとに日替わりのおみくじのプリセット
 
 // 選択肢
@@ -94,7 +94,7 @@ Ui:render([
 ])
 `;
 
-const PRESET_SHUFFLE = `/// @ 0.13.2
+const PRESET_SHUFFLE = `/// @ 0.13.3
 // 巻き戻し可能な文字シャッフルのプリセット
 
 let string = "ペペロンチーノ"
@@ -173,7 +173,7 @@ var cursor = 0
 do()
 `;
 
-const PRESET_QUIZ = `/// @ 0.13.2
+const PRESET_QUIZ = `/// @ 0.13.3
 let title = '地理クイズ'
 
 let qas = [{
@@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({
 Ui:render(qaEls)
 `;
 
-const PRESET_TIMELINE = `/// @ 0.13.2
+const PRESET_TIMELINE = `/// @ 0.13.3
 // APIリクエストを行いローカルタイムラインを表示するプリセット
 
 @fetch() {
@@ -442,7 +442,3 @@ definePageMetadata(computed(() => flash ? {
 	title: i18n.ts._play.new,
 }));
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index fc9cc7ae9e..3855a6d9d8 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -7,7 +7,7 @@
 				<MkFoldableSection class="_margin">
 					<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
 					<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
-						<div class="vfpdbgtk">
+						<div :class="$style.items">
 							<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
 						</div>
 					</MkPagination>
@@ -15,7 +15,7 @@
 				<MkFoldableSection class="_margin">
 					<template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template>
 					<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
-						<div class="vfpdbgtk">
+						<div :class="$style.items">
 							<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
 						</div>
 					</MkPagination>
@@ -23,7 +23,7 @@
 			</div>
 			<div v-else-if="tab === 'liked'">
 				<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
-					<div class="vfpdbgtk">
+					<div :class="$style.items">
 						<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
 					</div>
 				</MkPagination>
@@ -31,7 +31,7 @@
 			<div v-else-if="tab === 'my'">
 				<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA>
 				<MkPagination v-slot="{items}" :pagination="myPostsPagination">
-					<div class="vfpdbgtk">
+					<div :class="$style.items">
 						<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
 					</div>
 				</MkPagination>
@@ -119,15 +119,11 @@ definePageMetadata({
 });
 </script>
 
-<style lang="scss" scoped>
-.vfpdbgtk {
+<style lang="scss" module>
+.items {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
 	grid-gap: 12px;
 	margin: 0 var(--margin);
-
-	> .post {
-
-	}
 }
 </style>
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index c35af3e22a..14ab18a3df 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -38,7 +38,3 @@ definePageMetadata({
 	icon: 'ti ti-antenna',
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index 913fbde8e9..da9b2de48f 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -36,7 +36,3 @@ definePageMetadata({
 	icon: 'ti ti-antenna',
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue
index 26b7bcc71b..dd5f3222af 100644
--- a/packages/frontend/src/pages/my-antennas/editor.vue
+++ b/packages/frontend/src/pages/my-antennas/editor.vue
@@ -1,6 +1,6 @@
 <template>
 <MkSpacer :content-max="700">
-	<div class="shaynizk">
+	<div>
 		<div class="_gaps_m">
 			<MkInput v-model="name">
 				<template #label>{{ i18n.ts.name }}</template>
@@ -33,7 +33,7 @@
 			<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
 			<MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch>
 		</div>
-		<div class="actions">
+		<div :class="$style.actions">
 			<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 			<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 		</div>
@@ -128,12 +128,10 @@ function addUser() {
 }
 </script>
 
-<style lang="scss" scoped>
-.shaynizk {
-	> .actions {
-		margin-top: 16px;
-		padding: 24px 0;
-		border-top: solid 0.5px var(--divider);
-	}
+<style lang="scss" module>
+.actions {
+	margin-top: 16px;
+	padding: 24px 0;
+	border-top: solid 0.5px var(--divider);
 }
 </style>
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index 1b292e8f3c..eca3feda62 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -8,8 +8,8 @@
 		</button>
 	</template>
 
-	<section class="oyyftmcf">
-		<MkDriveFileThumbnail v-if="file" class="preview" :file="file" fit="contain" @click="choose()"/>
+	<section>
+		<MkDriveFileThumbnail v-if="file" style="height: 150px;" :file="file" fit="contain" @click="choose()"/>
 	</section>
 </XContainer>
 </template>
@@ -54,11 +54,3 @@ onMounted(async () => {
 	}
 });
 </script>
-
-<style lang="scss" scoped>
-.oyyftmcf {
-	> .preview {
-		height: 150px;
-	}
-}
-</style>
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 bf21ae3c67..3b15c17747 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
@@ -3,8 +3,8 @@
 <XContainer :draggable="true" @remove="() => $emit('remove')">
 	<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
 
-	<section class="vckmsadr">
-		<textarea v-model="text"></textarea>
+	<section>
+		<textarea v-model="text" :class="$style.textarea"></textarea>
 	</section>
 </XContainer>
 </template>
@@ -33,23 +33,21 @@ watch($$(text), () => {
 });
 </script>
 
-<style lang="scss" scoped>
-.vckmsadr {
-	> textarea {
-		display: block;
-		-webkit-appearance: none;
-		-moz-appearance: none;
-		appearance: none;
-		width: 100%;
-		min-width: 100%;
-		min-height: 150px;
-		border: none;
-		box-shadow: none;
-		padding: 16px;
-		background: transparent;
-		color: var(--fg);
-		font-size: 14px;
-		box-sizing: border-box;
-	}
+<style lang="scss" module>
+.textarea {
+	display: block;
+	-webkit-appearance: none;
+	-moz-appearance: none;
+	appearance: none;
+	width: 100%;
+	min-width: 100%;
+	min-height: 150px;
+	border: none;
+	box-shadow: none;
+	padding: 16px;
+	background: transparent;
+	color: var(--fg);
+	font-size: 14px;
+	box-sizing: border-box;
 }
 </style>
diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
index 97bdcfe80f..2c3d59256c 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
@@ -9,49 +9,41 @@
 </Sortable>
 </template>
 
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
 import XSection from './els/page-editor.el.section.vue';
 import XText from './els/page-editor.el.text.vue';
 import XImage from './els/page-editor.el.image.vue';
 import XNote from './els/page-editor.el.note.vue';
 
-export default defineComponent({
-	components: {
-		Sortable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
-		XSection, XText, XImage, XNote,
-	},
+const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
-	props: {
-		modelValue: {
-			type: Array,
-			required: true,
-		},
-	},
+const props = defineProps<{
+	modelValue: any[];
+}>();
 
-	emits: ['update:modelValue'],
+const emit = defineEmits<{
+	(ev: 'update:modelValue', value: any[]): void;
+}>();
 
-	methods: {
-		updateItem(v) {
-			const i = this.modelValue.findIndex(x => x.id === v.id);
-			const newValue = [
-				...this.modelValue.slice(0, i),
-				v,
-				...this.modelValue.slice(i + 1),
-			];
-			this.$emit('update:modelValue', newValue);
-		},
+function updateItem(v) {
+	const i = props.modelValue.findIndex(x => x.id === v.id);
+	const newValue = [
+		...props.modelValue.slice(0, i),
+		v,
+		...props.modelValue.slice(i + 1),
+	];
+	emit('update:modelValue', newValue);
+}
 
-		removeItem(el) {
-			const i = this.modelValue.findIndex(x => x.id === el.id);
-			const newValue = [
-				...this.modelValue.slice(0, i),
-				...this.modelValue.slice(i + 1),
-			];
-			this.$emit('update:modelValue', newValue);
-		},
-	},
-});
+function removeItem(el) {
+	const i = props.modelValue.findIndex(x => x.id === el.id);
+	const newValue = [
+		...props.modelValue.slice(0, i),
+		...props.modelValue.slice(i + 1),
+	];
+	emit('update:modelValue', newValue);
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue
index dd733403af..0842b4fd26 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.container.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
+<div class="cpjygsrt">
 	<header>
 		<div class="title"><slot name="header"></slot></div>
 		<div class="buttons">
@@ -16,58 +16,40 @@
 			</button>
 		</div>
 	</header>
-	<p v-show="showBody" v-if="error != null" class="error">{{ i18n.t('_pages.script.typeError', { slot: error.arg + 1, expect: i18n.t(`script.types.${error.expect}`), actual: i18n.t(`script.types.${error.actual}`) }) }}</p>
-	<p v-show="showBody" v-if="warn != null" class="warn">{{ i18n.t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
 	<div v-show="showBody" class="body">
 		<slot></slot>
 	</div>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
 import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		expanded: {
-			type: Boolean,
-			default: true,
-		},
-		removable: {
-			type: Boolean,
-			default: true,
-		},
-		draggable: {
-			type: Boolean,
-			default: false,
-		},
-		error: {
-			required: false,
-			default: null,
-		},
-		warn: {
-			required: false,
-			default: null,
-		},
-	},
-	emits: ['toggle', 'remove'],
-	data() {
-		return {
-			showBody: this.expanded,
-			i18n,
-		};
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			this.showBody = show;
-			this.$emit('toggle', show);
-		},
-		remove() {
-			this.$emit('remove');
-		},
-	},
+const props = withDefaults(defineProps<{
+	expanded?: boolean;
+	removable?: boolean;
+	draggable?: boolean;
+}>(), {
+	expanded: true,
+	removable: true,
 });
+
+const emit = defineEmits<{
+	(ev: 'toggle', show: boolean): void;
+	(ev: 'remove'): void;
+}>();
+
+const showBody = ref(props.expanded);
+
+function toggleContent(show: boolean) {
+	showBody.value = show;
+	emit('toggle', show);
+}
+
+function remove() {
+	emit('remove');
+}
 </script>
 
 <style lang="scss" scoped>
@@ -128,20 +110,6 @@ export default defineComponent({
 		}
 	}
 
-	> .warn {
-		color: #b19e49;
-		margin: 0;
-		padding: 16px 16px 0 16px;
-		font-size: 14px;
-	}
-
-	> .error {
-		color: #f00;
-		margin: 0;
-		padding: 16px 16px 0 16px;
-		font-size: 14px;
-	}
-
 	> .body {
 		::v-deep(.juejbjww), ::v-deep(.eiipwacr) {
 			&:not(.inline):first-child {
diff --git a/packages/frontend/src/pages/preview.vue b/packages/frontend/src/pages/preview.vue
deleted file mode 100644
index 354f686e46..0000000000
--- a/packages/frontend/src/pages/preview.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<template>
-<div class="graojtoi">
-	<MkSample/>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed } from 'vue';
-import MkSample from '@/components/MkSample.vue';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata(computed(() => ({
-	title: i18n.ts.preview,
-	icon: 'ti ti-eye',
-})));
-</script>
-
-<style lang="scss" scoped>
-.graojtoi {
-	padding: var(--margin);
-}
-</style>
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index c687b89eab..52b7c256e0 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -93,6 +93,3 @@ definePageMetadata({
 	icon: 'ti ti-adjustments',
 });
 </script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue
index 00e2ca5e03..6ff07e2b77 100644
--- a/packages/frontend/src/pages/registry.value.vue
+++ b/packages/frontend/src/pages/registry.value.vue
@@ -118,6 +118,3 @@ definePageMetadata({
 	icon: 'ti ti-adjustments',
 });
 </script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue
index 5a029cb0c7..016af22815 100644
--- a/packages/frontend/src/pages/registry.vue
+++ b/packages/frontend/src/pages/registry.vue
@@ -68,6 +68,3 @@ definePageMetadata({
 	icon: 'ti ti-adjustments',
 });
 </script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index 38c88cc650..ab7a96a8d0 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -53,7 +53,3 @@ definePageMetadata({
 	icon: 'ti ti-lock',
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 7c1522e059..9d06d35e60 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -144,12 +144,14 @@
 	</FormSection>
 
 	<FormSection>
-		<MkSwitch v-model="aiChanMode">{{ i18n.ts.aiChanMode }}</MkSwitch>
+		<template #label>{{ i18n.ts.other }}</template>
+
+		<div class="_gaps">
+			<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
+			<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
+			<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
+		</div>
 	</FormSection>
-
-	<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
-
-	<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
 </div>
 </template>
 
@@ -211,10 +213,10 @@ const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'))
 const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
 const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
 const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
-const aiChanMode = computed(defaultStore.makeGetterSetter('aiChanMode'));
 const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance'));
 const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
 const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
+const showTimelineReplies = computed(defaultStore.makeGetterSetter('showTimelineReplies'));
 
 watch(lang, () => {
 	miLocalStorage.setItem('lang', lang.value as string);
@@ -243,7 +245,6 @@ watch([
 	useSystemFont,
 	enableInfiniteScroll,
 	squareAvatars,
-	aiChanMode,
 	showNoteActionsOnlyHover,
 	showGapBetweenNotesInTimeline,
 	instanceTicker,
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 776305d723..0b73780a8b 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -53,6 +53,17 @@
 					</MkSwitch>
 				</div>
 			</MkFolder>
+
+			<MkFolder>
+				<template #icon><i class="ti ti-code"></i></template>
+				<template #label>{{ i18n.ts.developer }}</template>
+
+				<div class="_gaps_m">
+					<MkSwitch v-model="devMode">
+						<template #label>{{ i18n.ts.devMode }}</template>
+					</MkSwitch>
+				</div>
+			</MkFolder>
 		</div>
 	</FormSection>
 
@@ -80,6 +91,7 @@ import FormSection from '@/components/form/section.vue';
 
 const reportError = computed(defaultStore.makeGetterSetter('reportError'));
 const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
+const devMode = computed(defaultStore.makeGetterSetter('devMode'));
 
 function onChangeInjectFeaturedNote(v) {
 	os.api('i/update', {
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 8b57dceefb..f90ca737e9 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -94,7 +94,3 @@ definePageMetadata({
 	icon: 'ti ti-plug',
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 6613ce4c1d..86f633c445 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -40,7 +40,7 @@ import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os';
 import { ColdDeviceStorage, defaultStore } from '@/store';
 import { unisonReload } from '@/scripts/unison-reload';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { version, host } from '@/config';
@@ -125,7 +125,7 @@ type Profile = {
 	};
 };
 
-const connection = $i && stream.useChannel('main');
+const connection = $i && useStream().useChannel('main');
 
 let profiles = $ref<Record<string, Profile> | null>(null);
 
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 6ffd682610..35af43d789 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="_gaps_m">
-	<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
-		<div class="avatar">
-			<MkAvatar class="avatar" :user="$i" @click="changeAvatar"/>
-			<MkButton primary rounded class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
+	<div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
+		<div :class="$style.avatarContainer">
+			<MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/>
+			<MkButton primary rounded :class="$style.avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
 		</div>
-		<MkButton primary rounded class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
+		<MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
 	</div>
 
 	<MkInput v-model="profile.name" :max="30" manual-save>
@@ -91,8 +91,6 @@
 		<option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
 		<option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option>
 	</MkSelect>
-
-	<MkSwitch v-model="profile.showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
 </div>
 </template>
 
@@ -248,36 +246,39 @@ definePageMetadata({
 });
 </script>
 
-<style lang="scss" scoped>
-.llvierxe {
+<style lang="scss" module>
+.avatarAndBanner {
 	position: relative;
 	background-size: cover;
 	background-position: center;
 	border: solid 1px var(--divider);
 	border-radius: 10px;
 	overflow: clip;
-
-	> .avatar {
-		display: inline-block;
-		text-align: center;
-		padding: 16px;
-
-		> .avatar {
-			display: inline-block;
-			width: 72px;
-			height: 72px;
-			margin: 0 auto 16px auto;
-		}
-	}
-
-	> .bannerEdit {
-		position: absolute;
-		top: 16px;
-		right: 16px;
-	}
 }
-</style>
-<style lang="scss" module>
+
+.avatarContainer {
+	display: inline-block;
+	text-align: center;
+	padding: 16px;
+}
+
+.avatar {
+	display: inline-block;
+	width: 72px;
+	height: 72px;
+	margin: 0 auto 16px auto;
+}
+
+.avatarEdit {
+	
+}
+
+.bannerEdit {
+	position: absolute;
+	top: 16px;
+	right: 16px;
+}
+
 .metadataRoot {
 	container-type: inline-size;
 }
diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue
index ed913731d3..10169ccf18 100644
--- a/packages/frontend/src/pages/settings/reaction.vue
+++ b/packages/frontend/src/pages/settings/reaction.vue
@@ -3,15 +3,15 @@
 	<FromSlot>
 		<template #label>{{ i18n.ts.reactionSettingDescription }}</template>
 		<div v-panel style="border-radius: 6px;">
-			<Sortable v-model="reactions" class="zoaiodol" :item-key="item => item" :animation="150" :delay="100" :delay-on-touch-only="true">
+			<Sortable v-model="reactions" :class="$style.reactions" :item-key="item => item" :animation="150" :delay="100" :delay-on-touch-only="true">
 				<template #item="{element}">
-					<button class="_button item" @click="remove(element, $event)">
+					<button class="_button" :class="$style.reactionsItem" @click="remove(element, $event)">
 						<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/>
 						<MkEmoji v-else :emoji="element" :normal="true"/>
 					</button>
 				</template>
 				<template #footer>
-					<button class="_button add" @click="chooseEmoji"><i class="ti ti-plus"></i></button>
+					<button class="_button" :class="$style.reactionsAdd" @click="chooseEmoji"><i class="ti ti-plus"></i></button>
 				</template>
 			</Sortable>
 		</div>
@@ -135,20 +135,20 @@ definePageMetadata({
 });
 </script>
 
-<style lang="scss" scoped>
-.zoaiodol {
+<style lang="scss" module>
+.reactions {
 	padding: 12px;
 	font-size: 1.1em;
+}
 
-	> .item {
-		display: inline-block;
-		padding: 8px;
-		cursor: move;
-	}
+.reactionsItem {
+	display: inline-block;
+	padding: 8px;
+	cursor: move;
+}
 
-	> .add {
-		display: inline-block;
-		padding: 8px;
-	}
+.reactionsAdd {
+	display: inline-block;
+	padding: 8px;
 }
 </style>
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index 78e0710162..5abb234893 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -16,7 +16,7 @@
 			class="_panel"
 			@posted="state = 'posted'"
 		/>
-		<MkButton v-else-if="state === 'posted'" primary class="close" @click="close()">{{ i18n.ts.close }}</MkButton>
+		<MkButton v-else-if="state === 'posted'" primary :class="$style.close" @click="close()">{{ i18n.ts.close }}</MkButton>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
@@ -162,7 +162,7 @@ definePageMetadata({
 });
 </script>
 
-<style lang="scss" scoped>
+<style lang="scss" module>
 .close {
 	margin: 16px auto;
 }
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index 5459532310..079cbb3d33 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -35,7 +35,3 @@ definePageMetadata({
 	icon: 'ti ti-user',
 });
 </script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 9c133346d5..a39f206863 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -69,7 +69,7 @@
 					</div>
 					<div class="description">
 						<MkOmit>
-							<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i"/>
+							<Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user" :i="$i"/>
 							<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
 						</MkOmit>
 					</div>
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 7728d97a65..5a68292b1b 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -1,27 +1,32 @@
 <template>
-<form :class="$style.root" class="_panel" @submit.prevent="submit()">
-	<div :class="$style.title">
-		<div>Welcome to Misskey!</div>
-		<div :class="$style.version">v{{ version }}</div>
+<div :class="$style.root">
+	<MkAnimBg style="position: fixed; top: 0;"/>
+	<div :class="$style.formContainer">
+		<form :class="$style.form" class="_panel" @submit.prevent="submit()">
+			<div :class="$style.title">
+				<div>Welcome to Misskey!</div>
+				<div :class="$style.version">v{{ version }}</div>
+			</div>
+			<div class="_gaps_m" style="padding: 32px;">
+				<div>{{ i18n.ts.intro }}</div>
+				<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username>
+					<template #label>{{ i18n.ts.username }}</template>
+					<template #prefix>@</template>
+					<template #suffix>@{{ host }}</template>
+				</MkInput>
+				<MkInput v-model="password" type="password" data-cy-admin-password>
+					<template #label>{{ i18n.ts.password }}</template>
+					<template #prefix><i class="ti ti-lock"></i></template>
+				</MkInput>
+				<div>
+					<MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;">
+						{{ submitting ? i18n.ts.processing : i18n.ts.done }}<MkEllipsis v-if="submitting"/>
+					</MkButton>
+				</div>
+			</div>
+		</form>
 	</div>
-	<div class="_gaps_m" style="padding: 32px;">
-		<div>{{ i18n.ts.intro }}</div>
-		<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username>
-			<template #label>{{ i18n.ts.username }}</template>
-			<template #prefix>@</template>
-			<template #suffix>@{{ host }}</template>
-		</MkInput>
-		<MkInput v-model="password" type="password" data-cy-admin-password>
-			<template #label>{{ i18n.ts.password }}</template>
-			<template #prefix><i class="ti ti-lock"></i></template>
-		</MkInput>
-		<div>
-			<MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;">
-				{{ submitting ? i18n.ts.processing : i18n.ts.done }}<MkEllipsis v-if="submitting"/>
-			</MkButton>
-		</div>
-	</div>
-</form>
+</div>
 </template>
 
 <script lang="ts" setup>
@@ -32,6 +37,7 @@ import { host, version } from '@/config';
 import * as os from '@/os';
 import { login } from '@/account';
 import { i18n } from '@/i18n';
+import MkAnimBg from '@/components/MkAnimBg.vue';
 
 let username = $ref('');
 let password = $ref('');
@@ -59,11 +65,23 @@ function submit() {
 
 <style lang="scss" module>
 .root {
+}
+
+.formContainer {
+	min-height: 100svh;
+	padding: 32px 32px 64px 32px;
+	box-sizing: border-box;
+display: grid;
+place-content: center;
+}
+
+.form {
+	position: relative;
+	z-index: 10;
 	border-radius: var(--radius);
 	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
-	overflow: hidden;
+	overflow: clip;
 	max-width: 500px;
-	margin: 32px auto;
 }
 
 .title {
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index 2616a8a1d5..d97bd4be62 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -6,7 +6,7 @@ import { $i } from './account';
 import { api } from './os';
 import { get, set } from './scripts/idb-proxy';
 import { defaultStore } from './store';
-import { stream } from './stream';
+import { useStream } from './stream';
 import { deepClone } from './scripts/clone';
 
 type StateDef = Record<string, {
@@ -26,8 +26,6 @@ type PizzaxChannelMessage<T extends StateDef> = {
 	userId?: string;
 };
 
-const connection = $i && stream.useChannel('main');
-
 export class Storage<T extends StateDef> {
 	public readonly ready: Promise<void>;
 	public readonly loaded: Promise<void>;
@@ -105,8 +103,10 @@ export class Storage<T extends StateDef> {
 		});
 
 		if ($i) {
+			const connection = useStream().useChannel('main');
+
 			// streamingのuser storage updateイベントを監視して更新
-			connection?.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
+			connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
 				if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
 
 				this.reactiveState[key].value = this.state[key] = value;
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
index e46c1eeb77..add4bd9217 100644
--- a/packages/frontend/src/router.ts
+++ b/packages/frontend/src/router.ts
@@ -242,9 +242,6 @@ export const routes = [{
 }, {
 	path: '/scratchpad',
 	component: page(() => import('./pages/scratchpad.vue')),
-}, {
-	path: '/preview',
-	component: page(() => import('./pages/preview.vue')),
 }, {
 	path: '/auth/:token',
 	component: page(() => import('./pages/auth.vue')),
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index c8a6100253..960f26ca67 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -7,7 +7,7 @@ import { instance } from '@/instance';
 import * as os from '@/os';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { url } from '@/config';
-import { noteActions } from '@/store';
+import { defaultStore, noteActions } from '@/store';
 import { miLocalStorage } from '@/local-storage';
 import { getUserMenu } from '@/scripts/get-user-menu';
 import { clipsCache } from '@/cache';
@@ -396,5 +396,15 @@ export function getNoteMenu(props: {
 		}))]);
 	}
 
+	if (defaultStore.state.devMode) {
+		menu = menu.concat([null, {
+			icon: 'ti ti-id',
+			text: i18n.ts.copyNoteId,
+			action: () => {
+				copyToClipboard(appearNote.id);
+			},
+		}]);
+	}
+
 	return menu;
 }
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 6ff9fb63f1..b055d26473 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -4,7 +4,7 @@ import { i18n } from '@/i18n';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { host } from '@/config';
 import * as os from '@/os';
-import { userActions } from '@/store';
+import { defaultStore, userActions } from '@/store';
 import { $i, iAmModerator } from '@/account';
 import { mainRouter } from '@/router';
 import { Router } from '@/nirax';
@@ -240,6 +240,16 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
 		}]);
 	}
 
+	if (defaultStore.state.devMode) {
+		menu = menu.concat([null, {
+			icon: 'ti ti-id',
+			text: i18n.ts.copyUserId,
+			action: () => {
+				copyToClipboard(user.id);
+			},
+		}]);
+	}
+
 	if ($i && meId === user.id) {
 		menu = menu.concat([null, {
 			icon: 'ti ti-pencil',
diff --git a/packages/frontend/src/scripts/hpml/block.ts b/packages/frontend/src/scripts/hpml/block.ts
deleted file mode 100644
index 804c5c1124..0000000000
--- a/packages/frontend/src/scripts/hpml/block.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-// blocks
-
-export type BlockBase = {
-	id: string;
-	type: string;
-};
-
-export type TextBlock = BlockBase & {
-	type: 'text';
-	text: string;
-};
-
-export type SectionBlock = BlockBase & {
-	type: 'section';
-	title: string;
-	children: (Block | VarBlock)[];
-};
-
-export type ImageBlock = BlockBase & {
-	type: 'image';
-	fileId: string | null;
-};
-
-export type ButtonBlock = BlockBase & {
-	type: 'button';
-	text: any;
-	primary: boolean;
-	action: string;
-	content: string;
-	event: string;
-	message: string;
-	var: string;
-	fn: string;
-};
-
-export type IfBlock = BlockBase & {
-	type: 'if';
-	var: string;
-	children: Block[];
-};
-
-export type TextareaBlock = BlockBase & {
-	type: 'textarea';
-	text: string;
-};
-
-export type PostBlock = BlockBase & {
-	type: 'post';
-	text: string;
-	attachCanvasImage: boolean;
-	canvasId: string;
-};
-
-export type CanvasBlock = BlockBase & {
-	type: 'canvas';
-	name: string; // canvas id
-	width: number;
-	height: number;
-};
-
-export type NoteBlock = BlockBase & {
-	type: 'note';
-	detailed: boolean;
-	note: string | null;
-};
-
-export type Block =
-	TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock;
-
-// variable blocks
-
-export type VarBlockBase = BlockBase & {
-	name: string;
-};
-
-export type NumberInputVarBlock = VarBlockBase & {
-	type: 'numberInput';
-	text: string;
-};
-
-export type TextInputVarBlock = VarBlockBase & {
-	type: 'textInput';
-	text: string;
-};
-
-export type SwitchVarBlock = VarBlockBase & {
-	type: 'switch';
-	text: string;
-};
-
-export type RadioButtonVarBlock = VarBlockBase & {
-	type: 'radioButton';
-	title: string;
-	values: string[];
-};
-
-export type CounterVarBlock = VarBlockBase & {
-	type: 'counter';
-	text: string;
-	inc: number;
-};
-
-export type VarBlock =
-	NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock;
-
-const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter'];
-export function isVarBlock(block: Block): block is VarBlock {
-	return varBlock.includes(block.type);
-}
diff --git a/packages/frontend/src/scripts/hpml/evaluator.ts b/packages/frontend/src/scripts/hpml/evaluator.ts
deleted file mode 100644
index 9adfba7f27..0000000000
--- a/packages/frontend/src/scripts/hpml/evaluator.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { ref, Ref, unref } from 'vue';
-import { collectPageVars } from '../collect-page-vars';
-import { initHpmlLib } from './lib';
-import { Expr, isLiteralValue, Variable } from './expr';
-import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.';
-import { version } from '@/config';
-
-/**
- * Hpml evaluator
- */
-export class Hpml {
-	private variables: Variable[];
-	private pageVars: PageVar[];
-	private envVars: Record<keyof typeof envVarsDef, any>;
-	public pageVarUpdatedCallback?: values.VFn;
-	public canvases: Record<string, HTMLCanvasElement> = {};
-	public vars: Ref<Record<string, any>> = ref({});
-	public page: Record<string, any>;
-
-	private opts: {
-		randomSeed: string; visitor?: any; url?: string;
-	};
-
-	constructor(page: Hpml['page'], opts: Hpml['opts']) {
-		this.page = page;
-		this.variables = this.page.variables;
-		this.pageVars = collectPageVars(this.page.content);
-		this.opts = opts;
-
-		const date = new Date();
-
-		this.envVars = {
-			AI: 'kawaii',
-			VERSION: version,
-			URL: this.page ? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}` : '',
-			LOGIN: opts.visitor != null,
-			NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '',
-			USERNAME: opts.visitor ? opts.visitor.username : '',
-			USERID: opts.visitor ? opts.visitor.id : '',
-			NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0,
-			FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0,
-			FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0,
-			IS_CAT: opts.visitor ? opts.visitor.isCat : false,
-			SEED: opts.randomSeed ? opts.randomSeed : '',
-			YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`,
-			AISCRIPT_DISABLED: true,
-			NULL: null,
-		};
-
-		this.eval();
-	}
-
-	public eval() {
-		try {
-			this.vars.value = this.evaluateVars();
-		} catch (err) {
-			//this.onError(e);
-		}
-	}
-
-	public interpolate(str: string) {
-		if (str == null) return null;
-		return str.replace(/{(.+?)}/g, match => {
-			const v = unref(this.vars)[match.slice(1, -1).trim()];
-			return v == null ? 'NULL' : v.toString();
-		});
-	}
-
-	public registerCanvas(id: string, canvas: any) {
-		this.canvases[id] = canvas;
-	}
-
-	public updatePageVar(name: string, value: any) {
-		const pageVar = this.pageVars.find(v => v.name === name);
-		if (pageVar !== undefined) {
-			pageVar.value = value;
-		} else {
-			throw new HpmlError(`No such page var '${name}'`);
-		}
-	}
-
-	public updateRandomSeed(seed: string) {
-		this.opts.randomSeed = seed;
-		this.envVars.SEED = seed;
-	}
-
-	private _interpolateScope(str: string, scope: HpmlScope) {
-		return str.replace(/{(.+?)}/g, match => {
-			const v = scope.getState(match.slice(1, -1).trim());
-			return v == null ? 'NULL' : v.toString();
-		});
-	}
-
-	public evaluateVars(): Record<string, any> {
-		const values: Record<string, any> = {};
-
-		for (const [k, v] of Object.entries(this.envVars)) {
-			values[k] = v;
-		}
-
-		for (const v of this.pageVars) {
-			values[v.name] = v.value;
-		}
-
-		for (const v of this.variables) {
-			values[v.name] = this.evaluate(v, new HpmlScope([values]));
-		}
-
-		return values;
-	}
-
-	private evaluate(expr: Expr, scope: HpmlScope): any {
-		if (isLiteralValue(expr)) {
-			if (expr.type === null) {
-				return null;
-			}
-
-			if (expr.type === 'number') {
-				return parseInt((expr.value as any), 10);
-			}
-
-			if (expr.type === 'text' || expr.type === 'multiLineText') {
-				return this._interpolateScope(expr.value || '', scope);
-			}
-
-			if (expr.type === 'textList') {
-				return this._interpolateScope(expr.value || '', scope).trim().split('\n');
-			}
-
-			if (expr.type === 'ref') {
-				return scope.getState(expr.value);
-			}
-
-			// Define user function
-			if (expr.type === 'fn') {
-				return {
-					slots: expr.value.slots.map(x => x.name),
-					exec: (slotArg: Record<string, any>) => {
-						return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id));
-					},
-				} as Fn;
-			}
-			return;
-		}
-
-		// Call user function
-		if (expr.type.startsWith('fn:')) {
-			const fnName = expr.type.split(':')[1];
-			const fn = scope.getState(fnName);
-			const args = {} as Record<string, any>;
-			for (let i = 0; i < fn.slots.length; i++) {
-				const name = fn.slots[i];
-				args[name] = this.evaluate(expr.args[i], scope);
-			}
-			return fn.exec(args);
-		}
-
-		if (expr.args === undefined) return null;
-
-		const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor);
-
-		// Call function
-		const fnName = expr.type;
-		const fn = (funcs as any)[fnName];
-		if (fn == null) {
-			throw new HpmlError(`No such function '${fnName}'`);
-		} else {
-			return fn(...expr.args.map(x => this.evaluate(x, scope)));
-		}
-	}
-}
diff --git a/packages/frontend/src/scripts/hpml/expr.ts b/packages/frontend/src/scripts/hpml/expr.ts
deleted file mode 100644
index 18c7c2a14b..0000000000
--- a/packages/frontend/src/scripts/hpml/expr.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { literalDefs, Type } from '.';
-
-export type ExprBase = {
-	id: string;
-};
-
-// value
-
-export type EmptyValue = ExprBase & {
-	type: null;
-	value: null;
-};
-
-export type TextValue = ExprBase & {
-	type: 'text';
-	value: string;
-};
-
-export type MultiLineTextValue = ExprBase & {
-	type: 'multiLineText';
-	value: string;
-};
-
-export type TextListValue = ExprBase & {
-	type: 'textList';
-	value: string;
-};
-
-export type NumberValue = ExprBase & {
-	type: 'number';
-	value: number;
-};
-
-export type RefValue = ExprBase & {
-	type: 'ref';
-	value: string; // value is variable name
-};
-
-export type AiScriptRefValue = ExprBase & {
-	type: 'aiScriptVar';
-	value: string; // value is variable name
-};
-
-export type UserFnValue = ExprBase & {
-	type: 'fn';
-	value: UserFnInnerValue;
-};
-type UserFnInnerValue = {
-	slots: {
-		name: string;
-		type: Type;
-	}[];
-	expression: Expr;
-};
-
-export type Value =
-	EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue;
-
-export function isLiteralValue(expr: Expr): expr is Value {
-	if (expr.type == null) return true;
-	if (literalDefs[expr.type]) return true;
-	return false;
-}
-
-// call function
-
-export type CallFn = ExprBase & { // "fn:hoge" or string
-	type: string;
-	args: Expr[];
-	value: null;
-};
-
-// variable
-export type Variable = (Value | CallFn) & {
-	name: string;
-};
-
-// expression
-export type Expr = Variable | Value | CallFn;
diff --git a/packages/frontend/src/scripts/hpml/index.ts b/packages/frontend/src/scripts/hpml/index.ts
deleted file mode 100644
index 994f286b9f..0000000000
--- a/packages/frontend/src/scripts/hpml/index.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * Hpml
- */
-
-import { Hpml } from './evaluator';
-import { funcDefs } from './lib';
-
-export type Fn = {
-	slots: string[];
-	exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>;
-};
-
-export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
-
-export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = {
-	text: { out: 'string', category: 'value', icon: 'ti ti-quote' },
-	multiLineText: { out: 'string', category: 'value', icon: 'ti ti-align-left' },
-	textList: { out: 'stringArray', category: 'value', icon: 'ti ti-list' },
-	number: { out: 'number', category: 'value', icon: 'ti ti-list-numbers' },
-	ref: { out: null, category: 'value', icon: 'ti ti-wand' },
-	aiScriptVar: { out: null, category: 'value', icon: 'ti ti-wand' },
-	fn: { out: 'function', category: 'value', icon: 'ti ti-math-function' },
-};
-
-export const blockDefs = [
-	...Object.entries(literalDefs).map(([k, v]) => ({
-		type: k, out: v.out, category: v.category, icon: v.icon,
-	})),
-	...Object.entries(funcDefs).map(([k, v]) => ({
-		type: k, out: v.out, category: v.category, icon: v.icon,
-	})),
-];
-
-export type PageVar = { name: string; value: any; type: Type; };
-
-export const envVarsDef: Record<string, Type> = {
-	AI: 'string',
-	URL: 'string',
-	VERSION: 'string',
-	LOGIN: 'boolean',
-	NAME: 'string',
-	USERNAME: 'string',
-	USERID: 'string',
-	NOTES_COUNT: 'number',
-	FOLLOWERS_COUNT: 'number',
-	FOLLOWING_COUNT: 'number',
-	IS_CAT: 'boolean',
-	SEED: null,
-	YMD: 'string',
-	AISCRIPT_DISABLED: 'boolean',
-	NULL: null,
-};
-
-export class HpmlScope {
-	private layerdStates: Record<string, any>[];
-	public name: string;
-
-	constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) {
-		this.layerdStates = layerdStates;
-		this.name = name ?? 'anonymous';
-	}
-
-	public createChildScope(states: Record<string, any>, name?: HpmlScope['name']): HpmlScope {
-		const layer = [states, ...this.layerdStates];
-		return new HpmlScope(layer, name);
-	}
-
-	/**
-	 * 指定した名前の変数の値を取得します
-	 * @param name 変数名
-	 */
-	public getState(name: string): any {
-		for (const later of this.layerdStates) {
-			const state = later[name];
-			if (state !== undefined) {
-				return state;
-			}
-		}
-
-		throw new HpmlError(
-			`No such variable '${name}' in scope '${this.name}'`, {
-				scope: this.layerdStates,
-			});
-	}
-}
-
-export class HpmlError extends Error {
-	public info?: any;
-
-	constructor(message: string, info?: any) {
-		super(message);
-
-		this.info = info;
-
-		// Maintains proper stack trace for where our error was thrown (only available on V8)
-		if (Error.captureStackTrace) {
-			Error.captureStackTrace(this, HpmlError);
-		}
-	}
-}
diff --git a/packages/frontend/src/scripts/hpml/lib.ts b/packages/frontend/src/scripts/hpml/lib.ts
deleted file mode 100644
index 88db82dd27..0000000000
--- a/packages/frontend/src/scripts/hpml/lib.ts
+++ /dev/null
@@ -1,245 +0,0 @@
-import seedrandom from 'seedrandom';
-import { Hpml } from './evaluator';
-import { Expr } from './expr';
-import { Fn, HpmlScope } from '.';
-
-/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color
-// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
-Chart.pluginService.register({
-	beforeDraw: (chart, easing) => {
-		if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) {
-			const ctx = chart.chart.ctx;
-			ctx.save();
-			ctx.fillStyle = chart.config.options.chartArea.backgroundColor;
-			ctx.fillRect(0, 0, chart.chart.width, chart.chart.height);
-			ctx.restore();
-		}
-	}
-});
-*/
-
-export function initAiLib(hpml: Hpml) {
-	return {
-		'MkPages:updated': values.FN_NATIVE(([callback]) => {
-			hpml.pageVarUpdatedCallback = (callback as values.VFn);
-		}),
-		'MkPages:get_canvas': values.FN_NATIVE(([id]) => {
-			utils.assertString(id);
-			const canvas = hpml.canvases[id.value];
-			const ctx = canvas.getContext('2d');
-			return values.OBJ(new Map([
-				['clear_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.clearRect(x.value, y.value, width.value, height.value); })],
-				['fill_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.fillRect(x.value, y.value, width.value, height.value); })],
-				['stroke_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.strokeRect(x.value, y.value, width.value, height.value); })],
-				['fill_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.fillText(text.value, x.value, y.value, width ? width.value : undefined); })],
-				['stroke_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.strokeText(text.value, x.value, y.value, width ? width.value : undefined); })],
-				['set_line_width', values.FN_NATIVE(([width]) => { ctx.lineWidth = width.value; })],
-				['set_font', values.FN_NATIVE(([font]) => { ctx.font = font.value; })],
-				['set_fill_style', values.FN_NATIVE(([style]) => { ctx.fillStyle = style.value; })],
-				['set_stroke_style', values.FN_NATIVE(([style]) => { ctx.strokeStyle = style.value; })],
-				['begin_path', values.FN_NATIVE(() => { ctx.beginPath(); })],
-				['close_path', values.FN_NATIVE(() => { ctx.closePath(); })],
-				['move_to', values.FN_NATIVE(([x, y]) => { ctx.moveTo(x.value, y.value); })],
-				['line_to', values.FN_NATIVE(([x, y]) => { ctx.lineTo(x.value, y.value); })],
-				['arc', values.FN_NATIVE(([x, y, radius, startAngle, endAngle]) => { ctx.arc(x.value, y.value, radius.value, startAngle.value, endAngle.value); })],
-				['rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.rect(x.value, y.value, width.value, height.value); })],
-				['fill', values.FN_NATIVE(() => { ctx.fill(); })],
-				['stroke', values.FN_NATIVE(() => { ctx.stroke(); })],
-			]));
-		}),
-		'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
-			/* TODO
-			utils.assertString(id);
-			utils.assertObject(opts);
-			const canvas = hpml.canvases[id.value];
-			const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
-			Chart.defaults.color = '#555';
-			const chart = new Chart(canvas, {
-				type: opts.value.get('type').value,
-				data: {
-					labels: opts.value.get('labels').value.map(x => x.value),
-					datasets: opts.value.get('datasets').value.map(x => ({
-						label: x.value.has('label') ? x.value.get('label').value : '',
-						data: x.value.get('data').value.map(x => x.value),
-						pointRadius: 0,
-						lineTension: 0,
-						borderWidth: 2,
-						borderColor: x.value.has('color') ? x.value.get('color') : color,
-						backgroundColor: tinycolor(x.value.has('color') ? x.value.get('color') : color).setAlpha(0.1).toRgbString(),
-					}))
-				},
-				options: {
-					responsive: false,
-					devicePixelRatio: 1.5,
-					title: {
-						display: opts.value.has('title'),
-						text: opts.value.has('title') ? opts.value.get('title').value : '',
-						fontSize: 14,
-					},
-					layout: {
-						padding: {
-							left: 32,
-							right: 32,
-							top: opts.value.has('title') ? 16 : 32,
-							bottom: 16
-						}
-					},
-					legend: {
-						display: opts.value.get('datasets').value.filter(x => x.value.has('label') && x.value.get('label').value).length === 0 ? false : true,
-						position: 'bottom',
-						labels: {
-							boxWidth: 16,
-						}
-					},
-					tooltips: {
-						enabled: false,
-					},
-					chartArea: {
-						backgroundColor: '#fff'
-					},
-					...(opts.value.get('type').value === 'radar' ? {
-						scale: {
-							ticks: {
-								display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : false,
-								min: opts.value.has('min') ? opts.value.get('min').value : undefined,
-								max: opts.value.has('max') ? opts.value.get('max').value : undefined,
-								maxTicksLimit: 8,
-							},
-							pointLabels: {
-								fontSize: 12
-							}
-						}
-					} : {
-						scales: {
-							yAxes: [{
-								ticks: {
-									display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : true,
-									min: opts.value.has('min') ? opts.value.get('min').value : undefined,
-									max: opts.value.has('max') ? opts.value.get('max').value : undefined,
-								}
-							}]
-						}
-					})
-				}
-			});
-			*/
-		}),
-	};
-}
-
-export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
-	if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'ti ti-share' },
-	for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'ti ti-recycle' },
-	not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' },
-	or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' },
-	and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' },
-	add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-plus' },
-	subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-minus' },
-	multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-x' },
-	divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-divide' },
-	mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-divide' },
-	round: { in: ['number'], out: 'number', category: 'operation', icon: 'ti ti-calculator' },
-	eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'ti ti-equal' },
-	notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'ti ti-equal-not' },
-	gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-greater' },
-	lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-lower' },
-	gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-equal-greater' },
-	ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-equal-lower' },
-	strLen: { in: ['string'], out: 'number', category: 'text', icon: 'ti ti-quote' },
-	strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'ti ti-quote' },
-	strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' },
-	strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'ti ti-quote' },
-	join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' },
-	stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'ti ti-arrows-right-left' },
-	numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'ti ti-arrows-right-left' },
-	splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'ti ti-arrows-right-left' },
-	pick: { in: [null, 'number'], out: null, category: 'list', icon: 'ti ti-indent-increase' },
-	listLen: { in: [null], out: 'number', category: 'list', icon: 'ti ti-indent-increase' },
-	rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' },
-	dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' },
-	seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' },
-	random: { in: ['number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' },
-	dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' },
-	seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' },
-	randomPick: { in: [0], out: 0, category: 'random', icon: 'ti ti-dice' },
-	dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'ti ti-dice' },
-	seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'ti ti-dice' },
-	DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'ti ti-dice' }, // dailyRandomPickWithProbabilityMapping
-};
-
-export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) {
-	const date = new Date();
-	const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
-
-	// SHOULD be fine to ignore since it's intended + function shape isn't defined
-	// eslint-disable-next-line @typescript-eslint/ban-types
-	const funcs: Record<string, Function> = {
-		not: (a: boolean) => !a,
-		or: (a: boolean, b: boolean) => a || b,
-		and: (a: boolean, b: boolean) => a && b,
-		eq: (a: any, b: any) => a === b,
-		notEq: (a: any, b: any) => a !== b,
-		gt: (a: number, b: number) => a > b,
-		lt: (a: number, b: number) => a < b,
-		gtEq: (a: number, b: number) => a >= b,
-		ltEq: (a: number, b: number) => a <= b,
-		if: (bool: boolean, a: any, b: any) => bool ? a : b,
-		for: (times: number, fn: Fn) => {
-			const result: any[] = [];
-			for (let i = 0; i < times; i++) {
-				result.push(fn.exec({
-					[fn.slots[0]]: i + 1,
-				}));
-			}
-			return result;
-		},
-		add: (a: number, b: number) => a + b,
-		subtract: (a: number, b: number) => a - b,
-		multiply: (a: number, b: number) => a * b,
-		divide: (a: number, b: number) => a / b,
-		mod: (a: number, b: number) => a % b,
-		round: (a: number) => Math.round(a),
-		strLen: (a: string) => a.length,
-		strPick: (a: string, b: number) => a[b - 1],
-		strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
-		strReverse: (a: string) => a.split('').reverse().join(''),
-		join: (texts: string[], separator: string) => texts.join(separator || ''),
-		stringToNumber: (a: string) => parseInt(a),
-		numberToString: (a: number) => a.toString(),
-		splitStrByLine: (a: string) => a.split('\n'),
-		pick: (list: any[], i: number) => list[i - 1],
-		listLen: (list: any[]) => list.length,
-		random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability,
-		rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)),
-		randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)],
-		dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability,
-		dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)),
-		dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)],
-		seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability,
-		seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)),
-		seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)],
-		DRPWPM: (list: string[]) => {
-			const xs: any[] = [];
-			let totalFactor = 0;
-			for (const x of list) {
-				const parts = x.split(' ');
-				const factor = parseInt(parts.pop()!, 10);
-				const text = parts.join(' ');
-				totalFactor += factor;
-				xs.push({ factor, text });
-			}
-			const r = seedrandom(`${day}:${expr.id}`)() * totalFactor;
-			let stackedFactor = 0;
-			for (const x of xs) {
-				if (r >= stackedFactor && r <= stackedFactor + x.factor) {
-					return x.text;
-				} else {
-					stackedFactor += x.factor;
-				}
-			}
-			return xs[0].text;
-		},
-	};
-
-	return funcs;
-}
diff --git a/packages/frontend/src/scripts/hpml/type-checker.ts b/packages/frontend/src/scripts/hpml/type-checker.ts
deleted file mode 100644
index ea8133f297..0000000000
--- a/packages/frontend/src/scripts/hpml/type-checker.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { isLiteralValue } from './expr';
-import { funcDefs } from './lib';
-import { envVarsDef } from '.';
-import type { Type, PageVar } from '.';
-import type { Expr, Variable } from './expr';
-
-type TypeError = {
-	arg: number;
-	expect: Type;
-	actual: Type;
-};
-
-/**
- * Hpml type checker
- */
-export class HpmlTypeChecker {
-	public variables: Variable[];
-	public pageVars: PageVar[];
-
-	constructor(variables: HpmlTypeChecker['variables'] = [], pageVars: HpmlTypeChecker['pageVars'] = []) {
-		this.variables = variables;
-		this.pageVars = pageVars;
-	}
-
-	public typeCheck(v: Expr): TypeError | null {
-		if (isLiteralValue(v)) return null;
-
-		const def = funcDefs[v.type || ''];
-		if (def == null) {
-			throw new Error('Unknown type: ' + v.type);
-		}
-
-		const generic: Type[] = [];
-
-		for (let i = 0; i < def.in.length; i++) {
-			const arg = def.in[i];
-			const type = this.infer(v.args[i]);
-			if (type === null) continue;
-
-			if (typeof arg === 'number') {
-				if (generic[arg] === undefined) {
-					generic[arg] = type;
-				} else if (type !== generic[arg]) {
-					return {
-						arg: i,
-						expect: generic[arg],
-						actual: type,
-					};
-				}
-			} else if (type !== arg) {
-				return {
-					arg: i,
-					expect: arg,
-					actual: type,
-				};
-			}
-		}
-
-		return null;
-	}
-
-	public getExpectedType(v: Expr, slot: number): Type {
-		const def = funcDefs[v.type ?? ''];
-		if (def == null) {
-			throw new Error('Unknown type: ' + v.type);
-		}
-
-		const generic: Type[] = [];
-
-		for (let i = 0; i < def.in.length; i++) {
-			const arg = def.in[i];
-			const type = this.infer(v.args[i]);
-			if (type === null) continue;
-
-			if (typeof arg === 'number') {
-				if (generic[arg] === undefined) {
-					generic[arg] = type;
-				}
-			}
-		}
-
-		if (typeof def.in[slot] === 'number') {
-			return generic[def.in[slot]] ?? null;
-		} else {
-			return def.in[slot];
-		}
-	}
-
-	public infer(v: Expr): Type {
-		if (v.type === null) return null;
-		if (v.type === 'text') return 'string';
-		if (v.type === 'multiLineText') return 'string';
-		if (v.type === 'textList') return 'stringArray';
-		if (v.type === 'number') return 'number';
-		if (v.type === 'ref') {
-			const variable = this.variables.find(va => va.name === v.value);
-			if (variable) {
-				return this.infer(variable);
-			}
-
-			const pageVar = this.pageVars.find(va => va.name === v.value);
-			if (pageVar) {
-				return pageVar.type;
-			}
-
-			const envVar = envVarsDef[v.value ?? ''];
-			if (envVar !== undefined) {
-				return envVar;
-			}
-
-			return null;
-		}
-		if (v.type === 'aiScriptVar') return null;
-		if (v.type === 'fn') return null; // todo
-		if (v.type.startsWith('fn:')) return null; // todo
-
-		const generic: Type[] = [];
-
-		const def = funcDefs[v.type];
-
-		for (let i = 0; i < def.in.length; i++) {
-			const arg = def.in[i];
-			if (typeof arg === 'number') {
-				const type = this.infer(v.args[i]);
-
-				if (generic[arg] === undefined) {
-					generic[arg] = type;
-				} else {
-					if (type !== generic[arg]) {
-						generic[arg] = null;
-					}
-				}
-			}
-		}
-
-		if (typeof def.out === 'number') {
-			return generic[def.out];
-		} else {
-			return def.out;
-		}
-	}
-
-	public getVarByName(name: string): Variable {
-		const v = this.variables.find(x => x.name === name);
-		if (v !== undefined) {
-			return v;
-		} else {
-			throw new Error(`No such variable '${name}'`);
-		}
-	}
-
-	public getVarsByType(type: Type): Variable[] {
-		if (type == null) return this.variables;
-		return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type));
-	}
-
-	public getEnvVarsByType(type: Type): string[] {
-		if (type == null) return Object.keys(envVarsDef);
-		return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k);
-	}
-
-	public getPageVarsByType(type: Type): string[] {
-		if (type == null) return this.pageVars.map(v => v.name);
-		return this.pageVars.filter(v => type === v.type).map(v => v.name);
-	}
-
-	public isUsedName(name: string) {
-		if (this.variables.some(v => v.name === name)) {
-			return true;
-		}
-
-		if (this.pageVars.some(v => v.name === name)) {
-			return true;
-		}
-
-		if (envVarsDef[name]) {
-			return true;
-		}
-
-		return false;
-	}
-}
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index fe9f0a2447..44a58d6c7d 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -1,7 +1,7 @@
 import { ref } from 'vue';
 import { DriveFile } from 'misskey-js/built/entities';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import { defaultStore } from '@/store';
 import { uploadFile } from '@/scripts/upload';
@@ -51,7 +51,7 @@ export function chooseFileFromUrl(): Promise<DriveFile> {
 
 			const marker = Math.random().toString(); // TODO: UUIDとか使う
 
-			const connection = stream.useChannel('main');
+			const connection = useStream().useChannel('main');
 			connection.on('urlUploadFinished', urlResponse => {
 				if (urlResponse.marker === marker) {
 					res(urlResponse.file);
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index ffe33cccc0..22a01e066a 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -1,6 +1,6 @@
 import { onUnmounted, Ref } from 'vue';
 import * as misskey from 'misskey-js';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { $i } from '@/account';
 
 export function useNoteCapture(props: {
@@ -9,7 +9,7 @@ export function useNoteCapture(props: {
 	isDeletedRef: Ref<boolean>;
 }) {
 	const note = props.note;
-	const connection = $i ? stream : null;
+	const connection = $i ? useStream() : null;
 
 	function onStreamNoteUpdated(noteData): void {
 		const { type, id, body } = noteData;
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 245bcbefe1..0af4b5c021 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -102,6 +102,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'account',
 		default: [] as string[],
 	},
+	showTimelineReplies: {
+		where: 'account',
+		default: false,
+	},
 
 	menu: {
 		where: 'deviceAccount',
@@ -314,6 +318,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
+	devMode: {
+		where: 'device',
+		default: false,
+	},
 	mediaListWithOneImageAppearance: {
 		where: 'device',
 		default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index dea3459b86..9cae58a26a 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -3,6 +3,14 @@ import { markRaw } from 'vue';
 import { $i } from '@/account';
 import { url } from '@/config';
 
-export const stream = markRaw(new Misskey.Stream(url, $i ? {
-	token: $i.token,
-} : null));
+let stream: Misskey.Stream | null = null;
+
+export function useStream(): Misskey.Stream {
+	if (stream) return stream;
+
+	stream = markRaw(new Misskey.Stream(url, $i ? {
+		token: $i.token,
+	} : null));
+
+	return stream;
+}
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 20254d335e..689cb6ed82 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -483,3 +483,140 @@ hr {
     transform:  scaleX(1.00) scaleY(1.00) ;
   }
 }
+
+// MFM -----------------------------
+
+._mfm_blur_ {
+	filter: blur(6px);
+	transition: filter 0.3s;
+
+	&:hover {
+		filter: blur(0px);
+	}
+}
+
+.mfm-x2 {
+	--mfm-zoom-size: 200%;
+}
+
+.mfm-x3 {
+	--mfm-zoom-size: 400%;
+}
+
+.mfm-x4 {
+	--mfm-zoom-size: 600%;
+}
+
+.mfm-x2, .mfm-x3, .mfm-x4 {
+	font-size: var(--mfm-zoom-size);
+
+	.mfm-x2, .mfm-x3, .mfm-x4 {
+		/* only half effective */
+		font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
+
+		.mfm-x2, .mfm-x3, .mfm-x4 {
+			/* disabled */
+			font-size: 100%;
+		}
+	}
+}
+
+@keyframes mfm-spin {
+	0% { transform: rotate(0deg); }
+	100% { transform: rotate(360deg); }
+}
+
+@keyframes mfm-spinX {
+	0% { transform: perspective(128px) rotateX(0deg); }
+	100% { transform: perspective(128px) rotateX(360deg); }
+}
+
+@keyframes mfm-spinY {
+	0% { transform: perspective(128px) rotateY(0deg); }
+	100% { transform: perspective(128px) rotateY(360deg); }
+}
+
+@keyframes mfm-jump {
+	0% { transform: translateY(0); }
+	25% { transform: translateY(-16px); }
+	50% { transform: translateY(0); }
+	75% { transform: translateY(-8px); }
+	100% { transform: translateY(0); }
+}
+
+@keyframes mfm-bounce {
+	0% { transform: translateY(0) scale(1, 1); }
+	25% { transform: translateY(-16px) scale(1, 1); }
+	50% { transform: translateY(0) scale(1, 1); }
+	75% { transform: translateY(0) scale(1.5, 0.75); }
+	100% { transform: translateY(0) scale(1, 1); }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-twitch {
+	0% { transform: translate(7px, -2px) }
+	5% { transform: translate(-3px, 1px) }
+	10% { transform: translate(-7px, -1px) }
+	15% { transform: translate(0px, -1px) }
+	20% { transform: translate(-8px, 6px) }
+	25% { transform: translate(-4px, -3px) }
+	30% { transform: translate(-4px, -6px) }
+	35% { transform: translate(-8px, -8px) }
+	40% { transform: translate(4px, 6px) }
+	45% { transform: translate(-3px, 1px) }
+	50% { transform: translate(2px, -10px) }
+	55% { transform: translate(-7px, 0px) }
+	60% { transform: translate(-2px, 4px) }
+	65% { transform: translate(3px, -8px) }
+	70% { transform: translate(6px, 7px) }
+	75% { transform: translate(-7px, -2px) }
+	80% { transform: translate(-7px, -8px) }
+	85% { transform: translate(9px, 3px) }
+	90% { transform: translate(-3px, -2px) }
+	95% { transform: translate(-10px, 2px) }
+	100% { transform: translate(-2px, -6px) }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-shake {
+	0% { transform: translate(-3px, -1px) rotate(-8deg) }
+	5% { transform: translate(0px, -1px) rotate(-10deg) }
+	10% { transform: translate(1px, -3px) rotate(0deg) }
+	15% { transform: translate(1px, 1px) rotate(11deg) }
+	20% { transform: translate(-2px, 1px) rotate(1deg) }
+	25% { transform: translate(-1px, -2px) rotate(-2deg) }
+	30% { transform: translate(-1px, 2px) rotate(-3deg) }
+	35% { transform: translate(2px, 1px) rotate(6deg) }
+	40% { transform: translate(-2px, -3px) rotate(-9deg) }
+	45% { transform: translate(0px, -1px) rotate(-12deg) }
+	50% { transform: translate(1px, 2px) rotate(10deg) }
+	55% { transform: translate(0px, -3px) rotate(8deg) }
+	60% { transform: translate(1px, -1px) rotate(8deg) }
+	65% { transform: translate(0px, -1px) rotate(-7deg) }
+	70% { transform: translate(-1px, -3px) rotate(6deg) }
+	75% { transform: translate(0px, -2px) rotate(4deg) }
+	80% { transform: translate(-2px, -1px) rotate(3deg) }
+	85% { transform: translate(1px, -3px) rotate(-10deg) }
+	90% { transform: translate(1px, 0px) rotate(3deg) }
+	95% { transform: translate(-2px, 0px) rotate(-3deg) }
+	100% { transform: translate(2px, 1px) rotate(2deg) }
+}
+
+@keyframes mfm-rubberBand {
+	from { transform: scale3d(1, 1, 1); }
+	30% { transform: scale3d(1.25, 0.75, 1); }
+	40% { transform: scale3d(0.75, 1.25, 1); }
+	50% { transform: scale3d(1.15, 0.85, 1); }
+	65% { transform: scale3d(0.95, 1.05, 1); }
+	75% { transform: scale3d(1.05, 0.95, 1); }
+	to { transform: scale3d(1, 1, 1); }
+}
+
+@keyframes mfm-rainbow {
+	0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+	100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 71a4285e9d..2beebb0390 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -40,7 +40,7 @@ import { popups, pendingApiRequestsCount } from '@/os';
 import { uploads } from '@/scripts/upload';
 import * as sound from '@/scripts/sound';
 import { $i } from '@/account';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import { defaultStore } from '@/store';
 
@@ -55,7 +55,7 @@ function onNotification(notification) {
 	if ($i.mutingNotificationTypes.includes(notification.type)) return;
 
 	if (document.visibilityState === 'visible') {
-		stream.send('readNotification');
+		useStream().send('readNotification');
 
 		notifications.unshift(notification);
 		window.setTimeout(() => {
@@ -71,7 +71,7 @@ function onNotification(notification) {
 }
 
 if ($i) {
-	const connection = stream.useChannel('main', null, 'UI');
+	const connection = useStream().useChannel('main', null, 'UI');
 	connection.on('notification', onNotification);
 
 	//#region Listen message from SW
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index 2a856e2a45..3e97df7ee5 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -10,7 +10,7 @@
 
 <script lang="ts" setup>
 import { onUnmounted } from 'vue';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
@@ -32,10 +32,10 @@ function reload() {
 	location.reload();
 }
 
-stream.on('_disconnected_', onDisconnected);
+useStream().on('_disconnected_', onDisconnected);
 
 onUnmounted(() => {
-	stream.off('_disconnected_', onDisconnected);
+	useStream().off('_disconnected_', onDisconnected);
 });
 </script>
 
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 792c1ccc5e..c17450f6ed 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -10,7 +10,7 @@
 			<XWidgets place="left" :margin-top="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/>
 		</div>
 
-		<main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
+		<main class="main" @contextmenu.stop="onContextmenu">
 			<div class="content" style="container-type: inline-size;">
 				<RouterView/>
 			</div>
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
new file mode 100644
index 0000000000..628390e3f7
--- /dev/null
+++ b/packages/frontend/src/ui/minimum.vue
@@ -0,0 +1,34 @@
+<template>
+<div class="mk-app" style="container-type: inline-size;">
+	<RouterView/>
+
+	<XCommon/>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { provide, ComputedRef } from 'vue';
+import XCommon from './_common_/common.vue';
+import { mainRouter } from '@/router';
+import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
+import { instanceName } from '@/config';
+
+let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
+
+provide('router', mainRouter);
+provideMetadataReceiver((info) => {
+	pageMetadata = info;
+	if (pageMetadata.value) {
+		document.title = `${pageMetadata.value.title} | ${instanceName}`;
+	}
+});
+
+document.documentElement.style.overflowY = 'scroll';
+</script>
+
+<style lang="scss" scoped>
+.mk-app {
+	min-height: 100dvh;
+	box-sizing: border-box;
+}
+</style>
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 27d0c26ac4..f755dda627 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -4,7 +4,7 @@
 
 	<MkStickyContainer :class="$style.contents">
 		<template #header><XStatusBars :class="$style.statusbars"/></template>
-		<main style="min-width: 0;" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
+		<main style="min-width: 0;" @contextmenu.stop="onContextmenu">
 			<div :class="$style.content" style="container-type: inline-size;">
 				<RouterView/>
 			</div>
diff --git a/packages/frontend/src/widgets/WidgetActivity.chart.vue b/packages/frontend/src/widgets/WidgetActivity.chart.vue
index b61e419f94..cc4df65dd2 100644
--- a/packages/frontend/src/widgets/WidgetActivity.chart.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.chart.vue
@@ -1,26 +1,30 @@
 <template>
-<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" :class="$style.root" @mousedown.prevent="onMousedown">
 	<polyline
 		:points="pointsNote"
 		fill="none"
 		stroke-width="1"
-		stroke="#41ddde"/>
+		stroke="#41ddde"
+	/>
 	<polyline
 		:points="pointsReply"
 		fill="none"
 		stroke-width="1"
-		stroke="#f7796c"/>
+		stroke="#f7796c"
+	/>
 	<polyline
 		:points="pointsRenote"
 		fill="none"
 		stroke-width="1"
-		stroke="#a1de41"/>
+		stroke="#a1de41"
+	/>
 	<polyline
 		:points="pointsTotal"
 		fill="none"
 		stroke-width="1"
 		stroke="#555"
-		stroke-dasharray="2 2"/>
+		stroke-dasharray="2 2"
+	/>
 </svg>
 </template>
 
@@ -81,8 +85,8 @@ function render() {
 }
 </script>
 
-<style lang="scss" scoped>
-svg {
+<style lang="scss" module>
+.root {
 	display: block;
 	padding: 16px;
 	width: 100%;
diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue
index 37326ee981..0c485441d2 100644
--- a/packages/frontend/src/widgets/WidgetAichan.vue
+++ b/packages/frontend/src/widgets/WidgetAichan.vue
@@ -1,6 +1,6 @@
 <template>
 <MkContainer :naked="widgetProps.transparent" :show-header="false" data-cy-mkw-aichan class="mkw-aichan">
-	<iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe>
+	<iframe ref="live2d" :class="$style.root" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe>
 </MkContainer>
 </template>
 
@@ -64,8 +64,8 @@ defineExpose<WidgetComponentExpose>({
 });
 </script>
 
-<style lang="scss" scoped>
-.dedjhjmo {
+<style lang="scss" module>
+.root {
 	width: 100%;
 	height: 350px;
 	border: none;
diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue
index 9eee9680db..98260caeef 100644
--- a/packages/frontend/src/widgets/WidgetButton.vue
+++ b/packages/frontend/src/widgets/WidgetButton.vue
@@ -101,8 +101,3 @@ defineExpose<WidgetComponentExpose>({
 	id: props.widget ? props.widget.id : null,
 });
 </script>
-
-<style lang="scss" scoped>
-.mkw-button {
-}
-</style>
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index 84043cf13f..73a4802595 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -49,7 +49,7 @@
 import { onUnmounted, reactive } from 'vue';
 import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget';
 import { GetFormResultType } from '@/scripts/form';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import number from '@/filters/number';
 import * as sound from '@/scripts/sound';
 import { deepClone } from '@/scripts/clone';
@@ -81,7 +81,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const connection = stream.useChannel('queueStats');
+const connection = useStream().useChannel('queueStats');
 const current = reactive({
 	inbox: {
 		activeSincePrevTick: 0,
diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
index 44e073545d..19195d6fde 100644
--- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue
+++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
@@ -1,6 +1,6 @@
 <template>
 <div data-cy-mkw-onlineUsers class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }">
-	<I18n v-if="onlineUsersCount" :src="i18n.ts.onlineUsersCount" text-tag="span" class="text">
+	<I18n v-if="onlineUsersCount" :src="i18n.ts.onlineUsersCount" textTag="span" class="text">
 		<template #n><b>{{ number(onlineUsersCount) }}</b></template>
 	</I18n>
 </div>
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 716bbb4274..1484aa1941 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -20,7 +20,7 @@
 import { onUnmounted, ref } from 'vue';
 import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget';
 import { GetFormResultType } from '@/scripts/form';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { getStaticImageUrl } from '@/scripts/media-proxy';
 import * as os from '@/os';
 import MkContainer from '@/components/MkContainer.vue';
@@ -54,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const connection = stream.useChannel('main');
+const connection = useStream().useChannel('main');
 const images = ref([]);
 const fetching = ref(true);
 
diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue
index 357d0ab78b..df8de10f63 100644
--- a/packages/frontend/src/widgets/server-metric/index.vue
+++ b/packages/frontend/src/widgets/server-metric/index.vue
@@ -25,7 +25,7 @@ import XDisk from './disk.vue';
 import MkContainer from '@/components/MkContainer.vue';
 import { GetFormResultType } from '@/scripts/form';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import { useStream } from '@/stream';
 import { i18n } from '@/i18n';
 
 const name = 'serverMetric';
@@ -75,7 +75,7 @@ const toggleView = () => {
 	save();
 };
 
-const connection = stream.useChannel('serverStats');
+const connection = useStream().useChannel('serverStats');
 onUnmounted(() => {
 	connection.dispose();
 });
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 7fc21701ce..3de9f9bdba 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -117,7 +117,7 @@ export function getConfig(): UserConfig {
 			manifest: 'manifest.json',
 			rollupOptions: {
 				input: {
-					app: './src/init.ts',
+					app: './src/_boot_.ts',
 				},
 				output: {
 					manualChunks: {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1c43c71024..d5217b71fb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -629,34 +629,34 @@ importers:
         version: 14.1.2
       '@rollup/plugin-alias':
         specifier: 5.0.0
-        version: 5.0.0(rollup@3.21.6)
+        version: 5.0.0(rollup@3.22.0)
       '@rollup/plugin-json':
         specifier: 6.0.0
-        version: 6.0.0(rollup@3.21.6)
+        version: 6.0.0(rollup@3.22.0)
       '@rollup/plugin-replace':
         specifier: 5.0.2
-        version: 5.0.2(rollup@3.21.6)
+        version: 5.0.2(rollup@3.22.0)
       '@rollup/pluginutils':
         specifier: 5.0.2
-        version: 5.0.2(rollup@3.21.6)
+        version: 5.0.2(rollup@3.22.0)
       '@syuilo/aiscript':
-        specifier: 0.13.2
-        version: 0.13.2
+        specifier: 0.13.3
+        version: 0.13.3
       '@tabler/icons-webfont':
         specifier: 2.17.0
         version: 2.17.0
       '@vitejs/plugin-vue':
-        specifier: 4.2.2
-        version: 4.2.2(vite@4.3.5)(vue@3.3.1)
+        specifier: 4.2.3
+        version: 4.2.3(vite@4.3.7)(vue@3.3.2)
       '@vue-macros/reactivity-transform':
-        specifier: 0.3.6
-        version: 0.3.6(rollup@3.21.6)(vue@3.3.1)
+        specifier: 0.3.7
+        version: 0.3.7(rollup@3.22.0)(vue@3.3.2)
       '@vue/compiler-sfc':
-        specifier: 3.3.1
-        version: 3.3.1
+        specifier: 3.3.2
+        version: 3.3.2
       autosize:
-        specifier: 5.0.2
-        version: 5.0.2
+        specifier: 6.0.1
+        version: 6.0.1
       broadcast-channel:
         specifier: 4.20.2
         version: 4.20.2
@@ -665,7 +665,7 @@ importers:
         version: github.com/misskey-dev/browser-image-resizer/0227e860621e55cbed0aabe6dc601096a7748c4a
       buraha:
         specifier: github:misskey-dev/buraha
-        version: github.com/misskey-dev/buraha/0f45367d73bdf3671e7f6ebb5c6c0156337434e0
+        version: github.com/misskey-dev/buraha/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c
       canvas-confetti:
         specifier: 1.6.0
         version: 1.6.0
@@ -742,8 +742,8 @@ importers:
         specifier: 1.0.0
         version: 1.0.0
       rollup:
-        specifier: 3.21.6
-        version: 3.21.6
+        specifier: 3.22.0
+        version: 3.22.0
       s-age:
         specifier: 1.1.2
         version: 1.1.2
@@ -793,81 +793,81 @@ importers:
         specifier: 1.8.0
         version: 1.8.0
       vite:
-        specifier: 4.3.5
-        version: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
+        specifier: 4.3.7
+        version: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
       vue:
-        specifier: 3.3.1
-        version: 3.3.1
+        specifier: 3.3.2
+        version: 3.3.2
       vue-plyr:
         specifier: 7.0.0
         version: 7.0.0
       vue-prism-editor:
         specifier: 2.0.0-alpha.2
-        version: 2.0.0-alpha.2(vue@3.3.1)
+        version: 2.0.0-alpha.2(vue@3.3.2)
       vuedraggable:
         specifier: next
-        version: 4.1.0(vue@3.3.1)
+        version: 4.1.0(vue@3.3.2)
     devDependencies:
       '@storybook/addon-actions':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/addon-essentials':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/addon-interactions':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/addon-links':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/addon-storysource':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/addons':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/blocks':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/core-events':
-        specifier: 7.0.10
-        version: 7.0.10
+        specifier: 7.0.12
+        version: 7.0.12
       '@storybook/jest':
         specifier: 0.1.0
         version: 0.1.0
       '@storybook/manager-api':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/preview-api':
-        specifier: 7.0.10
-        version: 7.0.10
+        specifier: 7.0.12
+        version: 7.0.12
       '@storybook/react':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)
       '@storybook/react-vite':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.5)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.7)
       '@storybook/testing-library':
         specifier: 0.1.0
         version: 0.1.0
       '@storybook/theming':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)
       '@storybook/types':
-        specifier: 7.0.10
-        version: 7.0.10
+        specifier: 7.0.12
+        version: 7.0.12
       '@storybook/vue3':
-        specifier: 7.0.10
-        version: 7.0.10(vue@3.3.1)
+        specifier: 7.0.12
+        version: 7.0.12(vue@3.3.2)
       '@storybook/vue3-vite':
-        specifier: 7.0.10
-        version: 7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.5)(vue@3.3.1)
+        specifier: 7.0.12
+        version: 7.0.12(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.7)(vue@3.3.2)
       '@testing-library/jest-dom':
         specifier: 5.16.5
         version: 5.16.5
       '@testing-library/vue':
         specifier: 7.0.0
-        version: 7.0.0(@vue/compiler-sfc@3.3.1)(vue@3.3.1)
+        version: 7.0.0(@vue/compiler-sfc@3.3.2)(vue@3.3.2)
       '@types/escape-regexp':
         specifier: 0.0.1
         version: 0.0.1
@@ -887,8 +887,8 @@ importers:
         specifier: 4.0.2
         version: 4.0.2
       '@types/node':
-        specifier: 20.1.3
-        version: 20.1.3
+        specifier: 20.1.7
+        version: 20.1.7
       '@types/punycode':
         specifier: 2.1.0
         version: 2.1.0
@@ -926,8 +926,8 @@ importers:
         specifier: 0.31.0
         version: 0.31.0(vitest@0.31.0)
       '@vue/runtime-core':
-        specifier: 3.3.1
-        version: 3.3.1
+        specifier: 3.3.2
+        version: 3.3.2
       astring:
         specifier: 1.8.4
         version: 1.8.4
@@ -947,14 +947,14 @@ importers:
         specifier: 2.27.5
         version: 2.27.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)
       eslint-plugin-vue:
-        specifier: 9.12.0
-        version: 9.12.0(eslint@8.40.0)
+        specifier: 9.13.0
+        version: 9.13.0(eslint@8.40.0)
       fast-glob:
         specifier: 3.2.12
         version: 3.2.12
       happy-dom:
-        specifier: 9.16.0
-        version: 9.16.0
+        specifier: 9.18.3
+        version: 9.18.3
       micromatch:
         specifier: 3.1.10
         version: 3.1.10
@@ -977,11 +977,11 @@ importers:
         specifier: 2.0.0
         version: 2.0.0
       storybook:
-        specifier: 7.0.10
-        version: 7.0.10
+        specifier: 7.0.12
+        version: 7.0.12
       storybook-addon-misskey-theme:
         specifier: github:misskey-dev/storybook-addon-misskey-theme
-        version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.10)(@storybook/components@7.0.10)(@storybook/core-events@7.0.10)(@storybook/manager-api@7.0.10)(@storybook/preview-api@7.0.10)(@storybook/theming@7.0.10)(@storybook/types@7.0.10)(react-dom@18.2.0)(react@18.2.0)
+        version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.12)(@storybook/components@7.0.12)(@storybook/core-events@7.0.12)(@storybook/manager-api@7.0.12)(@storybook/preview-api@7.0.12)(@storybook/theming@7.0.12)(@storybook/types@7.0.12)(react-dom@18.2.0)(react@18.2.0)
       summaly:
         specifier: github:misskey-dev/summaly
         version: github.com/misskey-dev/summaly/77dd5654bb82280b38c1f50e51a771c33f3df503
@@ -990,16 +990,16 @@ importers:
         version: 1.0.2
       vitest:
         specifier: 0.31.0
-        version: 0.31.0(happy-dom@9.16.0)(sass@1.62.1)
+        version: 0.31.0(happy-dom@9.18.3)(sass@1.62.1)
       vitest-fetch-mock:
         specifier: 0.2.2
         version: 0.2.2(vitest@0.31.0)
       vue-eslint-parser:
-        specifier: 9.2.1
-        version: 9.2.1(eslint@8.40.0)
+        specifier: 9.3.0
+        version: 9.3.0(eslint@8.40.0)
       vue-tsc:
-        specifier: 1.6.4
-        version: 1.6.4(typescript@5.0.4)
+        specifier: 1.6.5
+        version: 1.6.5(typescript@5.0.4)
 
   packages/misskey-js:
     dependencies:
@@ -2316,10 +2316,6 @@ packages:
       '@babel/types': 7.21.5
     dev: true
 
-  /@babel/helper-string-parser@7.19.4:
-    resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
-    engines: {node: '>=6.9.0'}
-
   /@babel/helper-string-parser@7.21.5:
     resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
     engines: {node: '>=6.9.0'}
@@ -2365,19 +2361,12 @@ packages:
       js-tokens: 4.0.0
     dev: true
 
-  /@babel/parser@7.21.4:
-    resolution: {integrity: sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==}
-    engines: {node: '>=6.0.0'}
-    hasBin: true
-    dependencies:
-      '@babel/types': 7.21.4
-
   /@babel/parser@7.21.8:
     resolution: {integrity: sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==}
     engines: {node: '>=6.0.0'}
     hasBin: true
     dependencies:
-      '@babel/types': 7.21.4
+      '@babel/types': 7.21.5
 
   /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.21.3):
     resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
@@ -3379,9 +3368,10 @@ packages:
     resolution: {integrity: sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/helper-string-parser': 7.19.4
+      '@babel/helper-string-parser': 7.21.5
       '@babel/helper-validator-identifier': 7.19.1
       to-fast-properties: 2.0.0
+    dev: true
 
   /@babel/types@7.21.5:
     resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==}
@@ -3987,7 +3977,7 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       chalk: 4.1.2
       jest-message-util: 29.5.0
       jest-util: 29.5.0
@@ -4008,14 +3998,14 @@ packages:
       '@jest/test-result': 29.5.0
       '@jest/transform': 29.5.0
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       ci-info: 3.7.1
       exit: 0.1.2
       graceful-fs: 4.2.11
       jest-changed-files: 29.5.0
-      jest-config: 29.5.0(@types/node@20.1.3)
+      jest-config: 29.5.0(@types/node@20.1.7)
       jest-haste-map: 29.5.0
       jest-message-util: 29.5.0
       jest-regex-util: 29.4.3
@@ -4049,7 +4039,7 @@ packages:
     dependencies:
       '@jest/fake-timers': 29.5.0
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       jest-mock: 29.5.0
     dev: true
 
@@ -4076,7 +4066,7 @@ packages:
     dependencies:
       '@jest/types': 29.5.0
       '@sinonjs/fake-timers': 10.0.2
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       jest-message-util: 29.5.0
       jest-mock: 29.5.0
       jest-util: 29.5.0
@@ -4109,7 +4099,7 @@ packages:
       '@jest/transform': 29.5.0
       '@jest/types': 29.5.0
       '@jridgewell/trace-mapping': 0.3.17
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       chalk: 4.1.2
       collect-v8-coverage: 1.0.1
       exit: 0.1.2
@@ -4203,7 +4193,7 @@ packages:
     dependencies:
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       '@types/yargs': 16.0.5
       chalk: 4.1.2
     dev: true
@@ -4215,12 +4205,12 @@ packages:
       '@jest/schemas': 29.4.3
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       '@types/yargs': 17.0.19
       chalk: 4.1.2
     dev: true
 
-  /@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.0.4)(vite@4.3.5):
+  /@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.0.4)(vite@4.3.7):
     resolution: {integrity: sha512-ou4ZJSXMMWHqGS4g8uNRbC5TiTWxAgQZiVucoUrOCWuPrTbkpJbmVyIi9jU72SBry7gQtuMEDp4YR8EEXAg7VQ==}
     peerDependencies:
       typescript: '>= 4.3.x'
@@ -4234,7 +4224,7 @@ packages:
       magic-string: 0.27.0
       react-docgen-typescript: 2.2.2(typescript@5.0.4)
       typescript: 5.0.4
-      vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
+      vite: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
     dev: true
 
   /@jridgewell/gen-mapping@0.1.1:
@@ -4653,7 +4643,7 @@ packages:
       '@redis/client': 1.4.2
     dev: true
 
-  /@rollup/plugin-alias@5.0.0(rollup@3.21.6):
+  /@rollup/plugin-alias@5.0.0(rollup@3.22.0):
     resolution: {integrity: sha512-l9hY5chSCjuFRPsnRm16twWBiSApl2uYFLsepQYwtBuAxNMQ/1dJqADld40P0Jkqm65GRTLy/AC6hnpVebtLsA==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -4662,11 +4652,11 @@ packages:
       rollup:
         optional: true
     dependencies:
-      rollup: 3.21.6
+      rollup: 3.22.0
       slash: 4.0.0
     dev: false
 
-  /@rollup/plugin-json@6.0.0(rollup@3.21.6):
+  /@rollup/plugin-json@6.0.0(rollup@3.22.0):
     resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -4675,11 +4665,11 @@ packages:
       rollup:
         optional: true
     dependencies:
-      '@rollup/pluginutils': 5.0.2(rollup@3.21.6)
-      rollup: 3.21.6
+      '@rollup/pluginutils': 5.0.2(rollup@3.22.0)
+      rollup: 3.22.0
     dev: false
 
-  /@rollup/plugin-replace@5.0.2(rollup@3.21.6):
+  /@rollup/plugin-replace@5.0.2(rollup@3.22.0):
     resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -4688,9 +4678,9 @@ packages:
       rollup:
         optional: true
     dependencies:
-      '@rollup/pluginutils': 5.0.2(rollup@3.21.6)
+      '@rollup/pluginutils': 5.0.2(rollup@3.22.0)
       magic-string: 0.27.0
-      rollup: 3.21.6
+      rollup: 3.22.0
     dev: false
 
   /@rollup/pluginutils@4.2.1:
@@ -4701,7 +4691,7 @@ packages:
       picomatch: 2.3.1
     dev: true
 
-  /@rollup/pluginutils@5.0.2(rollup@3.21.6):
+  /@rollup/pluginutils@5.0.2(rollup@3.22.0):
     resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -4713,7 +4703,7 @@ packages:
       '@types/estree': 1.0.1
       estree-walker: 2.0.2
       picomatch: 2.3.1
-      rollup: 3.21.6
+      rollup: 3.22.0
     dev: false
 
   /@rushstack/node-core-library@3.58.0(@types/node@18.16.3):
@@ -4864,8 +4854,8 @@ packages:
     resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
     dev: false
 
-  /@storybook/addon-actions@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-U8c7n918/mOjXnc1Iu/sglbK+ryC4xoyjWE5SG/68h0+sHb1rioNq7leAi24mCP6jNwNI5Q7TWtuvflOGxQDKQ==}
+  /@storybook/addon-actions@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-f07Mc3qwcG9heGsuUUTIJbWF2nw/Ite3mvyIZY2VbgwhMUMVHj4knY4fh/LojwcUmmmc7CNZu3sJN/wIqpaHCQ==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -4875,14 +4865,14 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       dequal: 2.0.3
       lodash: 4.17.21
       polished: 4.2.2
@@ -4895,8 +4885,8 @@ packages:
       uuid: 9.0.0
     dev: true
 
-  /@storybook/addon-backgrounds@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-QtOxXO9hKtwBjjdLXWYKp4HpcpNOrLfc71dn78XbMKyCkQRlYtVe8GNk/++70UQtFfKCEJIB0hTHrPmSjDJE5A==}
+  /@storybook/addon-backgrounds@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-sAZSxsbj3CcabowALKTafpdnqXMBZB8C42s4Uxv11FCP50GqrP8jp2TqsIiDZxUbeXwI094W/gHnw41MSphG8Q==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -4906,22 +4896,22 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       memoizerific: 1.11.3
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       ts-dedent: 2.2.0
     dev: true
 
-  /@storybook/addon-controls@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-j5UiPH8ZJx0ieUoIeV3iENlsIRDuQCeg3gTlLD668sebx8KHOCSJygh0Zvg1sTUUGSIbenhWaPlqfaW6ShKFWQ==}
+  /@storybook/addon-controls@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-/+yBhswN1N7ttR1NGN94HE/25VELm4YuBtrkh+LJeKP/eQ5CZpLjexASN2GZcfmdnkwIYZAEH0X/AImLaCJAWA==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -4931,15 +4921,15 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/blocks': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-common': 7.0.10
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/node-logger': 7.0.10
-      '@storybook/preview-api': 7.0.10
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/blocks': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-common': 7.0.12
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/node-logger': 7.0.12
+      '@storybook/preview-api': 7.0.12
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       lodash: 4.17.21
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
@@ -4948,8 +4938,8 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/addon-docs@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-1tUsJ+fuBqk4oTOBLabyPQeQYiRKs9I6+soY7dG8jN15Bxe/Ey2giNpqUkA3xAIuqS75ydRVKmsfQvILu2nLjg==}
+  /@storybook/addon-docs@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-zgg4sq34Zz8TN74+kSogxRHsIZ5gsIazJpa0osZp91nJQvsKUEfldjBtQWbBWzjVCrWmzOhW5/RLCnmCNm9y/w==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -4958,19 +4948,19 @@ packages:
       '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.21.3)
       '@jest/transform': 29.5.0
       '@mdx-js/react': 2.3.0(react@18.2.0)
-      '@storybook/blocks': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/csf-plugin': 7.0.10
-      '@storybook/csf-tools': 7.0.10
+      '@storybook/blocks': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/csf-plugin': 7.0.12
+      '@storybook/csf-tools': 7.0.12
       '@storybook/global': 5.0.0
       '@storybook/mdx2-csf': 1.0.0
-      '@storybook/node-logger': 7.0.10
-      '@storybook/postinstall': 7.0.10
-      '@storybook/preview-api': 7.0.10
-      '@storybook/react-dom-shim': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/node-logger': 7.0.12
+      '@storybook/postinstall': 7.0.12
+      '@storybook/preview-api': 7.0.12
+      '@storybook/react-dom-shim': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       fs-extra: 11.1.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
@@ -4981,25 +4971,25 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/addon-essentials@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-nOeUtNbfLXOlgGqqqlsYC9gcYSrAxABBo8jHYiZg3xaEB9+cnKjCKK8VxrqJiR002AG5JZvi+uHeAauM94fkkQ==}
+  /@storybook/addon-essentials@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-Js2cxvauAf8fkA5D0QrqPPe/FvpY1DbJp61VNGh82Xu0zZrczCGYP3jkWG79vl0zllJNs7hnkV8W6xY1JWgLoA==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@storybook/addon-actions': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-backgrounds': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-controls': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-docs': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-highlight': 7.0.10
-      '@storybook/addon-measure': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-outline': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-toolbars': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-viewport': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-common': 7.0.10
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/node-logger': 7.0.10
-      '@storybook/preview-api': 7.0.10
+      '@storybook/addon-actions': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-backgrounds': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-controls': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-docs': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-highlight': 7.0.12
+      '@storybook/addon-measure': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-outline': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-toolbars': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-viewport': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-common': 7.0.12
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/node-logger': 7.0.12
+      '@storybook/preview-api': 7.0.12
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       ts-dedent: 2.2.0
@@ -5007,16 +4997,16 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/addon-highlight@7.0.10:
-    resolution: {integrity: sha512-TohDxElSu7JrSvhLRZAwtNk/7Ot626wvlODwklocE4kbtn1fulFoUlRta7NImBGX554LITDFRy0m4R1rRQ9OfQ==}
+  /@storybook/addon-highlight@7.0.12:
+    resolution: {integrity: sha512-ccIsBVjUlZ7cM1adSSFTqqWXiELPdDqfZLz4dWfDbiLyG3InC953ugtvoUWCIZpC2OOnjVLpF7Rbshq2O/QoMw==}
     dependencies:
-      '@storybook/core-events': 7.0.10
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 7.0.10
+      '@storybook/preview-api': 7.0.12
     dev: true
 
-  /@storybook/addon-interactions@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-7hdFgoetQblwysYwRlmC5fbMVDb6lIM6le1pVEmRci6X44Gr2Xe5w2s6h5bTp4tMpNS1CFKjru9kF/TqfK46wA==}
+  /@storybook/addon-interactions@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-Rb1mv1RQrTd3sA/WwNTdv00rW+7APfvZEeZks6+8+kS1C4EFMDmLnVBZlPllFdo1BOnTCyer4GZZ5ncVkWNLyQ==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5026,16 +5016,16 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-common': 7.0.10
-      '@storybook/core-events': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-common': 7.0.12
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/instrumenter': 7.0.10
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/instrumenter': 7.0.12
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       jest-mock: 27.5.1
       polished: 4.2.2
       react: 18.2.0
@@ -5045,8 +5035,8 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/addon-links@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-Odhe0eICqW9X2yyIjtOVb23cKXJ2WRxPHBm5oYf6hBBoXXK7EJicwyQSJLxJyHK7r1PeAnFxSGlNrO3w7JULjg==}
+  /@storybook/addon-links@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-6kGClsIpX9dRKc5bUAPNcp/4wlgPIxMrieUV+6k1dTsRQqbaEfxih/Fq259D5+yVBDNi3YAnvRjMiIibl8fa5A==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5056,22 +5046,22 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/core-events': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/core-events': 7.0.12
       '@storybook/csf': 0.1.0
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/router': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/router': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       prop-types: 15.8.1
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       ts-dedent: 2.2.0
     dev: true
 
-  /@storybook/addon-measure@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-70BQT8PM6r3qjXDgXuN5mx9CBq9dYTdEgR1tlZ8FbMi8B8tB1oZJD0o6tfGM3r8WjdI0sTwX70ic5pv9Ma/MiA==}
+  /@storybook/addon-measure@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-Uq9cj9QmN7WKBQ6wqeneFmTqo1UQKXIc4CpGBEtJtfsYNLsERrVzOs/tRUf66Zl3lWgfFZxs1B5Ij6RDsYEjRw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5081,19 +5071,19 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/types': 7.0.12
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/addon-outline@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-Aakoc+II7orfgUDmjgMbnSp5HZS/47z0NeRAfh+FP4fxL0lFd9vmaeIXWYo1DjJqdEFfvlSLd8aS9Ltb+souMw==}
+  /@storybook/addon-outline@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-eZPkm3mECdqx1EDJ0S6DAzZ9WZLPIsZH7fRy6vdJJuAgvnOSzkt7AEpA0hlgiNyXcFpE1Cav6/g12FUf4Zo82g==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5103,20 +5093,20 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/types': 7.0.12
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       ts-dedent: 2.2.0
     dev: true
 
-  /@storybook/addon-storysource@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-5anwqnBOcHDI/EB3F2q3Vs/JN+vCBRr8UVqnKS8NqN3BrpJ4q7jUeQ2cA0Q2/aAmdHJn9FLh/Cgx7aTO+6iC2w==}
+  /@storybook/addon-storysource@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-drWG7mY76eL8BwkD5KFMSxnErqF6iOZCk3bpUFGqT5uzx6oVkjmOimZHTEuHtHEi6TtK8QN5KTUi6vXzsGSGpA==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5126,13 +5116,13 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/router': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/source-loader': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/router': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/source-loader': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
       estraverse: 5.3.0
       prop-types: 15.8.1
       react: 18.2.0
@@ -5140,8 +5130,8 @@ packages:
       react-syntax-highlighter: 15.5.0(react@18.2.0)
     dev: true
 
-  /@storybook/addon-toolbars@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-U4a45CDw4SZzrgboYVMgxyiD7Ejud1kSz2lyS+J3fGTZGXq2+tmJS/2oNrLJlSH7v8629lVUbKnFxsP0HbfShg==}
+  /@storybook/addon-toolbars@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-7xRxk+999NVdEwzn2z1O9Tg5iuUSEXQ5jo+hiyK934VvuyqUsZnflKbSvwVEHb2W+DroaaXu8bdHWxGSH+6moQ==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5151,17 +5141,17 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/addon-viewport@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-Ck9sdCg3T8ChXoxYL5IEi+ZUOwdH6Je5XeK4kRVq+Ar+Ytm5CFTGJCCZjI6biroTnuJCUefaV2K5NUZoHkZI+A==}
+  /@storybook/addon-viewport@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-pMgqtDQF8e9AErnRKbbSK9m1lcKn1dFSOkk0PgSBwIIjmha6q+GeT45EHQrQGtkLdtWT0iTktC8ivzIiGKmHkg==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5171,49 +5161,49 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
       memoizerific: 1.11.3
       prop-types: 15.8.1
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/addons@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-RRtozbB0JovZdLgTgC03kOjNh/5sAN77VHZFC5aK/Y9Hz2A0C6V4w/SqTt0382skSllcGMcrHjB1k06BlxlZ8A==}
+  /@storybook/addons@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-yVADbWCFdb12cSpspeb+/6lfTNarPtZZLql49Bhu6E7PxECw/Q3kfHu0LXBLcSnU7f4QqQvQjp88ultt01ABBQ==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/types': 7.0.12
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/blocks@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-OqXuN1x2TjXgrOqGSaD0Vz8iCqmLjiPkrQpWMD7bToFpHH0dpmcrzzRhLhxgJLN2CAzyr98IYIkUgXX9Da1neA==}
+  /@storybook/blocks@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-MbJKjuTJ7xVbkUVwkEwb6vTYGrkRk4+Xtx1UGo+512o91ubqFs8hXwCHP+x/49RCIIQs5zl93Ig8fTtm+MejWw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@storybook/channels': 7.0.10
-      '@storybook/client-logger': 7.0.10
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.10
+      '@storybook/channels': 7.0.12
+      '@storybook/client-logger': 7.0.12
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.12
       '@storybook/csf': 0.1.0
-      '@storybook/docs-tools': 7.0.10
+      '@storybook/docs-tools': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       '@types/lodash': 4.14.191
       color-convert: 2.0.1
       dequal: 2.0.3
@@ -5231,13 +5221,13 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/builder-manager@7.0.10:
-    resolution: {integrity: sha512-izCVE4JEbDVN5DPkX/Ym1PifAJKlheBvXKmGXGklnJQ2l+TEuvesPbOmVFNuu7ptJAFw4JO5n2KAo9+a5FRwiw==}
+  /@storybook/builder-manager@7.0.12:
+    resolution: {integrity: sha512-bkZPSDH38/dUSsO087oQ8+goyaEDP/xD0/O61QcQ8EbaVeT6s6Qt7mMhqsLrtmEZHvPMQwKeIXhOJlRNNXB+SA==}
     dependencies:
       '@fal-works/esbuild-plugin-global-externals': 2.1.2
-      '@storybook/core-common': 7.0.10
-      '@storybook/manager': 7.0.10
-      '@storybook/node-logger': 7.0.10
+      '@storybook/core-common': 7.0.12
+      '@storybook/manager': 7.0.12
+      '@storybook/node-logger': 7.0.12
       '@types/ejs': 3.1.2
       '@types/find-cache-dir': 3.2.1
       '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.17.18)
@@ -5254,8 +5244,8 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/builder-vite@7.0.10(typescript@5.0.4)(vite@4.3.5):
-    resolution: {integrity: sha512-tKY2QnHni10TE3+Sy2wfR7h4FuribR849VBpDI/LcwtRkCgoOBfMCdEnAKMWyU6qAlY+9KDSOQq9SDTu3WZGOg==}
+  /@storybook/builder-vite@7.0.12(typescript@5.0.4)(vite@4.3.7):
+    resolution: {integrity: sha512-6FJNXis+dYs/KrhfRQgz8HcRw/Oq4FaEghCwsjngQDy4PcpQuxkDbvGJKlBaSr92vUL36FWSPq8u5+VGCHjh5Q==}
     peerDependencies:
       '@preact/preset-vite': '*'
       typescript: '>= 4.3.x'
@@ -5269,16 +5259,16 @@ packages:
       vite-plugin-glimmerx:
         optional: true
     dependencies:
-      '@storybook/channel-postmessage': 7.0.10
-      '@storybook/channel-websocket': 7.0.10
-      '@storybook/client-logger': 7.0.10
-      '@storybook/core-common': 7.0.10
-      '@storybook/csf-plugin': 7.0.10
+      '@storybook/channel-postmessage': 7.0.12
+      '@storybook/channel-websocket': 7.0.12
+      '@storybook/client-logger': 7.0.12
+      '@storybook/core-common': 7.0.12
+      '@storybook/csf-plugin': 7.0.12
       '@storybook/mdx2-csf': 1.0.0
-      '@storybook/node-logger': 7.0.10
-      '@storybook/preview': 7.0.10
-      '@storybook/preview-api': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/node-logger': 7.0.12
+      '@storybook/preview': 7.0.12
+      '@storybook/preview-api': 7.0.12
+      '@storybook/types': 7.0.12
       browser-assert: 1.2.1
       es-module-lexer: 0.9.3
       express: 4.18.2
@@ -5288,19 +5278,19 @@ packages:
       magic-string: 0.27.0
       remark-external-links: 8.0.0
       remark-slug: 6.1.0
-      rollup: 3.21.6
+      rollup: 3.22.0
       typescript: 5.0.4
-      vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
+      vite: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@storybook/channel-postmessage@7.0.10:
-    resolution: {integrity: sha512-Y5ZSp9WYH3HznQ2rrGN78Y5fYM16Bfvyn3iKy5QD38gsQk1gTrra1osIZ0X+lk3sep14D4oW4QMW3DCSrn0Big==}
+  /@storybook/channel-postmessage@7.0.12:
+    resolution: {integrity: sha512-Tc7kQZ5yxlZ44Nmmzec92JaDJ6UZ3Ze4cBfiHik4XcnM1PtN8hr8VFoC6a2AIm1ybfIRenfT5w9TH5yriiPIhw==}
     dependencies:
-      '@storybook/channels': 7.0.10
-      '@storybook/client-logger': 7.0.10
-      '@storybook/core-events': 7.0.10
+      '@storybook/channels': 7.0.12
+      '@storybook/client-logger': 7.0.12
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
       qs: 6.11.1
       telejson: 7.0.4
@@ -5328,17 +5318,17 @@ packages:
       telejson: 7.0.4
     dev: true
 
-  /@storybook/channel-websocket@7.0.10:
-    resolution: {integrity: sha512-WXueykS71YxEqKlsIbbmmA6QSChEePffzqs7QASUpHhi21IDqmtq2HhAqYWlQptyqSWYdv6wxrOqwe6z4zakLA==}
+  /@storybook/channel-websocket@7.0.12:
+    resolution: {integrity: sha512-UV6b9gX2mQLtXlKaFKCHcy+6MaK2od6BYqSJfainnBjDsMIXyhcf7fJaj0XQkJrbNnRBwGhw+6s8JxL98xp7Ew==}
     dependencies:
-      '@storybook/channels': 7.0.10
-      '@storybook/client-logger': 7.0.10
+      '@storybook/channels': 7.0.12
+      '@storybook/client-logger': 7.0.12
       '@storybook/global': 5.0.0
       telejson: 7.0.4
     dev: true
 
-  /@storybook/channels@7.0.10:
-    resolution: {integrity: sha512-hdPaGV3W7s6MkVcg33S5hmkVgqXC16AA7H0ID9MROiU1lQzolKhSqCs2iVfGPQfmWzEJeqWQoEXU7dmCclRM0w==}
+  /@storybook/channels@7.0.12:
+    resolution: {integrity: sha512-KDdDmDs8kxAJU+vndTqTNazjLO+XoIPiTRlfP7mk7cgHiQXSjMYy3JSCQ7W0of0Q+9VSl/ve9CNbnGbcQF7rNQ==}
     dev: true
 
   /@storybook/channels@7.0.2:
@@ -5349,20 +5339,20 @@ packages:
     resolution: {integrity: sha512-+34cVmrXZ3lb1s5tDK+OWd5HLtEPSUMas0VKFJ0k9LBpFlVl9aiCZBJRvSYmWL7beauUfa+HSmJgjlD6228ChQ==}
     dev: true
 
-  /@storybook/cli@7.0.10:
-    resolution: {integrity: sha512-FhtE6Yrk7MMa9AgLb3MTmqiQ3IlWHjjrj7Vcj2QM6BcP342xSe7C1d+V6+tYX/oPOEB3Upz+PKNrju1iHxurQQ==}
+  /@storybook/cli@7.0.12:
+    resolution: {integrity: sha512-OABCRIujxsszIJ0CCpKg8Uj4C1UlAwBpBQhv2aMX3lA/pur6Od524syv2ypWu6J2FyvK/ooeyMbjoP7330cIuA==}
     hasBin: true
     dependencies:
       '@babel/core': 7.21.3
       '@babel/preset-env': 7.21.4(@babel/core@7.21.3)
       '@ndelangen/get-tarball': 3.0.7
-      '@storybook/codemod': 7.0.10
-      '@storybook/core-common': 7.0.10
-      '@storybook/core-server': 7.0.10
-      '@storybook/csf-tools': 7.0.10
-      '@storybook/node-logger': 7.0.10
-      '@storybook/telemetry': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/codemod': 7.0.12
+      '@storybook/core-common': 7.0.12
+      '@storybook/core-server': 7.0.12
+      '@storybook/csf-tools': 7.0.12
+      '@storybook/node-logger': 7.0.12
+      '@storybook/telemetry': 7.0.12
+      '@storybook/types': 7.0.12
       '@types/semver': 7.5.0
       boxen: 5.1.2
       chalk: 4.1.2
@@ -5398,8 +5388,8 @@ packages:
       - utf-8-validate
     dev: true
 
-  /@storybook/client-logger@7.0.10:
-    resolution: {integrity: sha512-hb8tO+w28ErzjEw69ERMtZT81Xyg835FQjH6Y42ejoGcBA9Z0W6RZmx4RgkcIUOlYXkU6lSnNVne9gXodV4/Hw==}
+  /@storybook/client-logger@7.0.12:
+    resolution: {integrity: sha512-MQMtIgGEgdixvxnBvZ2m8hhc0DGJWeCpHtxg7oqBLBEBmCYFueTqDZHl4Z6SoCrK0a2YS5X/BIXOcEtP1ulMKw==}
     dependencies:
       '@storybook/global': 5.0.0
     dev: true
@@ -5416,16 +5406,16 @@ packages:
       '@storybook/global': 5.0.0
     dev: true
 
-  /@storybook/codemod@7.0.10:
-    resolution: {integrity: sha512-BnPknLV3wnaSk0azjFBAWLVfwgUHtFvVk9I6y1idIaQhc0nnegKoa0jTxWigthftZK/Pv9yG3gxG7o7O4KcChQ==}
+  /@storybook/codemod@7.0.12:
+    resolution: {integrity: sha512-eGbGZSglvbnY1omzRyEC4XP0FbpuCFKgjXmdHn9faGQUU5EJHwcGYYrRW8JZL3nEVIvNDuRAKzM3p0BVo1xeSQ==}
     dependencies:
       '@babel/core': 7.21.3
       '@babel/preset-env': 7.21.4(@babel/core@7.21.3)
       '@babel/types': 7.21.5
       '@storybook/csf': 0.1.0
-      '@storybook/csf-tools': 7.0.10
-      '@storybook/node-logger': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/csf-tools': 7.0.12
+      '@storybook/node-logger': 7.0.12
+      '@storybook/types': 7.0.12
       cross-spawn: 7.0.3
       globby: 11.1.0
       jscodeshift: 0.14.0(@babel/preset-env@7.21.4)
@@ -5436,17 +5426,17 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/components@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-jdGiVP+a3XqoGpKkDFGt4g2cgb23aLfMS/RhnuhT7FK6hGh7WFfuuqx4QqQHx4VZCdXIWVIzszaCdGCs7AsW2w==}
+  /@storybook/components@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-6TxByzYS4+LxwZRioGpP6Zh9If5ctjQs5OnR2UmQvP6HDjmMWYTntoHKIbDwAL9C6MrnQYpPOGCPkqrtODQ4/w==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@storybook/client-logger': 7.0.10
+      '@storybook/client-logger': 7.0.12
       '@storybook/csf': 0.1.0
       '@storybook/global': 5.0.0
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       memoizerific: 1.11.3
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
@@ -5454,18 +5444,18 @@ packages:
       util-deprecate: 1.0.2
     dev: true
 
-  /@storybook/core-client@7.0.10:
-    resolution: {integrity: sha512-sN/TKB7QHWP6josdjyNtoqDXihROPtgvzo5+akfW6+S7hhfsQ4BJd09nkBqEX9E7z81blmFFDUOU3a8bQbPdKQ==}
+  /@storybook/core-client@7.0.12:
+    resolution: {integrity: sha512-m0r+Vl3LfU8cJl8UqIwzh0sEN9I//nMaT8UIIm481AINhQTNihQcnYi9jRw7USjfz2fv5CYkg8cEr4KhI8QlRA==}
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/preview-api': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/preview-api': 7.0.12
     dev: true
 
-  /@storybook/core-common@7.0.10:
-    resolution: {integrity: sha512-AAYXixukGlpMy8XoSM8cTfcyQ6ijBq5q50xNTj/ssTbGnGSk6POgtoJZf6g8XtS0OxsFXBSxuBuMBBBbKtoztw==}
+  /@storybook/core-common@7.0.12:
+    resolution: {integrity: sha512-PFVjYXHUxDQO1oqfqwQe7S3XoLNO0aZYEr9Zl0LiexlxxnU1v+TQjEfNd/H3T0xxpXlsgzhtEcagdzJeAKyh2g==}
     dependencies:
-      '@storybook/node-logger': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/node-logger': 7.0.12
+      '@storybook/types': 7.0.12
       '@types/node': 16.18.16
       '@types/pretty-hrtime': 1.0.1
       chalk: 4.1.2
@@ -5487,8 +5477,8 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/core-events@7.0.10:
-    resolution: {integrity: sha512-OyBqhxVQOdI78Vgv6nKwXOdIVNChyfktpdxQZP1rz9MpO6MrqMaGAUL7k8xQMQAVx0VY+dAMYZB3bnyN2IC8FA==}
+  /@storybook/core-events@7.0.12:
+    resolution: {integrity: sha512-VTmb/zjbz3o1bg+bATzLigVXMVDC/S1FP8CqIrz4mkiys52139FGzMandL2Y2AecPZPGss7ZRdfma28HKVYTRg==}
     dev: true
 
   /@storybook/core-events@7.0.2:
@@ -5499,23 +5489,23 @@ packages:
     resolution: {integrity: sha512-kGrtjlYtjd4iTVk+Phb4CymZaVkB+MGscKAgcO8gfgJ/Q/gq8HQLVZSIzeoCDcDSHOGlBzbg2WVtdHIHhCKlOQ==}
     dev: true
 
-  /@storybook/core-server@7.0.10:
-    resolution: {integrity: sha512-KFCc3turPed8tiC5IUKTV7oObVmFckMP1XqO7zec2g2NlGQsN83DRso+BA1wpV/bb8AD1NJDU6LJnyN3KKdi1Q==}
+  /@storybook/core-server@7.0.12:
+    resolution: {integrity: sha512-X35Kmg7y35Ph4J+gCDJrnVgBwlz4/DzOQofUS6rAbi4KvrPWDJXeM2OzOgx6B0abKl4CeMmjuc0tjbg4vbUFuA==}
     dependencies:
       '@aw-web-design/x-default-browser': 1.4.88
       '@discoveryjs/json-ext': 0.5.7
-      '@storybook/builder-manager': 7.0.10
-      '@storybook/core-common': 7.0.10
-      '@storybook/core-events': 7.0.10
+      '@storybook/builder-manager': 7.0.12
+      '@storybook/core-common': 7.0.12
+      '@storybook/core-events': 7.0.12
       '@storybook/csf': 0.1.0
-      '@storybook/csf-tools': 7.0.10
+      '@storybook/csf-tools': 7.0.12
       '@storybook/docs-mdx': 0.1.0
       '@storybook/global': 5.0.0
-      '@storybook/manager': 7.0.10
-      '@storybook/node-logger': 7.0.10
-      '@storybook/preview-api': 7.0.10
-      '@storybook/telemetry': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/manager': 7.0.12
+      '@storybook/node-logger': 7.0.12
+      '@storybook/preview-api': 7.0.12
+      '@storybook/telemetry': 7.0.12
+      '@storybook/types': 7.0.12
       '@types/detect-port': 1.3.2
       '@types/node': 16.18.16
       '@types/node-fetch': 2.6.2
@@ -5551,24 +5541,24 @@ packages:
       - utf-8-validate
     dev: true
 
-  /@storybook/csf-plugin@7.0.10:
-    resolution: {integrity: sha512-uUty5rLs6O32tJaXIne2/42UxFL3eaRCDgtAoAkGxbUPa/FLYpO0rYtqF2OG9MagwXU7+As5RlLkDLeYAvUzlQ==}
+  /@storybook/csf-plugin@7.0.12:
+    resolution: {integrity: sha512-iiH0ynLQV5BYFc0o7RlSJS2S3GT/ffyfbV4rnCnPKdqyo4REEVvmhOuLhwzurtsXsjh+xF6VUYUDN+8/5mbkYw==}
     dependencies:
-      '@storybook/csf-tools': 7.0.10
+      '@storybook/csf-tools': 7.0.12
       unplugin: 0.10.2
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@storybook/csf-tools@7.0.10:
-    resolution: {integrity: sha512-sl/995jq03HD7/Q9cb54h0glgt7JLGTkfikSlB35NGMEkgEXEswDmpQHA/TbzUYylIxuAwTKghwMqL3IwSSHwA==}
+  /@storybook/csf-tools@7.0.12:
+    resolution: {integrity: sha512-EcDzKeENzs4awyjx0VxlONDLibiEtIPDP1XdOCcZGtv3nXXBFtS2WDsYhJHkwyvE37jWTyw2e4xKQmBi0Hjvbw==}
     dependencies:
       '@babel/generator': 7.21.3
       '@babel/parser': 7.21.8
       '@babel/traverse': 7.21.3
       '@babel/types': 7.21.5
       '@storybook/csf': 0.1.0
-      '@storybook/types': 7.0.10
+      '@storybook/types': 7.0.12
       fs-extra: 11.1.0
       recast: 0.23.1
       ts-dedent: 2.2.0
@@ -5586,13 +5576,13 @@ packages:
     resolution: {integrity: sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==}
     dev: true
 
-  /@storybook/docs-tools@7.0.10:
-    resolution: {integrity: sha512-w3m7+LlQGI50i07XjiOzIfoap8rnmsrs8hXGUTodbs9vvLt8HBdUaapOGnYr/1BzA0YQJ7Nz2z1nTirQEphmsQ==}
+  /@storybook/docs-tools@7.0.12:
+    resolution: {integrity: sha512-+HykeQLgjyDyF9G7HqY0FHXlX7X5YpQcmNjftJzBrc/GO1EeO0M78d54avcOPyyTfuWOh7oZtSJ0MzjA1qrqaQ==}
     dependencies:
       '@babel/core': 7.21.3
-      '@storybook/core-common': 7.0.10
-      '@storybook/preview-api': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/core-common': 7.0.12
+      '@storybook/preview-api': 7.0.12
+      '@storybook/types': 7.0.12
       '@types/doctrine': 0.0.3
       doctrine: 3.0.0
       lodash: 4.17.21
@@ -5610,14 +5600,14 @@ packages:
     resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
     dev: true
 
-  /@storybook/instrumenter@7.0.10:
-    resolution: {integrity: sha512-Z+kIidnxaq3tneUnIKB2d0DCqb4lwUdOS/AC43LNvd9C6BWYgj89cIPdLDTNhOWa0ZiEju7wTS+K/3uMvcHZ4w==}
+  /@storybook/instrumenter@7.0.12:
+    resolution: {integrity: sha512-jx4rb4AMT1YIOpE0HCdfyLvpYU+94wPkC9vt7sZGWAp7nnYG+KO/lx3XCJaR9qQPIxVYejJtWkeGn4RID79SoQ==}
     dependencies:
-      '@storybook/channels': 7.0.10
-      '@storybook/client-logger': 7.0.10
-      '@storybook/core-events': 7.0.10
+      '@storybook/channels': 7.0.12
+      '@storybook/client-logger': 7.0.12
+      '@storybook/core-events': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 7.0.10
+      '@storybook/preview-api': 7.0.12
     dev: true
 
   /@storybook/instrumenter@7.0.2:
@@ -5649,20 +5639,20 @@ packages:
       jest-mock: 27.5.1
     dev: true
 
-  /@storybook/manager-api@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-Dik73GKUX9QCFOvukTXjZoZX0G6n/LrRMkwLggb28E9m8iFt2ivWvF9MVvyRoDffR9VP5t53+nV5fqxqpXWoQw==}
+  /@storybook/manager-api@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-3QXARtxpc6Xxqf5pviUw2UuhK53+IsINSljeWhAqdQ1Gzbywl67TpibTd7xVN6NKxhUH5Bzo9bIZTAzMZGqaKw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@storybook/channels': 7.0.10
-      '@storybook/client-logger': 7.0.10
-      '@storybook/core-events': 7.0.10
+      '@storybook/channels': 7.0.12
+      '@storybook/client-logger': 7.0.12
+      '@storybook/core-events': 7.0.12
       '@storybook/csf': 0.1.0
       '@storybook/global': 5.0.0
-      '@storybook/router': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/router': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       dequal: 2.0.3
       lodash: 4.17.21
       memoizerific: 1.11.3
@@ -5674,16 +5664,16 @@ packages:
       ts-dedent: 2.2.0
     dev: true
 
-  /@storybook/manager@7.0.10:
-    resolution: {integrity: sha512-cFMOOXmcRx1tN50TqC2huOsF91fAvNM82wTDnAbT2FtA+ZHFHNyE1PgWgiKDDepzOpKaG+FfT4bJcQAaAfYOBg==}
+  /@storybook/manager@7.0.12:
+    resolution: {integrity: sha512-19BsDcwJOYXn6zEarxvNGDdYLUqZyhX92x6GPHSC4cf8BoxHuhmtnz5vOTZHusCxkKIu/C9W0H6wH2Ma47kDCg==}
     dev: true
 
   /@storybook/mdx2-csf@1.0.0:
     resolution: {integrity: sha512-dBAnEL4HfxxJmv7LdEYUoZlQbWj9APZNIbOaq0tgF8XkxiIbzqvgB0jhL/9UOrysSDbQWBiCRTu2wOVxedGfmw==}
     dev: true
 
-  /@storybook/node-logger@7.0.10:
-    resolution: {integrity: sha512-btCCreucTApi7EP84jbfqlFQZDD4Kz9lFLftalZA7nskDZW6i8reNNykTU2Y22TQvlbpqs5kL1N1cEsbG3vepw==}
+  /@storybook/node-logger@7.0.12:
+    resolution: {integrity: sha512-VL+NXzc9NuOP6/9alg4Sofz9kh8tmlo3p+UtCIYCHH088yCsB3XsNhkG9lF1C5EZVWcuHxc2u6MMF3ezOjvKfQ==}
     dependencies:
       '@types/npmlog': 4.1.4
       chalk: 4.1.2
@@ -5691,20 +5681,20 @@ packages:
       pretty-hrtime: 1.0.3
     dev: true
 
-  /@storybook/postinstall@7.0.10:
-    resolution: {integrity: sha512-SVPKGuuvfn1MceLWzYHGbpP77+waLKXglAH4Gkdoa2mKdk3XO45Zn8OhwwNzHuP698boMNaGaB/utBLBpkXMMg==}
+  /@storybook/postinstall@7.0.12:
+    resolution: {integrity: sha512-RKNvBLgABBTQwvGyF2jX4vP7OMLB3KvEEOQDoeOKjqyWfekDn5smI+eT714mtmKIH0YMcwmvzLgEdZkjmM/XhA==}
     dev: true
 
-  /@storybook/preview-api@7.0.10:
-    resolution: {integrity: sha512-URj2YJKbs8hc6JZQ3aA+MmjB4hTSzGZAVFVs3kLUEuaQPDbU1RT5GKxedwF5zlMnkZQPNoaUtopN3z7aF+SKFQ==}
+  /@storybook/preview-api@7.0.12:
+    resolution: {integrity: sha512-YI/AfHszIOYt967fsRlc7j6I0zZB+RSsBwD/nMA8y9vszdpQ0MgRhxHgQxFf6cgqbuQcdCsnTIpT0iQ4GHjDXg==}
     dependencies:
-      '@storybook/channel-postmessage': 7.0.10
-      '@storybook/channels': 7.0.10
-      '@storybook/client-logger': 7.0.10
-      '@storybook/core-events': 7.0.10
+      '@storybook/channel-postmessage': 7.0.12
+      '@storybook/channels': 7.0.12
+      '@storybook/client-logger': 7.0.12
+      '@storybook/core-events': 7.0.12
       '@storybook/csf': 0.1.0
       '@storybook/global': 5.0.0
-      '@storybook/types': 7.0.10
+      '@storybook/types': 7.0.12
       '@types/qs': 6.9.7
       dequal: 2.0.3
       lodash: 4.17.21
@@ -5755,12 +5745,12 @@ packages:
       util-deprecate: 1.0.2
     dev: true
 
-  /@storybook/preview@7.0.10:
-    resolution: {integrity: sha512-IQX8v7OpKeo2Oqeyxo6/uSRys+dJ7zms12jViJWGzx9fg6IchV/iNtf4TBrF3Z2JBNKovk03kICAMHTpZuz9Qg==}
+  /@storybook/preview@7.0.12:
+    resolution: {integrity: sha512-za8El/nnkyAo/uqyqAg7PMuP6DSdPoEnDRyIk4LzY7sAGly6i4Uge377cdo1nUBQLS5S4kKIc4xf8TUegb3G1Q==}
     dev: true
 
-  /@storybook/react-dom-shim@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-NLuE2Be/BGmXHufwLp1Gje+IsTb0HWvwzHlci2U430WgwGU8fsTPNgALMrwCpqN9o1KnrRGpysQEoyIYStQBdg==}
+  /@storybook/react-dom-shim@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-4z9J54TD7uphxPqSuLEzeKTV4oF8Fmv8qFfnT0XZJ2mpYTC2NTbkYoYZQ8N0eYzvNOk6xgfpDqBdmIANf4NaYw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5769,25 +5759,25 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/react-vite@7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.5):
-    resolution: {integrity: sha512-ZEwRpMEJAQtMruG/XGha52XHkb3extXudWT5SoXOcfiRy9eK7Y3oJwHR8KHNH3LE+LrRh7c+D53k7eMudRtsNA==}
+  /@storybook/react-vite@7.0.12(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.7):
+    resolution: {integrity: sha512-SIszevqIKOW+5TwzNposDI+3giSZNVZ7HSu7u2JEpu0Iw/CWyYI06rUgH2ft8Xluhb8vEorZKiZjsdiQDVo64w==}
     engines: {node: '>=16'}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
       vite: ^3.0.0 || ^4.0.0
     dependencies:
-      '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.0.4)(vite@4.3.5)
+      '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.0.4)(vite@4.3.7)
       '@rollup/pluginutils': 4.2.1
-      '@storybook/builder-vite': 7.0.10(typescript@5.0.4)(vite@4.3.5)
-      '@storybook/react': 7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)
-      '@vitejs/plugin-react': 3.1.0(vite@4.3.5)
+      '@storybook/builder-vite': 7.0.12(typescript@5.0.4)(vite@4.3.7)
+      '@storybook/react': 7.0.12(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)
+      '@vitejs/plugin-react': 3.1.0(vite@4.3.7)
       ast-types: 0.14.2
       magic-string: 0.27.0
       react: 18.2.0
       react-docgen: 6.0.0-alpha.3
       react-dom: 18.2.0(react@18.2.0)
-      vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
+      vite: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
     transitivePeerDependencies:
       - '@preact/preset-vite'
       - supports-color
@@ -5795,8 +5785,8 @@ packages:
       - vite-plugin-glimmerx
     dev: true
 
-  /@storybook/react@7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4):
-    resolution: {integrity: sha512-/DDUGFz0bk5c/HCfSr7fL74rQc+3s317TDDKY6ZrgUzdIkze4D/TlAbWV78XV/ceeFNi1fLAUzGjFzuDwmVkJw==}
+  /@storybook/react@7.0.12(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4):
+    resolution: {integrity: sha512-dKHKc02LSgn3St7U/xj/Rr2DFLbS4dWQka+pS/AOvPPvMAR2gGHVhkmoFuFMf176hUTuE5MCoWBoNJIRMz7ZiQ==}
     engines: {node: '>=16.0.0'}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -5806,13 +5796,13 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/core-client': 7.0.10
-      '@storybook/docs-tools': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/core-client': 7.0.12
+      '@storybook/docs-tools': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 7.0.10
-      '@storybook/react-dom-shim': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/preview-api': 7.0.12
+      '@storybook/react-dom-shim': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       '@types/escodegen': 0.0.6
       '@types/estree': 0.0.51
       '@types/node': 16.18.16
@@ -5834,27 +5824,27 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/router@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-Vq3nuyrGsvbPYhsaVu0TwtzX8Yb5TZYg7v5gY/uk1brSIk7Mvw64E8WF4TKNhPcWnlxNrfP9S96IZgT9iuuCpw==}
+  /@storybook/router@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-dOtBiCBGeDem86BCWR7AlTVQjoBk0yw/XZLXS9qcpUfpe+UDjd0Rh21ZdEEMHG1Wfu4d2AhhG5l/JSJ1IE83jQ==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@storybook/client-logger': 7.0.10
+      '@storybook/client-logger': 7.0.12
       memoizerific: 1.11.3
       qs: 6.11.1
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/source-loader@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-DtdYllq0piU6vgoVjsuPsWaGlhSOJgJr/kRovu5zltaZzdEOyQZ7e0zQmA4Py0h9jnGbg2fQG9zccofY3jUdJw==}
+  /@storybook/source-loader@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-dIT4uHiEFgIX/W1aYpKazeu+8GN2OljQsB84oO6Ea887f3emmVJRBGwvoChSAZH+ps2zGput88Lby+W5Paesow==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
       '@storybook/csf': 0.1.0
-      '@storybook/types': 7.0.10
+      '@storybook/types': 7.0.12
       estraverse: 5.3.0
       lodash: 4.17.21
       prettier: 2.8.8
@@ -5862,11 +5852,11 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/telemetry@7.0.10:
-    resolution: {integrity: sha512-0xlMECcSU2UnmpDTxKE/+pKpcW88fhxEZxh54yoA6NPpq6SGUN1r5ybUMffJCZ0JgaQ8HOc3Vxd13T3VXAMLXA==}
+  /@storybook/telemetry@7.0.12:
+    resolution: {integrity: sha512-oxqe15bn5W+1pLpLjXTfj3H+YPZq3jExjdJwTCUHtFrrsNs0k6dyqAUk8qTOUqOTclANHb6vlNBFJDvZ6qbfEQ==}
     dependencies:
-      '@storybook/client-logger': 7.0.10
-      '@storybook/core-common': 7.0.10
+      '@storybook/client-logger': 7.0.12
+      '@storybook/core-common': 7.0.12
       chalk: 4.1.2
       detect-package-manager: 2.0.1
       fetch-retry: 5.0.4
@@ -5889,24 +5879,24 @@ packages:
       ts-dedent: 2.2.0
     dev: true
 
-  /@storybook/theming@7.0.10(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-kKxIMElOUAyIAJOlhU6NS6/F6KpZLWvfGnUYC5V4f5Rsu+lKnbWI/TJ1rCIooz2wZBQ6dv+fjA3sOh5K+LRh2w==}
+  /@storybook/theming@7.0.12(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-frBkvH7LF8j23ODaywLK4m4LLscw49oKblkZ+30QZkBAzRf2o3a/QSZW2V1zfBo7ygcXiUJ5bIjh7Y17mMJqbQ==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
       '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0)
-      '@storybook/client-logger': 7.0.10
+      '@storybook/client-logger': 7.0.12
       '@storybook/global': 5.0.0
       memoizerific: 1.11.3
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/types@7.0.10:
-    resolution: {integrity: sha512-mFktvN8PjjDFJSjck4spikmjtr0AwfOhcEtIf4UCmUD5JHgGppkQmvO6483nGcprSFcWOvD2uYGs8Wp32wG/MQ==}
+  /@storybook/types@7.0.12:
+    resolution: {integrity: sha512-nlvU4MyO2grwPCRQ8alA3AnY1bQxGJ6A4QgJu+1MhtjVenifFlxOQX4H1OiA+YXfjlV096oO5LrxvetJPFAKKQ==}
     dependencies:
-      '@storybook/channels': 7.0.10
+      '@storybook/channels': 7.0.12
       '@types/babel__core': 7.20.0
       '@types/express': 4.17.17
       file-system-cache: 2.0.2
@@ -5930,23 +5920,23 @@ packages:
       file-system-cache: 2.0.2
     dev: true
 
-  /@storybook/vue3-vite@7.0.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.5)(vue@3.3.1):
-    resolution: {integrity: sha512-BbA6uLlNFIpSBW9UAJ9e96yCGGoMho0pogEbkzoRLdw/0OoqDqnRMue78CwW5eiIWXYjNZb3UwAyh9VgYqKk5g==}
+  /@storybook/vue3-vite@7.0.12(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4)(vite@4.3.7)(vue@3.3.2):
+    resolution: {integrity: sha512-SdAGfBRfm4cR9VNLRcBCLo3rTzeUTvZfyh5ll0cgInCo9gTxwfs1Y4zEmmVqDDOWQ7qlpJanITNGFGiSsdvRmg==}
     engines: {node: ^14.18 || >=16}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
       vite: ^3.0.0 || ^4.0.0
     dependencies:
-      '@storybook/builder-vite': 7.0.10(typescript@5.0.4)(vite@4.3.5)
-      '@storybook/core-server': 7.0.10
-      '@storybook/vue3': 7.0.10(vue@3.3.1)
-      '@vitejs/plugin-vue': 4.2.2(vite@4.3.5)(vue@3.3.1)
+      '@storybook/builder-vite': 7.0.12(typescript@5.0.4)(vite@4.3.7)
+      '@storybook/core-server': 7.0.12
+      '@storybook/vue3': 7.0.12(vue@3.3.2)
+      '@vitejs/plugin-vue': 4.2.3(vite@4.3.7)(vue@3.3.2)
       magic-string: 0.27.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
-      vue-docgen-api: 4.64.1(vue@3.3.1)
+      vite: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
+      vue-docgen-api: 4.64.1(vue@3.3.2)
     transitivePeerDependencies:
       - '@preact/preset-vite'
       - bufferutil
@@ -5958,20 +5948,20 @@ packages:
       - vue
     dev: true
 
-  /@storybook/vue3@7.0.10(vue@3.3.1):
-    resolution: {integrity: sha512-B4DW/lR9Am06RJM3TGrIgIYzurG6tsgUX9EQ6rQRDFd4EWw1bskcG8MrNwFBBiDBnXe1frL4AdDidF47CFStNg==}
+  /@storybook/vue3@7.0.12(vue@3.3.2):
+    resolution: {integrity: sha512-zxRhuuNcM9hT1/s968iHL+diqFqRmpwvEoI7rF1yje09saMck+PFStlE8b/ohQeDtm0GdwVqjbzfHZIdPbivYg==}
     engines: {node: '>=16.0.0'}
     peerDependencies:
       vue: ^3.0.0
     dependencies:
-      '@storybook/core-client': 7.0.10
-      '@storybook/docs-tools': 7.0.10
+      '@storybook/core-client': 7.0.12
+      '@storybook/docs-tools': 7.0.12
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 7.0.10
-      '@storybook/types': 7.0.10
+      '@storybook/preview-api': 7.0.12
+      '@storybook/types': 7.0.12
       ts-dedent: 2.2.0
       type-fest: 2.19.0
-      vue: 3.3.1
+      vue: 3.3.2
     transitivePeerDependencies:
       - supports-color
     dev: true
@@ -6126,8 +6116,8 @@ packages:
     dev: false
     optional: true
 
-  /@syuilo/aiscript@0.13.2:
-    resolution: {integrity: sha512-1aqQSH6U+vV01UDUotXUEjIwJKcZZPASJyIJ9msxXRpSInPGJJ/q1kGkZMgSpVhzYFT7/QBxo0UC1ZVEOsrDTw==}
+  /@syuilo/aiscript@0.13.3:
+    resolution: {integrity: sha512-0YFlWA+7YhyRRsp+9Nl72SoSUg5ghskthjCdLvj4qdGyLedeyanKZWJlH2A9d47Nes03UYY8CRDsMHHv64IWcg==}
     dependencies:
       autobind-decorator: 2.4.0
       seedrandom: 3.0.5
@@ -6323,7 +6313,7 @@ packages:
       '@testing-library/dom': 8.20.0
     dev: true
 
-  /@testing-library/vue@7.0.0(@vue/compiler-sfc@3.3.1)(vue@3.3.1):
+  /@testing-library/vue@7.0.0(@vue/compiler-sfc@3.3.2)(vue@3.3.2):
     resolution: {integrity: sha512-JU/q93HGo2qdm1dCgWymkeQlfpC0/0/DBZ2nAHgEAsVZxX11xVIxT7gbXdI7HACQpUbsUWt1zABGU075Fzt9XQ==}
     engines: {node: '>=14'}
     peerDependencies:
@@ -6332,9 +6322,9 @@ packages:
     dependencies:
       '@babel/runtime': 7.21.0
       '@testing-library/dom': 9.2.0
-      '@vue/compiler-sfc': 3.3.1
-      '@vue/test-utils': 2.3.2(vue@3.3.1)
-      vue: 3.3.1
+      '@vue/compiler-sfc': 3.3.2
+      '@vue/test-utils': 2.3.2(vue@3.3.2)
+      vue: 3.3.2
     dev: true
 
   /@tokenizer/token@0.3.0:
@@ -6373,8 +6363,8 @@ packages:
   /@types/babel__core@7.20.0:
     resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==}
     dependencies:
-      '@babel/parser': 7.21.4
-      '@babel/types': 7.21.4
+      '@babel/parser': 7.21.8
+      '@babel/types': 7.21.5
       '@types/babel__generator': 7.6.4
       '@types/babel__template': 7.4.1
       '@types/babel__traverse': 7.18.3
@@ -6407,7 +6397,7 @@ packages:
     resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
     dependencies:
       '@types/connect': 3.4.35
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/braces@3.0.1:
@@ -6427,7 +6417,7 @@ packages:
     dependencies:
       '@types/http-cache-semantics': 4.0.1
       '@types/keyv': 3.1.4
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       '@types/responselike': 1.0.0
     dev: false
 
@@ -6460,7 +6450,7 @@ packages:
   /@types/connect@3.4.35:
     resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/content-disposition@0.5.5:
@@ -6525,7 +6515,7 @@ packages:
   /@types/express-serve-static-core@4.17.33:
     resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       '@types/qs': 6.9.7
       '@types/range-parser': 1.2.4
     dev: true
@@ -6553,27 +6543,27 @@ packages:
     resolution: {integrity: sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==}
     dependencies:
       '@types/glob': 8.1.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/glob@7.2.0:
     resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
     dependencies:
       '@types/minimatch': 5.1.2
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/glob@8.1.0:
     resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==}
     dependencies:
       '@types/minimatch': 5.1.2
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/graceful-fs@4.1.6:
     resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/gulp-rename@2.0.1:
@@ -6586,7 +6576,7 @@ packages:
   /@types/gulp-rename@2.0.2:
     resolution: {integrity: sha512-CQsXqTVtAXqrPd4IbrrlJEEzRkUR3RXsyZbrVoOVqjlchDDmnyRDatAUisjpQjjCg/wjJrSiNg8T1uAbJ/7Qqg==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       '@types/vinyl': 2.0.7
     dev: true
 
@@ -6665,7 +6655,7 @@ packages:
   /@types/keyv@3.1.4:
     resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: false
 
   /@types/lodash@4.14.191:
@@ -6713,7 +6703,7 @@ packages:
   /@types/node-fetch@2.6.2:
     resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       form-data: 3.0.1
 
   /@types/node-fetch@3.0.3:
@@ -6743,6 +6733,10 @@ packages:
 
   /@types/node@20.1.3:
     resolution: {integrity: sha512-NP2yfZpgmf2eDRPmgGq+fjGjSwFgYbihA8/gK+ey23qT9RkxsgNTZvGOEpXgzIGqesTYkElELLgtKoMQTys5vA==}
+    dev: true
+
+  /@types/node@20.1.7:
+    resolution: {integrity: sha512-WCuw/o4GSwDGMoonES8rcvwsig77dGCMbZDrZr2x4ZZiNW4P/gcoZXe/0twgtobcTkmg9TuKflxYL/DuwDyJzg==}
 
   /@types/nodemailer@6.4.7:
     resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==}
@@ -6833,7 +6827,7 @@ packages:
   /@types/readdir-glob@1.1.1:
     resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/redis@4.0.11:
@@ -6849,7 +6843,7 @@ packages:
   /@types/responselike@1.0.0:
     resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: false
 
   /@types/sanitize-html@2.9.0:
@@ -6877,7 +6871,7 @@ packages:
     resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==}
     dependencies:
       '@types/mime': 3.0.1
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/serviceworker@0.0.67:
@@ -6887,7 +6881,7 @@ packages:
   /@types/set-cookie-parser@2.4.2:
     resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /@types/sharp@0.32.0:
@@ -6952,7 +6946,7 @@ packages:
   /@types/undertaker@1.2.8:
     resolution: {integrity: sha512-gW3PRqCHYpo45XFQHJBhch7L6hytPsIe0QeLujlnFsjHPnXLhJcPdN6a9368d7aIQgH2I/dUTPFBlGeSNA3qOg==}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       '@types/undertaker-registry': 1.0.1
       async-done: 1.3.2
     dev: true
@@ -6981,7 +6975,7 @@ packages:
     resolution: {integrity: sha512-LgBpYIWuuGsihnlF+OOWWz4ovwCYlT03gd3DuLwex50cYZLmX3yrW+sFF9ndtmh7zcZpS6Ri47PrIu+fV+sbXw==}
     dependencies:
       '@types/glob-stream': 6.1.1
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       '@types/vinyl': 2.0.7
     dev: true
 
@@ -6989,7 +6983,7 @@ packages:
     resolution: {integrity: sha512-4UqPv+2567NhMQuMLdKAyK4yzrfCqwaTt6bLhHEs8PFcxbHILsrxaY63n4wgE/BRLDWDQeI+WcTmkXKExh9hQg==}
     dependencies:
       '@types/expect': 1.20.4
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
 
   /@types/web-push@3.3.2:
     resolution: {integrity: sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==}
@@ -7037,7 +7031,7 @@ packages:
     resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
     requiresBuild: true
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
     optional: true
 
@@ -7171,7 +7165,7 @@ packages:
       eslint-visitor-keys: 3.4.1
     dev: true
 
-  /@vitejs/plugin-react@3.1.0(vite@4.3.5):
+  /@vitejs/plugin-react@3.1.0(vite@4.3.7):
     resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
@@ -7182,20 +7176,20 @@ packages:
       '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.3)
       magic-string: 0.27.0
       react-refresh: 0.14.0
-      vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
+      vite: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@vitejs/plugin-vue@4.2.2(vite@4.3.5)(vue@3.3.1):
-    resolution: {integrity: sha512-kNH4wMAqs13UiZe/2If1ioO0Mjz71rr2oALTl2c5ajBIox9Vz/UGW/wGkr7GA3SC6Eb29c1HtzAtxdGfbXAkfQ==}
+  /@vitejs/plugin-vue@4.2.3(vite@4.3.7)(vue@3.3.2):
+    resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
       vite: ^4.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
-      vue: 3.3.1
+      vite: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
+      vue: 3.3.2
 
   /@vitest/coverage-c8@0.31.0(vitest@0.31.0):
     resolution: {integrity: sha512-h72qN1D962AO7UefQVulm9JFP5ACS7OfhCdBHioXU8f7ohH/+NTZCgAqmgcfRNHHO/8wLFxx+93YVxhodkEJVA==}
@@ -7207,7 +7201,7 @@ packages:
       magic-string: 0.30.0
       picocolors: 1.0.0
       std-env: 3.3.2
-      vitest: 0.31.0(happy-dom@9.16.0)(sass@1.62.1)
+      vitest: 0.31.0(happy-dom@9.18.3)(sass@1.62.1)
     dev: true
 
   /@vitest/expect@0.31.0:
@@ -7261,8 +7255,8 @@ packages:
       muggle-string: 0.2.2
     dev: true
 
-  /@volar/typescript@1.4.1(typescript@5.0.4):
-    resolution: {integrity: sha512-phTy6p9yG6bgMIKQWEeDOi/aeT0njZsb1a/G1mrEuDsLmAn24Le4gDwSsGNhea6Uhu+3gdpUZn2PmZXa+WG2iQ==}
+  /@volar/typescript@1.4.1-patch.2(typescript@5.0.4):
+    resolution: {integrity: sha512-lPFYaGt8OdMEzNGJJChF40uYqMO4Z/7Q9fHPQC/NRVtht43KotSXLrkPandVVMf9aPbiJ059eAT+fwHGX16k4w==}
     peerDependencies:
       typescript: '*'
     dependencies:
@@ -7270,31 +7264,31 @@ packages:
       typescript: 5.0.4
     dev: true
 
-  /@volar/vue-language-core@1.6.4:
-    resolution: {integrity: sha512-1o+cAtN2DIDNAX/HS8rkjZc8wTMTK+zCab/qtYbvEVlmokhZiDrQeoD9/l0Ug7YCNg+mVuMNHKNBY7pX8U2/Jw==}
+  /@volar/vue-language-core@1.6.5:
+    resolution: {integrity: sha512-IF2b6hW4QAxfsLd5mePmLgtkXzNi+YnH6ltCd80gb7+cbdpFMjM1I+w+nSg2kfBTyfu+W8useCZvW89kPTBpzg==}
     dependencies:
       '@volar/language-core': 1.4.1
       '@volar/source-map': 1.4.1
-      '@vue/compiler-dom': 3.3.1
-      '@vue/compiler-sfc': 3.3.1
-      '@vue/reactivity': 3.3.1
-      '@vue/shared': 3.3.1
+      '@vue/compiler-dom': 3.3.2
+      '@vue/compiler-sfc': 3.3.2
+      '@vue/reactivity': 3.3.2
+      '@vue/shared': 3.3.2
       minimatch: 9.0.0
       muggle-string: 0.2.2
       vue-template-compiler: 2.7.14
     dev: true
 
-  /@volar/vue-typescript@1.6.4(typescript@5.0.4):
-    resolution: {integrity: sha512-qKwgP0KVQR/aaH/SN3AP7RB8NnXPWDn3tjyXP6IT6etxkDeZLBLsXWUD9KMak/RvV1DgbXDuz4F9yuZlbt29rA==}
+  /@volar/vue-typescript@1.6.5(typescript@5.0.4):
+    resolution: {integrity: sha512-er9rVClS4PHztMUmtPMDTl+7c7JyrxweKSAEe/o/Noeq2bQx6v3/jZHVHBe8ZNUti5ubJL/+Tg8L3bzmlalV8A==}
     peerDependencies:
       typescript: '*'
     dependencies:
-      '@volar/typescript': 1.4.1(typescript@5.0.4)
-      '@volar/vue-language-core': 1.6.4
+      '@volar/typescript': 1.4.1-patch.2(typescript@5.0.4)
+      '@volar/vue-language-core': 1.6.5
       typescript: 5.0.4
     dev: true
 
-  /@vue-macros/common@1.3.1(rollup@3.21.6)(vue@3.3.1):
+  /@vue-macros/common@1.3.1(rollup@3.22.0)(vue@3.3.2):
     resolution: {integrity: sha512-Lc5aP/8HNJD1XrnvpeNuWcCf82bZdR3auN/chA1b/1rKZgSnmQkH9f33tKO9qLwXSy+u4hpCi8Rw+oUuF1KCeg==}
     engines: {node: '>=14.19.0'}
     peerDependencies:
@@ -7304,28 +7298,28 @@ packages:
         optional: true
     dependencies:
       '@babel/types': 7.21.5
-      '@rollup/pluginutils': 5.0.2(rollup@3.21.6)
-      '@vue/compiler-sfc': 3.3.1
+      '@rollup/pluginutils': 5.0.2(rollup@3.22.0)
+      '@vue/compiler-sfc': 3.3.2
       local-pkg: 0.4.3
       magic-string-ast: 0.1.2
-      vue: 3.3.1
+      vue: 3.3.2
     transitivePeerDependencies:
       - rollup
     dev: false
 
-  /@vue-macros/reactivity-transform@0.3.6(rollup@3.21.6)(vue@3.3.1):
-    resolution: {integrity: sha512-PFJRXHEdIP03LeNnfcwjUk8pKWjvyeOZjCNwbLgfqunI9tknG4IQMfl86qswK83f+DoOTMCoeMFoUnmlbr+yUw==}
+  /@vue-macros/reactivity-transform@0.3.7(rollup@3.22.0)(vue@3.3.2):
+    resolution: {integrity: sha512-o+u5qstvUjNoaZjr4lUtNf5MuLgQHikvurnk6b2DFd9nB52j+BqOhI22uyn6K6TTAU0i0/PxT5YgwDlwVdvEUw==}
     engines: {node: '>=14.19.0'}
     peerDependencies:
       vue: ^2.7.0 || ^3.2.25
     dependencies:
       '@babel/parser': 7.21.8
-      '@vue-macros/common': 1.3.1(rollup@3.21.6)(vue@3.3.1)
+      '@vue-macros/common': 1.3.1(rollup@3.22.0)(vue@3.3.2)
       '@vue/compiler-core': 3.3.1
-      '@vue/shared': 3.3.1
+      '@vue/shared': 3.3.2
       magic-string: 0.30.0
       unplugin: 1.3.1
-      vue: 3.3.1
+      vue: 3.3.2
     transitivePeerDependencies:
       - rollup
     dev: false
@@ -7337,12 +7331,21 @@ packages:
       '@vue/shared': 3.3.1
       estree-walker: 2.0.2
       source-map-js: 1.0.2
+    dev: false
 
-  /@vue/compiler-dom@3.3.1:
-    resolution: {integrity: sha512-VmgIsoLivCft3+oNc5KM7b9wd0nZxP/g2qilMwi1hJyGA624KWnNKHn4hzBQs4FpzydUVpNy+TWVT8KiRCh3MQ==}
+  /@vue/compiler-core@3.3.2:
+    resolution: {integrity: sha512-CKZWo1dzsQYTNTft7whzjL0HsrEpMfiK7pjZ2WFE3bC1NA7caUjWioHSK+49y/LK7Bsm4poJZzAMnvZMQ7OTeg==}
     dependencies:
-      '@vue/compiler-core': 3.3.1
-      '@vue/shared': 3.3.1
+      '@babel/parser': 7.21.8
+      '@vue/shared': 3.3.2
+      estree-walker: 2.0.2
+      source-map-js: 1.0.2
+
+  /@vue/compiler-dom@3.3.2:
+    resolution: {integrity: sha512-6gS3auANuKXLw0XH6QxkWqyPYPunziS2xb6VRenM3JY7gVfZcJvkCBHkb5RuNY1FCbBO3lkIi0CdXUCW1c7SXw==}
+    dependencies:
+      '@vue/compiler-core': 3.3.2
+      '@vue/shared': 3.3.2
 
   /@vue/compiler-sfc@2.7.14:
     resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@@ -7352,75 +7355,79 @@ packages:
       source-map: 0.6.1
     dev: false
 
-  /@vue/compiler-sfc@3.3.1:
-    resolution: {integrity: sha512-G+FPwBbXSLaA4+Ry5/bdD9Oda+sRslQcE9o6JSZaougRiT4OjVL0vtkbQHPrGRTULZV28OcrAjRfSZOSB0OTXQ==}
+  /@vue/compiler-sfc@3.3.2:
+    resolution: {integrity: sha512-jG4jQy28H4BqzEKsQqqW65BZgmo3vzdLHTBjF+35RwtDdlFE+Fk1VWJYUnDMMqkFBo6Ye1ltSKVOMPgkzYj7SQ==}
     dependencies:
-      '@babel/parser': 7.21.4
-      '@vue/compiler-core': 3.3.1
-      '@vue/compiler-dom': 3.3.1
-      '@vue/compiler-ssr': 3.3.1
-      '@vue/reactivity-transform': 3.3.1
-      '@vue/shared': 3.3.1
+      '@babel/parser': 7.21.8
+      '@vue/compiler-core': 3.3.2
+      '@vue/compiler-dom': 3.3.2
+      '@vue/compiler-ssr': 3.3.2
+      '@vue/reactivity-transform': 3.3.2
+      '@vue/shared': 3.3.2
       estree-walker: 2.0.2
       magic-string: 0.30.0
       postcss: 8.4.23
       source-map-js: 1.0.2
 
-  /@vue/compiler-ssr@3.3.1:
-    resolution: {integrity: sha512-QOQWGNCWuSeyKx4KvWSJlnIMGg+/2oCHgkFUYo7aJ+9Uaaz45yRgKQ+FNigy50NYBQIhpXn2e4OSR8GXh4knrQ==}
+  /@vue/compiler-ssr@3.3.2:
+    resolution: {integrity: sha512-K8OfY5FQtZaSOJHHe8xhEfIfLrefL/Y9frv4k4NsyQL3+0lRKxr9QuJhfdBDjkl7Fhz8CzKh63mULvmOfx3l2w==}
     dependencies:
-      '@vue/compiler-dom': 3.3.1
-      '@vue/shared': 3.3.1
+      '@vue/compiler-dom': 3.3.2
+      '@vue/shared': 3.3.2
 
-  /@vue/reactivity-transform@3.3.1:
-    resolution: {integrity: sha512-MkOrJauAGH4MNdxGW/PmrDegMyOGX0wGIdKUZJRBXOTpotDONg7/TPJe2QeGeBCow/5v9iOqZOWCfvmOWIaDMg==}
+  /@vue/reactivity-transform@3.3.2:
+    resolution: {integrity: sha512-iu2WaQvlJHdnONrsyv4ibIEnSsuKF+aHFngGj/y1lwpHQtalpVhKg9wsKMoiKXS9zPNjG9mNKzJS9vudvjzvyg==}
     dependencies:
       '@babel/parser': 7.21.8
-      '@vue/compiler-core': 3.3.1
-      '@vue/shared': 3.3.1
+      '@vue/compiler-core': 3.3.2
+      '@vue/shared': 3.3.2
       estree-walker: 2.0.2
       magic-string: 0.30.0
 
-  /@vue/reactivity@3.3.1:
-    resolution: {integrity: sha512-zCfmazOtyUdC1NS/EPiSYJ4RqojqmTAviJyBbyVvY8zAv5NhK44Yfw0E1tt+m5vz0ZO1ptI9jDKBr3MWIEkpgw==}
+  /@vue/reactivity@3.3.2:
+    resolution: {integrity: sha512-yX8C4uTgg2Tdj+512EEMnMKbLveoITl7YdQX35AYgx8vBvQGszKiiCN46g4RY6/deeo/5DLbeUUGxCq1qWMf5g==}
     dependencies:
-      '@vue/shared': 3.3.1
+      '@vue/shared': 3.3.2
 
-  /@vue/runtime-core@3.3.1:
-    resolution: {integrity: sha512-Ljb37LYafhQqKIasc0r32Cva8gIh6VeSMjlwO6V03tCjHd18gmjP0F4UD+8/a59sGTysAgA8Rb9lIC2DVxRz2Q==}
+  /@vue/runtime-core@3.3.2:
+    resolution: {integrity: sha512-qSl95qj0BvKfcsO+hICqFEoLhJn6++HtsPxmTkkadFbuhe3uQfJ8HmQwvEr7xbxBd2rcJB6XOJg7nWAn/ymC5A==}
     dependencies:
-      '@vue/reactivity': 3.3.1
-      '@vue/shared': 3.3.1
+      '@vue/reactivity': 3.3.2
+      '@vue/shared': 3.3.2
 
-  /@vue/runtime-dom@3.3.1:
-    resolution: {integrity: sha512-NBjYbQPtMklb7lsJsM2Juv5Ygry6mvZP7PdH1GZqrzfLkvlplQT3qCtQMd/sib6yiy8t9m/Y4hVU7X9nzb9Oeg==}
+  /@vue/runtime-dom@3.3.2:
+    resolution: {integrity: sha512-+drStsJT+0mtgHdarT7cXZReCcTFfm6ptxMrz0kAW5hms6UNBd8Q1pi4JKlncAhu+Ld/TevsSp7pqAZxBBoGng==}
     dependencies:
-      '@vue/runtime-core': 3.3.1
-      '@vue/shared': 3.3.1
+      '@vue/runtime-core': 3.3.2
+      '@vue/shared': 3.3.2
       csstype: 3.1.1
 
-  /@vue/server-renderer@3.3.1(vue@3.3.1):
-    resolution: {integrity: sha512-sod8ggOwbkQXw3lBjfzrbdxRS9lw/lNHoMaXghHawNYowf+4WoaLWD5ouz6fPZadUqNKAsqK95p8DYb1vcVfPA==}
+  /@vue/server-renderer@3.3.2(vue@3.3.2):
+    resolution: {integrity: sha512-QCwh6OGwJg6GDLE0fbQhRTR6tnU+XDJ1iCsTYHXBiezCXAhqMygFRij7BiLF4ytvvHcg5kX9joX5R5vP85++wg==}
     peerDependencies:
-      vue: 3.3.1
+      vue: 3.3.2
     dependencies:
-      '@vue/compiler-ssr': 3.3.1
-      '@vue/shared': 3.3.1
-      vue: 3.3.1
+      '@vue/compiler-ssr': 3.3.2
+      '@vue/shared': 3.3.2
+      vue: 3.3.2
 
   /@vue/shared@3.3.1:
     resolution: {integrity: sha512-ybDBtQ+479HL/bkeIOIAwgpeAEACzztkvulJLbK3JMFuTOv4qDivmV3AIsR8RHYJ+RD9tQxcHWBsX4GqEcYrfw==}
+    dev: false
 
-  /@vue/test-utils@2.3.2(vue@3.3.1):
+  /@vue/shared@3.3.2:
+    resolution: {integrity: sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ==}
+
+  /@vue/test-utils@2.3.2(vue@3.3.2):
     resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==}
     peerDependencies:
       vue: ^3.0.1
     dependencies:
       js-beautify: 1.14.6
-      vue: 3.3.1
+      vue: 3.3.2
     optionalDependencies:
-      '@vue/compiler-dom': 3.3.1
-      '@vue/server-renderer': 3.3.1(vue@3.3.1)
+      '@vue/compiler-dom': 3.3.2
+      '@vue/server-renderer': 3.3.2(vue@3.3.2)
     dev: true
 
   /@webgpu/types@0.1.30:
@@ -8113,8 +8120,8 @@ packages:
       postcss-value-parser: 3.3.1
     dev: false
 
-  /autosize@5.0.2:
-    resolution: {integrity: sha512-FPVt5ynkqUAA9gcMZnJHka1XfQgr1WNd/yRfIjmj5WGmjua+u5Hl9hn8M2nU5CNy2bEIcj1ZUwXq7IOHsfZG9w==}
+  /autosize@6.0.1:
+    resolution: {integrity: sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==}
     dev: false
 
   /autwh@0.1.0:
@@ -8289,7 +8296,7 @@ packages:
     resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==}
     engines: {node: '>= 10.0.0'}
     dependencies:
-      '@babel/types': 7.21.4
+      '@babel/types': 7.21.5
 
   /bach@1.2.0:
     resolution: {integrity: sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==}
@@ -9435,8 +9442,8 @@ packages:
   /constantinople@4.0.1:
     resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
     dependencies:
-      '@babel/parser': 7.21.4
-      '@babel/types': 7.21.4
+      '@babel/parser': 7.21.8
+      '@babel/types': 7.21.5
 
   /content-disposition@0.5.4:
     resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
@@ -10551,8 +10558,8 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue@9.12.0(eslint@8.40.0):
-    resolution: {integrity: sha512-xH8PgpDW2WwmFSmRfs/3iWogef1CJzQqX264I65zz77jDuxF2yLy7+GA2diUM8ZNATuSl1+UehMQkb5YEyau5w==}
+  /eslint-plugin-vue@9.13.0(eslint@8.40.0):
+    resolution: {integrity: sha512-aBz9A8WB4wmpnVv0pYUt86cmH9EkcwWzgEwecBxMoRNhQjTL5i4sqadnwShv/hOdr8Hbl8XANGV7dtX9UQIAyA==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
@@ -10563,7 +10570,7 @@ packages:
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.11
       semver: 7.5.0
-      vue-eslint-parser: 9.2.1(eslint@8.40.0)
+      vue-eslint-parser: 9.3.0(eslint@8.40.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
@@ -12050,6 +12057,18 @@ packages:
       webidl-conversions: 7.0.0
       whatwg-encoding: 2.0.0
       whatwg-mimetype: 3.0.0
+    dev: false
+
+  /happy-dom@9.18.3:
+    resolution: {integrity: sha512-b7iMGYeIXvUryNultA0AHEVU0FPpb2djJ/xSVlMDfP7HG4z7FomdqkCEpWtSv1zDL+t1gRUoBbpqFCoUBvjYtg==}
+    dependencies:
+      css.escape: 1.5.1
+      entities: 4.5.0
+      iconv-lite: 0.6.3
+      webidl-conversions: 7.0.0
+      whatwg-encoding: 2.0.0
+      whatwg-mimetype: 3.0.0
+    dev: true
 
   /har-schema@2.0.0:
     resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
@@ -13130,7 +13149,7 @@ packages:
       '@jest/expect': 29.5.0
       '@jest/test-result': 29.5.0
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       chalk: 4.1.2
       co: 4.6.0
       dedent: 0.7.0
@@ -13284,6 +13303,45 @@ packages:
       - supports-color
     dev: true
 
+  /jest-config@29.5.0(@types/node@20.1.7):
+    resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+    peerDependencies:
+      '@types/node': '*'
+      ts-node: '>=9.0.0'
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      ts-node:
+        optional: true
+    dependencies:
+      '@babel/core': 7.21.3
+      '@jest/test-sequencer': 29.5.0
+      '@jest/types': 29.5.0
+      '@types/node': 20.1.7
+      babel-jest: 29.5.0(@babel/core@7.21.3)
+      chalk: 4.1.2
+      ci-info: 3.7.1
+      deepmerge: 4.2.2
+      glob: 7.2.3
+      graceful-fs: 4.2.11
+      jest-circus: 29.5.0
+      jest-environment-node: 29.5.0
+      jest-get-type: 29.4.3
+      jest-regex-util: 29.4.3
+      jest-resolve: 29.5.0
+      jest-runner: 29.5.0
+      jest-util: 29.5.0
+      jest-validate: 29.5.0
+      micromatch: 4.0.5
+      parse-json: 5.2.0
+      pretty-format: 29.5.0
+      slash: 3.0.0
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /jest-diff@28.1.3:
     resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==}
     engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
@@ -13329,7 +13387,7 @@ packages:
       '@jest/environment': 29.5.0
       '@jest/fake-timers': 29.5.0
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       jest-mock: 29.5.0
       jest-util: 29.5.0
     dev: true
@@ -13359,7 +13417,7 @@ packages:
     dependencies:
       '@jest/types': 29.5.0
       '@types/graceful-fs': 4.1.6
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
@@ -13410,7 +13468,7 @@ packages:
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
     dependencies:
       '@jest/types': 27.5.1
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
     dev: true
 
   /jest-mock@29.5.0:
@@ -13473,7 +13531,7 @@ packages:
       '@jest/test-result': 29.5.0
       '@jest/transform': 29.5.0
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -13504,7 +13562,7 @@ packages:
       '@jest/test-result': 29.5.0
       '@jest/transform': 29.5.0
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       chalk: 4.1.2
       cjs-module-lexer: 1.2.2
       collect-v8-coverage: 1.0.1
@@ -13559,7 +13617,7 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       chalk: 4.1.2
       ci-info: 3.7.1
       graceful-fs: 4.2.11
@@ -13584,7 +13642,7 @@ packages:
     dependencies:
       '@jest/test-result': 29.5.0
       '@jest/types': 29.5.0
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -13603,7 +13661,7 @@ packages:
     resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       jest-util: 29.5.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
@@ -17444,8 +17502,8 @@ packages:
       seedrandom: 2.4.2
     dev: false
 
-  /rollup@3.21.6:
-    resolution: {integrity: sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==}
+  /rollup@3.22.0:
+    resolution: {integrity: sha512-imsigcWor5Y/dC0rz2q0bBt9PabcL3TORry2hAa6O6BuMvY71bqHyfReAz5qyAqiQATD1m70qdntqBfBQjVWpQ==}
     engines: {node: '>=14.18.0', npm: '>=8.0.0'}
     hasBin: true
     optionalDependencies:
@@ -18248,11 +18306,11 @@ packages:
     resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==}
     dev: true
 
-  /storybook@7.0.10:
-    resolution: {integrity: sha512-L36+Um+Ra8AKTvv84ODFJfuthmWnR1Lc6pjslcb8LYO+PVlqEOeqSknmTcKntDYwgvKx5lg62urtJxzGdwO0yw==}
+  /storybook@7.0.12:
+    resolution: {integrity: sha512-HKi7NQQTZhBGEU3KUFxTNGtIZcG8+hokiO5TwcIr7s7smAVKdvj9vY5YGsVkiWF39o+5UtafW1B/i9D8lBFsYg==}
     hasBin: true
     dependencies:
-      '@storybook/cli': 7.0.10
+      '@storybook/cli': 7.0.12
     transitivePeerDependencies:
       - bufferutil
       - encoding
@@ -19598,7 +19656,7 @@ packages:
       replace-ext: 1.0.1
     dev: false
 
-  /vite-node@0.31.0(@types/node@20.1.3)(sass@1.62.1):
+  /vite-node@0.31.0(@types/node@20.1.7)(sass@1.62.1):
     resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -19608,7 +19666,7 @@ packages:
       mlly: 1.2.0
       pathe: 1.1.0
       picocolors: 1.0.0
-      vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
+      vite: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -19623,8 +19681,8 @@ packages:
     resolution: {integrity: sha512-irjKcKXRn7v5bPAg4mAbsS6DgibpP1VUFL9tlgxU6lloK6V9yw9qCZkS+s2PtbkZpWNzr3TN3zVJAc6J7gJZmA==}
     dev: true
 
-  /vite@4.3.5(@types/node@20.1.3)(sass@1.62.1):
-    resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==}
+  /vite@4.3.7(@types/node@20.1.7)(sass@1.62.1):
+    resolution: {integrity: sha512-MTIFpbIm9v7Hh5b0wSBgkcWzSBz7SAa6K/cBTwS4kUiQJfQLFlZZRJRQgqunCVzhTPCk674tW+0Qaqh3Q00dBg==}
     engines: {node: ^14.18.0 || >=16.0.0}
     hasBin: true
     peerDependencies:
@@ -19648,10 +19706,10 @@ packages:
       terser:
         optional: true
     dependencies:
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       esbuild: 0.17.18
       postcss: 8.4.23
-      rollup: 3.21.6
+      rollup: 3.22.0
       sass: 1.62.1
     optionalDependencies:
       fsevents: 2.3.2
@@ -19663,12 +19721,12 @@ packages:
       vitest: '>=0.16.0'
     dependencies:
       cross-fetch: 3.1.5
-      vitest: 0.31.0(happy-dom@9.16.0)(sass@1.62.1)
+      vitest: 0.31.0(happy-dom@9.18.3)(sass@1.62.1)
     transitivePeerDependencies:
       - encoding
     dev: true
 
-  /vitest@0.31.0(happy-dom@9.16.0)(sass@1.62.1):
+  /vitest@0.31.0(happy-dom@9.18.3)(sass@1.62.1):
     resolution: {integrity: sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -19701,7 +19759,7 @@ packages:
     dependencies:
       '@types/chai': 4.3.4
       '@types/chai-subset': 1.3.3
-      '@types/node': 20.1.3
+      '@types/node': 20.1.7
       '@vitest/expect': 0.31.0
       '@vitest/runner': 0.31.0
       '@vitest/snapshot': 0.31.0
@@ -19713,7 +19771,7 @@ packages:
       chai: 4.3.7
       concordance: 5.0.4
       debug: 4.3.4(supports-color@8.1.1)
-      happy-dom: 9.16.0
+      happy-dom: 9.18.3
       local-pkg: 0.4.3
       magic-string: 0.30.0
       pathe: 1.1.0
@@ -19722,8 +19780,8 @@ packages:
       strip-literal: 1.0.1
       tinybench: 2.4.0
       tinypool: 0.5.0
-      vite: 4.3.5(@types/node@20.1.3)(sass@1.62.1)
-      vite-node: 0.31.0(@types/node@20.1.3)(sass@1.62.1)
+      vite: 4.3.7(@types/node@20.1.7)(sass@1.62.1)
+      vite-node: 0.31.0(@types/node@20.1.7)(sass@1.62.1)
       why-is-node-running: 2.2.2
     transitivePeerDependencies:
       - less
@@ -19738,26 +19796,26 @@ packages:
     resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
     engines: {node: '>=0.10.0'}
 
-  /vue-docgen-api@4.64.1(vue@3.3.1):
+  /vue-docgen-api@4.64.1(vue@3.3.2):
     resolution: {integrity: sha512-jbOf7ByE3Zvtuk+429Jorl+eIeh2aB2Fx1GUo3xJd1aByJWE8KDlSEa6b11PB1ze8f0sRUBraRDinICCk0KY7g==}
     dependencies:
       '@babel/parser': 7.21.8
-      '@babel/types': 7.21.4
-      '@vue/compiler-dom': 3.3.1
-      '@vue/compiler-sfc': 3.3.1
+      '@babel/types': 7.21.5
+      '@vue/compiler-dom': 3.3.2
+      '@vue/compiler-sfc': 3.3.2
       ast-types: 0.14.2
       hash-sum: 2.0.0
       lru-cache: 8.0.4
       pug: 3.0.2
       recast: 0.22.0
       ts-map: 1.0.3
-      vue-inbrowser-compiler-independent-utils: 4.64.1(vue@3.3.1)
+      vue-inbrowser-compiler-independent-utils: 4.64.1(vue@3.3.2)
     transitivePeerDependencies:
       - vue
     dev: true
 
-  /vue-eslint-parser@9.2.1(eslint@8.40.0):
-    resolution: {integrity: sha512-tPOex4n6jit4E7h68auOEbDMwE58XiP4dylfaVTCOVCouR45g+QFDBjgIdEU52EXJxKyjgh91dLfN2rxUcV0bQ==}
+  /vue-eslint-parser@9.3.0(eslint@8.40.0):
+    resolution: {integrity: sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
@@ -19774,12 +19832,12 @@ packages:
       - supports-color
     dev: true
 
-  /vue-inbrowser-compiler-independent-utils@4.64.1(vue@3.3.1):
+  /vue-inbrowser-compiler-independent-utils@4.64.1(vue@3.3.2):
     resolution: {integrity: sha512-Hn32n07XZ8j9W8+fmOXPQL+i+W2e/8i6mkH4Ju3H6nR0+cfvmWM95GhczYi5B27+Y8JlCKgAo04IUiYce4mKAw==}
     peerDependencies:
       vue: '>=2'
     dependencies:
-      vue: 3.3.1
+      vue: 3.3.2
     dev: true
 
   /vue-plyr@7.0.0:
@@ -19789,13 +19847,13 @@ packages:
       vue: 2.7.14
     dev: false
 
-  /vue-prism-editor@2.0.0-alpha.2(vue@3.3.1):
+  /vue-prism-editor@2.0.0-alpha.2(vue@3.3.2):
     resolution: {integrity: sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==}
     engines: {node: '>=10'}
     peerDependencies:
       vue: ^3.0.0
     dependencies:
-      vue: 3.3.1
+      vue: 3.3.2
     dev: false
 
   /vue-template-compiler@2.7.14:
@@ -19805,14 +19863,14 @@ packages:
       he: 1.2.0
     dev: true
 
-  /vue-tsc@1.6.4(typescript@5.0.4):
-    resolution: {integrity: sha512-8rg8S1AhRJ6/WriENQEhyqH5wsxSxuD5iaD+QnkZn2ArZ6evlhqfBAIcVN8mfSyCV9DeLkQXkOSv/MaeJiJPAQ==}
+  /vue-tsc@1.6.5(typescript@5.0.4):
+    resolution: {integrity: sha512-Wtw3J7CC+JM2OR56huRd5iKlvFWpvDiU+fO1+rqyu4V2nMTotShz4zbOZpW5g9fUOcjnyZYfBo5q5q+D/q27JA==}
     hasBin: true
     peerDependencies:
       typescript: '*'
     dependencies:
-      '@volar/vue-language-core': 1.6.4
-      '@volar/vue-typescript': 1.6.4(typescript@5.0.4)
+      '@volar/vue-language-core': 1.6.5
+      '@volar/vue-typescript': 1.6.5(typescript@5.0.4)
       semver: 7.5.0
       typescript: 5.0.4
     dev: true
@@ -19824,22 +19882,22 @@ packages:
       csstype: 3.1.1
     dev: false
 
-  /vue@3.3.1:
-    resolution: {integrity: sha512-3Rwy4I5idbPVSDZu6I+fFh6tdDSZbauImCTqLxE7y0LpHtiDvPeY01OI7RkFPbva1nk4hoO0sv/NzosH2h60sg==}
+  /vue@3.3.2:
+    resolution: {integrity: sha512-98hJcAhyDwZoOo2flAQBSPVYG/o0HA9ivIy2ktHshjE+6/q8IMQ+kvDKQzOZTFPxvnNMcGM+zS2A00xeZMA7tA==}
     dependencies:
-      '@vue/compiler-dom': 3.3.1
-      '@vue/compiler-sfc': 3.3.1
-      '@vue/runtime-dom': 3.3.1
-      '@vue/server-renderer': 3.3.1(vue@3.3.1)
-      '@vue/shared': 3.3.1
+      '@vue/compiler-dom': 3.3.2
+      '@vue/compiler-sfc': 3.3.2
+      '@vue/runtime-dom': 3.3.2
+      '@vue/server-renderer': 3.3.2(vue@3.3.2)
+      '@vue/shared': 3.3.2
 
-  /vuedraggable@4.1.0(vue@3.3.1):
+  /vuedraggable@4.1.0(vue@3.3.2):
     resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
     peerDependencies:
       vue: ^3.0.1
     dependencies:
       sortablejs: 1.14.0
-      vue: 3.3.1
+      vue: 3.3.2
     dev: false
 
   /w3c-xmlserializer@4.0.0:
@@ -20054,8 +20112,8 @@ packages:
     resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==}
     engines: {node: '>= 10.0.0'}
     dependencies:
-      '@babel/parser': 7.21.4
-      '@babel/types': 7.21.4
+      '@babel/parser': 7.21.8
+      '@babel/types': 7.21.5
       assert-never: 1.2.1
       babel-walk: 3.0.0-canary-5
 
@@ -20352,8 +20410,8 @@ packages:
     version: 2.2.1-misskey.3
     dev: false
 
-  github.com/misskey-dev/buraha/0f45367d73bdf3671e7f6ebb5c6c0156337434e0:
-    resolution: {tarball: https://codeload.github.com/misskey-dev/buraha/tar.gz/0f45367d73bdf3671e7f6ebb5c6c0156337434e0}
+  github.com/misskey-dev/buraha/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c:
+    resolution: {tarball: https://codeload.github.com/misskey-dev/buraha/tar.gz/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c}
     name: buraha
     version: 0.0.0
     dev: false
@@ -20368,7 +20426,7 @@ packages:
       sharp: 0.31.3
     dev: false
 
-  github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.10)(@storybook/components@7.0.10)(@storybook/core-events@7.0.10)(@storybook/manager-api@7.0.10)(@storybook/preview-api@7.0.10)(@storybook/theming@7.0.10)(@storybook/types@7.0.10)(react-dom@18.2.0)(react@18.2.0):
+  github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.12)(@storybook/components@7.0.12)(@storybook/core-events@7.0.12)(@storybook/manager-api@7.0.12)(@storybook/preview-api@7.0.12)(@storybook/theming@7.0.12)(@storybook/types@7.0.12)(react-dom@18.2.0)(react@18.2.0):
     resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
     id: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640
     name: storybook-addon-misskey-theme
@@ -20389,13 +20447,13 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/blocks': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/components': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.10
-      '@storybook/manager-api': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.10
-      '@storybook/theming': 7.0.10(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.10
+      '@storybook/blocks': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/components': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.12
+      '@storybook/manager-api': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.12
+      '@storybook/theming': 7.0.12(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.12
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true