From 97d485bdd279b1756b99937edcac6866673aa1c7 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 30 Dec 2025 15:59:18 +0900
Subject: enhance(frontend): ウィジェットの設定項目の多言語対応 (#17032)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(frontend): ウィジェットの設定項目の多言語対応

* Update Changelog

* refactor: move options locale key to root for optimizing artifacts for locale inlining

* fix

* fix

* ✌️

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 packages/frontend/src/widgets/WidgetSlideshow.vue | 1 +
 1 file changed, 1 insertion(+)

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

diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 240210c1fb..13ca278c23 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -33,6 +33,7 @@ const name = 'slideshow';
 const widgetPropsDef = {
 	height: {
 		type: 'number',
+		label: i18n.ts._widgetOptions.height,
 		default: 300,
 	},
 	folderId: {
-- 
cgit v1.2.3-freya


From 2a14025c29081bd8080b4dec0823a7625a791950 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 9 Jan 2026 12:21:08 +0900
Subject: fix(frontend): popupのemit型が正しく利用できるように修正 (#16826)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): popupのemit型が正しく利用できるように修正

* fix: revert unnecessary code (for testing purpose)

* fix lint

* fix type errors

* fix types

* add comment

* fix

* fix

* fix: OverloadToUnionの仕組みを変更

* add comments, clean up

* fix lint

* fix types

* clean up [ci skip]

* fix

* add comments [ci skip]
---
 packages/backend/src/core/GlobalEventService.ts    |   6 +-
 .../src/core/entities/EmojiEntityService.ts        |   4 +-
 .../src/server/api/endpoints/admin/emoji/list.ts   |  34 +-----
 .../src/components/MkAnnouncementDialog.vue        |   6 +-
 .../frontend/src/components/MkCropperDialog.vue    |  14 ++-
 packages/frontend/src/components/MkDialog.vue      |  11 +-
 .../frontend/src/components/MkDrive.folder.vue     |  10 +-
 packages/frontend/src/components/MkDrive.vue       |  10 +-
 .../frontend/src/components/MkImgPreviewDialog.vue |   5 +
 .../src/components/MkNotificationSelectWindow.vue  |   2 +-
 packages/frontend/src/components/MkUpdated.vue     |   6 +-
 .../frontend/src/components/MkYouTubePlayer.vue    |   6 +-
 packages/frontend/src/os.ts                        | 124 +++++++++------------
 .../frontend/src/pages/custom-emojis-manager.vue   |  15 ++-
 packages/frontend/src/pages/drive.file.info.vue    |   5 +-
 packages/frontend/src/pages/emoji-edit-dialog.vue  |  24 ++--
 packages/frontend/src/pages/settings/drive.vue     |   5 +-
 packages/frontend/src/types/overload-to-union.ts   |  31 ++++++
 packages/frontend/src/utility/autocomplete.ts      |   3 +-
 packages/frontend/src/utility/drive.ts             |  16 ++-
 .../frontend/src/utility/get-drive-file-menu.ts    |   5 +-
 packages/frontend/src/widgets/WidgetSlideshow.vue  |   6 +-
 packages/frontend/src/widgets/widget.ts            |   4 +-
 packages/misskey-js/src/autogen/types.ts           |  11 +-
 24 files changed, 196 insertions(+), 167 deletions(-)
 create mode 100644 packages/frontend/src/types/overload-to-union.ts

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

diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index f4c747b139..da5982abf6 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -38,11 +38,7 @@ export interface BroadcastTypes {
 		emojis: Packed<'EmojiDetailed'>[];
 	};
 	emojiDeleted: {
-		emojis: {
-			id?: string;
-			name: string;
-			[other: string]: any;
-		}[];
+		emojis: Packed<'EmojiDetailed'>[];
 	};
 	announcementCreated: {
 		announcement: Packed<'Announcement'>;
diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts
index 490d3f2511..309de3b08f 100644
--- a/packages/backend/src/core/entities/EmojiEntityService.ts
+++ b/packages/backend/src/core/entities/EmojiEntityService.ts
@@ -41,7 +41,7 @@ export class EmojiEntityService {
 
 	@bindThis
 	public packSimpleMany(
-		emojis: any[],
+		emojis: (MiEmoji['id'] | MiEmoji)[],
 	) {
 		return Promise.all(emojis.map(x => this.packSimple(x)));
 	}
@@ -69,7 +69,7 @@ export class EmojiEntityService {
 
 	@bindThis
 	public packDetailedMany(
-		emojis: any[],
+		emojis: (MiEmoji['id'] | MiEmoji)[],
 	): Promise<Packed<'EmojiDetailed'>[]> {
 		return Promise.all(emojis.map(x => this.packDetailed(x)));
 	}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 34d200455e..658367409c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -24,39 +24,7 @@ export const meta = {
 		optional: false, nullable: false,
 		items: {
 			type: 'object',
-			optional: false, nullable: false,
-			properties: {
-				id: {
-					type: 'string',
-					optional: false, nullable: false,
-					format: 'id',
-				},
-				aliases: {
-					type: 'array',
-					optional: false, nullable: false,
-					items: {
-						type: 'string',
-						optional: false, nullable: false,
-					},
-				},
-				name: {
-					type: 'string',
-					optional: false, nullable: false,
-				},
-				category: {
-					type: 'string',
-					optional: false, nullable: true,
-				},
-				host: {
-					type: 'string',
-					optional: false, nullable: true,
-					description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
-				},
-				url: {
-					type: 'string',
-					optional: false, nullable: false,
-				},
-			},
+			ref: 'EmojiDetailed',
 		},
 	},
 } as const;
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 81c92bfb5c..da0f618e95 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" :zPriority="'middle'" :preferType="'dialog'" @closed="$emit('closed')" @click="onBgClick">
+<MkModal ref="modal" :zPriority="'middle'" :preferType="'dialog'" @closed="emit('closed')" @click="onBgClick">
 	<div ref="rootEl" :class="$style.root">
 		<div :class="$style.header">
 			<span :class="$style.icon">
@@ -44,6 +44,10 @@ const props = defineProps<{
 	announcement: Misskey.entities.Announcement;
 }>();
 
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+}>();
+
 const rootEl = useTemplateRef('rootEl');
 const bottomEl = useTemplateRef('bottomEl');
 const modal = useTemplateRef('modal');
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 6c07eac47a..1fad936d16 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </MkModalWindow>
 </template>
 
-<script lang="ts" setup>
+<script lang="ts" setup generic="F extends File | Blob">
 import { onMounted, useTemplateRef, ref, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import Cropper from 'cropperjs';
@@ -38,13 +38,13 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	imageFile: File | Blob;
+	imageFile: F;
 	aspectRatio: number | null;
 	uploadFolder?: string | null;
 }>();
 
 const emit = defineEmits<{
-	(ev: 'ok', cropped: File | Blob): void;
+	(ev: 'ok', cropped: F): void;
 	(ev: 'cancel'): void;
 	(ev: 'closed'): void;
 }>();
@@ -74,8 +74,14 @@ async function ok() {
 	});
 
 	const f = await promise;
+	let finalFile: F;
+	if (props.imageFile instanceof File) {
+		finalFile = new File([f], props.imageFile.name, { type: f.type }) as F;
+	} else {
+		finalFile = f as F;
+	}
 
-	emit('ok', f);
+	emit('ok', finalFile);
 	if (dialogEl.value != null) dialogEl.value.close();
 }
 
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 705301a6a6..bea0392d2d 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -41,6 +41,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 </MkModal>
 </template>
 
+<script lang="ts">
+export type Result = string | number | true | null;
+export type MkDialogReturnType<T = Result> = { canceled: true, result: undefined } | { canceled: false, result: T };
+</script>
+
 <script lang="ts" setup>
 import { ref, useTemplateRef, computed } from 'vue';
 import MkModal from '@/components/MkModal.vue';
@@ -65,8 +70,6 @@ type Select = {
 	default: OptionValue | null;
 };
 
-type Result = string | number | true | null;
-
 const props = withDefaults(defineProps<{
 	type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
 	title?: string;
@@ -93,7 +96,7 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
+	(ev: 'done', v: MkDialogReturnType): void;
 	(ev: 'closed'): void;
 }>();
 
@@ -131,7 +134,7 @@ function done(canceled: true): void;
 function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
 
 function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
-	emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
+	emit('done', { canceled, result } as MkDialogReturnType);
 	modal.value?.close();
 }
 
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index d7dd12408c..8b2609852c 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -231,17 +231,17 @@ function rename() {
 }
 
 function move() {
-	selectDriveFolder(null).then(folder => {
-		if (folder[0] && folder[0].id === props.folder.id) return;
+	selectDriveFolder(null).then(({ canceled, folders }) => {
+		if (canceled || (folders[0] && folders[0].id === props.folder.id)) return;
 
 		misskeyApi('drive/folders/update', {
 			folderId: props.folder.id,
-			parentId: folder[0] ? folder[0].id : null,
+			parentId: folders[0] ? folders[0].id : null,
 		}).then(() => {
 			globalEvents.emit('driveFoldersUpdated', [{
 				...props.folder,
-				parentId: folder[0] ? folder[0].id : null,
-				parent: folder[0] ?? null,
+				parentId: folders[0] ? folders[0].id : null,
+				parent: folders[0] ?? null,
 			}]);
 		});
 	});
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 71ffb252df..6e286f4882 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -577,17 +577,19 @@ function cd(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder
 async function moveFilesBulk() {
 	if (selectedFiles.value.length === 0) return;
 
-	const toFolder = await selectDriveFolder(folder.value ? folder.value.id : null);
+	const { canceled, folders } = await selectDriveFolder(folder.value ? folder.value.id : null);
+
+	if (canceled) return;
 
 	await os.apiWithDialog('drive/files/move-bulk', {
 		fileIds: selectedFiles.value.map(f => f.id),
-		folderId: toFolder[0] ? toFolder[0].id : null,
+		folderId: folders[0] ? folders[0].id : null,
 	});
 
 	globalEvents.emit('driveFilesUpdated', selectedFiles.value.map(x => ({
 		...x,
-		folderId: toFolder[0] ? toFolder[0].id : null,
-		folder: toFolder[0] ?? null,
+		folderId: folders[0] ? folders[0].id : null,
+		folder: folders[0] ?? null,
 	})));
 }
 
diff --git a/packages/frontend/src/components/MkImgPreviewDialog.vue b/packages/frontend/src/components/MkImgPreviewDialog.vue
index e17a1651cf..32b36727b0 100644
--- a/packages/frontend/src/components/MkImgPreviewDialog.vue
+++ b/packages/frontend/src/components/MkImgPreviewDialog.vue
@@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@close="close"
 	@esc="close"
 	@click="close"
+	@closed="emit('closed')"
 >
 	<template #header>{{ file.name }}</template>
 	<div :class="$style.container">
@@ -27,6 +28,10 @@ defineProps<{
 	file: Misskey.entities.DriveFile;
 }>();
 
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+}>();
+
 const modal = ref<typeof MkModalWindow | null>(null);
 
 function close() {
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 7205e516d2..5300abd0cf 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js';
 type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>;
 
 const emit = defineEmits<{
-	(ev: 'done', v: { excludeTypes: string[] }): void,
+	(ev: 'done', v: { excludeTypes: typeof notificationTypes[number][] }): void,
 	(ev: 'closed'): void,
 }>();
 
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index eba8e5472c..09cf595eab 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" preferType="dialog" :zPriority="'middle'" @click="modal?.close()" @closed="$emit('closed')">
+<MkModal ref="modal" preferType="dialog" :zPriority="'middle'" @click="modal?.close()" @closed="emit('closed')">
 	<div :class="$style.root">
 		<div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
 		<div :class="$style.version">✨{{ version }}🚀</div>
@@ -26,6 +26,10 @@ import { confetti } from '@/utility/confetti.js';
 
 const modal = useTemplateRef('modal');
 
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+}>();
+
 const isBeta = version.includes('-beta') || version.includes('-alpha') || version.includes('-rc');
 
 function whatIsNew() {
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 2375bcc9eb..20c4475779 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkWindow :initialWidth="640" :initialHeight="402" :canResize="true" :closeButton="true">
+<MkWindow :initialWidth="640" :initialHeight="402" :canResize="true" :closeButton="true" @closed="emit('closed')">
 	<template #header>
 		<i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i>
 		<span>{{ title ?? 'YouTube' }}</span>
@@ -34,6 +34,10 @@ const props = defineProps<{
 	url: string;
 }>();
 
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+}>();
+
 const requestUrl = new URL(props.url);
 if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
 
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 3fb204c2b2..73f18bc6b5 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -8,13 +8,15 @@
 import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import * as Misskey from 'misskey-js';
-import type { Component, Ref } from 'vue';
+import type { Component, MaybeRef } from 'vue';
 import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers';
 import type { Form, GetFormResultType } from '@/utility/form.js';
 import type { MenuItem } from '@/types/menu.js';
 import type { PostFormProps } from '@/types/post-form.js';
 import type { UploaderFeatures } from '@/composables/use-uploader.js';
 import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
+import type { MkDialogReturnType } from '@/components/MkDialog.vue';
+import type { OverloadToUnion } from '@/types/overload-to-union.js';
 import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
 import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -159,12 +161,34 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
 }
 
 // props に ref を許可するようにする
-type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> };
+type PropsWithRefs<P> = { [K in keyof P]: MaybeRef<P[K]> };
+type ComponentProps<T extends Component> = PropsWithRefs<CP<T>>;
+
+// 関数の引数が any[] (もっとも広義なもの) かどうかを判定し、any[] の場合は排除 (never) するヘルパー
+type FilterSpecificFunc<T> = T extends (...args: any[]) => void
+	? (any[] extends Parameters<T> ? never : T)
+	: T;
+
+// オブジェクトの各プロパティに対して再帰的、あるいは単純に適用する型関数
+type CleanFunctions<T> = {
+	[K in keyof T]: T[K] extends (...args: any[]) => any
+		? FilterSpecificFunc<T[K]>
+		: T[K];
+};
+
+// emitの関数群をオブジェクト型に変換する（InstanceType<Component>['$emit']はFunctionalComponent = ジェネリックコンポーネントでは使用できない）
+type ComponentEmitsObject<C extends Component, IE = OverloadToUnion<ComponentEmit<C>>> = CleanFunctions<{
+	[K in IE extends (evName: infer U, ...args: any[]) => any ? U & PropertyKey : never]: IE extends (evName: K, ...args: infer A) => infer R
+		? (...args: A) => R
+		: (...args: any[]) => void;
+}>;
 
+// NOTE: ジェネリック型つきのコンポーネントでは、emitsの型推論がうまく働かない（型変数を取り出すことはできないため）
+// NOTE: emitsがOverloadToUnionで対応しているオーバーロードの数を超える場合は、OverloadToUnionの個数を増やせばOK
 export function popup<T extends Component>(
 	component: T,
 	props: ComponentProps<T>,
-	events: Partial<ComponentEmit<T>> = {},
+	events: Partial<ComponentEmitsObject<T>> = {},
 ): { dispose: () => void } {
 	markRaw(component);
 
@@ -192,10 +216,10 @@ export function popup<T extends Component>(
 export async function popupAsyncWithDialog<T extends Component>(
 	componentFetching: Promise<T>,
 	props: ComponentProps<T>,
-	events: Partial<ComponentEmit<T>> = {},
+	events: Partial<ComponentEmitsObject<T>> = {},
 ): Promise<{ dispose: () => void }> {
 	let component: T;
-	let closeWaiting = () => {};
+	let closeWaiting = () => { };
 
 	const timer = window.setTimeout(() => {
 		closeWaiting = waiting();
@@ -291,23 +315,19 @@ export function confirm(props: {
 	});
 }
 
-// TODO: const T extends ... にしたい
-// https://zenn.dev/general_link/articles/813e47b7a0eef7#const-type-parameters
-export function actions<T extends {
+type ActionsAction = {
 	value: string;
 	text: string;
-	primary?: boolean,
-	danger?: boolean,
-}[]>(props: {
+	primary?: boolean;
+	danger?: boolean;
+};
+
+export function actions<const T extends ActionsAction[]>(props: {
 	type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
 	title?: string;
 	text?: string;
 	actions: T;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: T[number]['value'];
-}> {
+}): Promise<MkDialogReturnType<T[number]['value']>> {
 	return new Promise(resolve => {
 		const { dispose } = popup(MkDialog, {
 			...props,
@@ -321,7 +341,7 @@ export function actions<T extends {
 			})),
 		}, {
 			done: result => {
-				resolve(result ? result : { canceled: true });
+				resolve(result as MkDialogReturnType<T[number]['value']>);
 			},
 			closed: () => dispose(),
 		});
@@ -338,11 +358,7 @@ export function inputText(props: {
 	default: string;
 	minLength?: number;
 	maxLength?: number;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: string;
-}>;
+}): Promise<MkDialogReturnType<string>>;
 // min lengthが指定されてたら result は null になり得ないことを保証する overload function
 export function inputText(props: {
 	type?: 'text' | 'email' | 'password' | 'url';
@@ -353,11 +369,7 @@ export function inputText(props: {
 	default?: string;
 	minLength: number;
 	maxLength?: number;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: string;
-}>;
+}): Promise<MkDialogReturnType<string>>;
 export function inputText(props: {
 	type?: 'text' | 'email' | 'password' | 'url';
 	title?: string;
@@ -367,11 +379,7 @@ export function inputText(props: {
 	default?: string | null;
 	minLength?: number;
 	maxLength?: number;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: string | null;
-}>;
+}): Promise<MkDialogReturnType<string | null>>;
 export function inputText(props: {
 	type?: 'text' | 'email' | 'password' | 'url';
 	title?: string;
@@ -381,11 +389,7 @@ export function inputText(props: {
 	default?: string | null;
 	minLength?: number;
 	maxLength?: number;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: string | null;
-}> {
+}): Promise<MkDialogReturnType<string | null>> {
 	return new Promise(resolve => {
 		const { dispose } = popup(MkDialog, {
 			title: props.title,
@@ -400,7 +404,7 @@ export function inputText(props: {
 			},
 		}, {
 			done: result => {
-				resolve(result ? result : { canceled: true });
+				resolve(result as MkDialogReturnType<string | null>);
 			},
 			closed: () => dispose(),
 		});
@@ -414,33 +418,21 @@ export function inputNumber(props: {
 	placeholder?: string | null;
 	autocomplete?: string;
 	default: number;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: number;
-}>;
+}): Promise<MkDialogReturnType<number>>;
 export function inputNumber(props: {
 	title?: string;
 	text?: string;
 	placeholder?: string | null;
 	autocomplete?: string;
 	default?: number | null;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: number | null;
-}>;
+}): Promise<MkDialogReturnType<number | null>>;
 export function inputNumber(props: {
 	title?: string;
 	text?: string;
 	placeholder?: string | null;
 	autocomplete?: string;
 	default?: number | null;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: number | null;
-}> {
+}): Promise<MkDialogReturnType<number | null>> {
 	return new Promise(resolve => {
 		const { dispose } = popup(MkDialog, {
 			title: props.title,
@@ -453,7 +445,7 @@ export function inputNumber(props: {
 			},
 		}, {
 			done: result => {
-				resolve(result ? result : { canceled: true });
+				resolve(result as MkDialogReturnType<number | null>);
 			},
 			closed: () => dispose(),
 		});
@@ -465,11 +457,7 @@ export function inputDatetime(props: {
 	text?: string;
 	placeholder?: string | null;
 	default?: string | null;
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: Date;
-}> {
+}): Promise<MkDialogReturnType<Date>> {
 	return new Promise(resolve => {
 		const { dispose } = popup(MkDialog, {
 			title: props.title,
@@ -481,7 +469,7 @@ export function inputDatetime(props: {
 			},
 		}, {
 			done: result => {
-				resolve(result != null && result.result != null ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
+				resolve(result != null && typeof result.result === 'string' ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
 			},
 			closed: () => dispose(),
 		});
@@ -508,11 +496,7 @@ export function select<C extends OptionValue, D extends C | null = null>(props:
 	text?: string;
 	default?: D;
 	items: (MkSelectItem<C> | undefined)[];
-}): Promise<{
-	canceled: true; result: undefined;
-} | {
-	canceled: false; result: Exclude<D, undefined> extends null ? C | null : C;
-}> {
+}): Promise<MkDialogReturnType<Exclude<D, undefined> extends null ? C | null : C>> {
 	return new Promise(resolve => {
 		const { dispose } = popup(MkDialog, {
 			title: props.title,
@@ -523,7 +507,7 @@ export function select<C extends OptionValue, D extends C | null = null>(props:
 			},
 		}, {
 			done: result => {
-				resolve(result ? result : { canceled: true });
+				resolve(result as MkDialogReturnType<Exclude<D, undefined> extends null ? C | null : C>);
 			},
 			closed: () => dispose(),
 		});
@@ -582,7 +566,7 @@ export function form<F extends Form>(title: string, f: F): Promise<{ canceled: t
 	return new Promise(resolve => {
 		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
 			done: result => {
-				resolve(result);
+				resolve(result as { canceled?: false, result: GetFormResultType<F> });
 			},
 			closed: () => dispose(),
 		});
@@ -634,16 +618,16 @@ export async function pickEmoji(anchorElement: HTMLElement, opts: ComponentProps
 	});
 }
 
-export async function cropImageFile(imageFile: File | Blob, options: {
+export async function cropImageFile<F extends File | Blob>(imageFile: F, options: {
 	aspectRatio: number | null;
-}): Promise<File> {
+}): Promise<F> {
 	return new Promise(resolve => {
 		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
 			imageFile: imageFile,
 			aspectRatio: options.aspectRatio,
 		}, {
 			ok: x => {
-				resolve(x);
+				resolve(x as F);
 			},
 			closed: () => dispose(),
 		});
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 0f306896c9..9bc8992155 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -71,7 +71,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, markRaw, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { computed, markRaw, ref } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkPagination from '@/components/MkPagination.vue';
@@ -116,7 +117,7 @@ const selectAll = () => {
 	}
 };
 
-const toggleSelect = (emoji) => {
+const toggleSelect = (emoji: Misskey.entities.EmojiDetailed) => {
 	if (selectedEmojis.value.includes(emoji.id)) {
 		selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
 	} else {
@@ -124,19 +125,23 @@ const toggleSelect = (emoji) => {
 	}
 };
 
-const add = async (ev: MouseEvent) => {
+const add = async () => {
 	const { dispose } = await os.popupAsyncWithDialog(import('./emoji-edit-dialog.vue').then(x => x.default), {
 	}, {
 		done: result => {
 			if (result.created) {
-				paginator.prepend(result.created);
+				const nowIso = (new Date()).toISOString();
+				paginator.prepend({
+					...result.created,
+					createdAt: nowIso,
+				});
 			}
 		},
 		closed: () => dispose(),
 	});
 };
 
-const edit = async (emoji) => {
+const edit = async (emoji: Misskey.entities.EmojiDetailed) => {
 	const { dispose } = await os.popupAsyncWithDialog(import('./emoji-edit-dialog.vue').then(x => x.default), {
 		emoji: emoji,
 	}, {
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index e3cc1d988e..6b57684188 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -131,10 +131,11 @@ function move() {
 
 	const f = file.value;
 
-	selectDriveFolder(null).then(folder => {
+	selectDriveFolder(null).then(({ canceled, folders }) => {
+		if (canceled) return;
 		misskeyApi('drive/files/update', {
 			fileId: f.id,
-			folderId: folder[0] ? folder[0].id : null,
+			folderId: folders[0] ? folders[0].id : null,
 		}).then(async () => {
 			await _fetch_();
 		});
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index ea4863950d..4b6c5e1c51 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo>
 					</div>
 				</MkFolder>
-				<MkSwitch v-model="isSensitive">isSensitive</MkSwitch>
+				<MkSwitch v-model="isSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
 				<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
 				<MkButton v-if="emoji" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 			</div>
@@ -99,7 +99,7 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
+	(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.EmojiDetailed; created?: Misskey.entities.EmojiDetailed }): void,
 	(ev: 'closed'): void
 }>();
 
@@ -157,19 +157,29 @@ async function done() {
 		localOnly: localOnly.value,
 		roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id),
 		fileId: file.value ? file.value.id : undefined,
-	};
+	} satisfies Misskey.entities.AdminEmojiUpdateRequest;
 
 	if (props.emoji) {
+		const emojiDetailed = {
+			id: props.emoji.id,
+			aliases: params.aliases,
+			name: params.name,
+			category: params.category,
+			host: props.emoji.host,
+			url: file.value ? file.value.url : props.emoji.url,
+			license: params.license,
+			isSensitive: params.isSensitive,
+			localOnly: params.localOnly,
+			roleIdsThatCanBeUsedThisEmojiAsReaction: params.roleIdsThatCanBeUsedThisEmojiAsReaction,
+		} satisfies Misskey.entities.EmojiDetailed;
+
 		await os.apiWithDialog('admin/emoji/update', {
 			id: props.emoji.id,
 			...params,
 		});
 
 		emit('done', {
-			updated: {
-				id: props.emoji.id,
-				...params,
-			},
+			updated: emojiDetailed,
 		});
 
 		windowEl.value?.close();
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 8d443921a9..b170d17a5a 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -296,8 +296,9 @@ if (prefer.s.uploadFolder) {
 }
 
 function chooseUploadFolder() {
-	selectDriveFolder(null).then(async folder => {
-		prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
+	selectDriveFolder(null).then(async ({ canceled, folders }) => {
+		if (canceled) return;
+		prefer.commit('uploadFolder', folders[0] ? folders[0].id : null);
 		os.success();
 		if (prefer.s.uploadFolder) {
 			uploadFolder.value = await misskeyApi('drive/folders/show', {
diff --git a/packages/frontend/src/types/overload-to-union.ts b/packages/frontend/src/types/overload-to-union.ts
new file mode 100644
index 0000000000..3cf16a5f3c
--- /dev/null
+++ b/packages/frontend/src/types/overload-to-union.ts
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+type FlattenAndDedup<T> = T extends (...args: infer A) => infer R ? (...args: A) => R : never;
+
+// 10個で足りなかった場合は増やす
+export type OverloadToUnion<T> = FlattenAndDedup<T extends {
+	(...args: infer A1): infer R1;
+	(...args: infer A2): infer R2;
+	(...args: infer A3): infer R3;
+	(...args: infer A4): infer R4;
+	(...args: infer A5): infer R5;
+	(...args: infer A6): infer R6;
+	(...args: infer A7): infer R7;
+	(...args: infer A8): infer R8;
+	(...args: infer A9): infer R9;
+	(...args: infer A10): infer R10;
+} ? (
+	((...args: A1) => R1) |
+	((...args: A2) => R2) |
+	((...args: A3) => R3) |
+	((...args: A4) => R4) |
+	((...args: A5) => R5) |
+	((...args: A6) => R6) |
+	((...args: A7) => R7) |
+	((...args: A8) => R8) |
+	((...args: A9) => R9) |
+	((...args: A10) => R10)
+) : never>;
diff --git a/packages/frontend/src/utility/autocomplete.ts b/packages/frontend/src/utility/autocomplete.ts
index 82109af1a0..c0b1865617 100644
--- a/packages/frontend/src/utility/autocomplete.ts
+++ b/packages/frontend/src/utility/autocomplete.ts
@@ -213,10 +213,11 @@ export class Autocomplete {
 			const _y = ref(y);
 			const _q = ref(q);
 
-			const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
+			const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
 				textarea: this.textarea,
 				close: this.close,
 				type: type,
+				//@ts-expect-error popupは今のところジェネリック型のコンポーネントに対応していない
 				q: _q,
 				x: _x,
 				y: _y,
diff --git a/packages/frontend/src/utility/drive.ts b/packages/frontend/src/utility/drive.ts
index 4fe2042e78..7578ed36e6 100644
--- a/packages/frontend/src/utility/drive.ts
+++ b/packages/frontend/src/utility/drive.ts
@@ -301,14 +301,26 @@ export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFi
 	});
 }
 
-export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<(Misskey.entities.DriveFolder | null)[]> {
+export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<{
+	canceled: false;
+	folders: (Misskey.entities.DriveFolder | null)[];
+} | {
+	canceled: true;
+	folders: undefined;
+}> {
 	return new Promise((resolve, reject) => {
 		let dispose: () => void;
 		os.popupAsyncWithDialog(import('@/components/MkDriveFolderSelectDialog.vue').then(x => x.default), {
 			initialFolder,
 		}, {
 			done: folders => {
-				resolve(folders);
+				resolve(folders == null ? {
+					canceled: true,
+					folders: undefined,
+				} : {
+					canceled: false,
+					folders,
+				});
 			},
 			closed: () => dispose(),
 		}).then(d => dispose = d.dispose, reject);
diff --git a/packages/frontend/src/utility/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts
index 53ca4389bf..fea7f8f1d3 100644
--- a/packages/frontend/src/utility/get-drive-file-menu.ts
+++ b/packages/frontend/src/utility/get-drive-file-menu.ts
@@ -44,10 +44,11 @@ async function describe(file: Misskey.entities.DriveFile) {
 }
 
 function move(file: Misskey.entities.DriveFile) {
-	selectDriveFolder(null).then(folder => {
+	selectDriveFolder(null).then(({ canceled, folders }) => {
+		if (canceled) return;
 		misskeyApi('drive/files/update', {
 			fileId: file.id,
-			folderId: folder[0] ? folder[0].id : null,
+			folderId: folders[0] ? folders[0].id : null,
 		});
 	});
 }
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 13ca278c23..b812c89e08 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -96,11 +96,11 @@ const fetch = () => {
 };
 
 const choose = () => {
-	selectDriveFolder(null).then(folder => {
-		if (folder[0] == null) {
+	selectDriveFolder(null).then(({ folders, canceled }) => {
+		if (canceled || folders[0] == null) {
 			return;
 		}
-		widgetProps.folderId = folder[0].id;
+		widgetProps.folderId = folders[0].id;
 		save();
 		fetch();
 	});
diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts
index 90adea538f..603cf8a31d 100644
--- a/packages/frontend/src/widgets/widget.ts
+++ b/packages/frontend/src/widgets/widget.ts
@@ -80,8 +80,8 @@ export const useWidgetPropsManager = <F extends FormWithDefault>(
 				form: form,
 				currentSettings: widgetProps,
 			}, {
-				saved: (newProps: GetFormResultType<F>) => {
-					resolve({ canceled: false, result: newProps });
+				saved: (newProps) => {
+					resolve({ canceled: false, result: newProps as GetFormResultType<F> });
 				},
 				canceled: () => {
 					resolve({ canceled: true });
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index f3648e5112..87dc70a4db 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -8226,16 +8226,7 @@ export interface operations {
                     [name: string]: unknown;
                 };
                 content: {
-                    'application/json': {
-                        /** Format: id */
-                        id: string;
-                        aliases: string[];
-                        name: string;
-                        category: string | null;
-                        /** @description The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files. */
-                        host: string | null;
-                        url: string;
-                    }[];
+                    'application/json': components['schemas']['EmojiDetailed'][];
                 };
             };
             /** @description Client error */
-- 
cgit v1.2.3-freya

