From b941c896aa5512240de9121a1850d55aa5f8b68b Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 14 Jan 2026 14:02:50 +0900
Subject: refactor(frontend): MkRadiosの指定をpropsから行うように (#16597)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor(frontend): MkRadiosの指定をpropsから行うように

* spdx

* fix lint

* fix: mkradiosを動的slotsに対応させる

* fix: remove comment [ci skip]

* fix lint

* fix lint

* migrate

* rename

* fix

* fix

* fix types

* remove unused imports

* fix

* wip

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 packages/frontend/src/components/MkRadios.vue | 193 +++++++++++++++-----------
 1 file changed, 111 insertions(+), 82 deletions(-)

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

diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index 426a1d2c2b..43957a0673 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -3,99 +3,128 @@ SPDX-FileCopyrightText: syuilo and misskey-project
 SPDX-License-Identifier: AGPL-3.0-only
 -->
 
+<template>
+<div :class="{ [$style.vertical]: vertical }">
+	<div :class="$style.label">
+		<slot name="label"></slot>
+	</div>
+	<div :class="$style.body">
+		<MkRadio
+			v-for="option in options"
+			:key="getKey(option.value)"
+			v-model="model"
+			:disabled="option.disabled"
+			:value="option.value"
+		>
+			<div :class="[$style.optionContent, { [$style.checked]: model === option.value }]">
+				<i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
+				<div>
+					<slot v-if="option.slotId != null" :name="`option-${option.slotId as SlotNames}`"></slot>
+					<template v-else>
+						<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
+						<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
+					</template>
+				</div>
+			</div>
+		</MkRadio>
+	</div>
+	<div :class="$style.caption">
+		<slot name="caption"></slot>
+	</div>
+</div>
+</template>
+
 <script lang="ts">
-import { Comment, defineComponent, h, ref, watch } from 'vue';
+import type { StyleValue } from 'vue';
+import type { OptionValue } from '@/types/option-value.js';
+
+export type MkRadiosOption<T = OptionValue, S = string> = {
+	value: T;
+	slotId?: S;
+	label?: string;
+	labelStyle?: StyleValue;
+	icon?: string;
+	iconStyle?: StyleValue;
+	caption?: string;
+	disabled?: boolean;
+};
+</script>
+
+<script setup lang="ts" generic="const T extends MkRadiosOption">
 import MkRadio from './MkRadio.vue';
-import type { VNode } from 'vue';
-
-export default defineComponent({
-	props: {
-		modelValue: {
-			required: false,
-		},
-		vertical: {
-			type: Boolean,
-			default: false,
-		},
-	},
-	setup(props, context) {
-		const value = ref(props.modelValue);
-		watch(value, () => {
-			context.emit('update:modelValue', value.value);
-		});
-		watch(() => props.modelValue, v => {
-			value.value = v;
-		});
-		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[];
-
-		// vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
-		options = options.filter(vnode => vnode.type !== Comment);
-
-		return () => h('div', {
-			class: [
-				'novjtcto',
-				...(props.vertical ? ['vertical'] : []),
-			],
-		}, [
-			...(label ? [h('div', {
-				class: 'label',
-			}, label)] : []),
-			h('div', {
-				class: 'body',
-			}, options.map(option => h(MkRadio, {
-				key: option.key as string,
-				value: option.props?.value,
-				disabled: option.props?.disabled,
-				modelValue: value.value,
-				'onUpdate:modelValue': _v => value.value = _v,
-			}, () => option.children)),
-			),
-			...(caption ? [h('div', {
-				class: 'caption',
-			}, caption)] : []),
-		]);
-	},
-});
+
+defineProps<{
+	options: T[];
+	vertical?: boolean;
+}>();
+
+type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
+
+defineSlots<{
+	label?: () => any;
+	caption?: () => any;
+} & {
+	[K in `option-${SlotNames}`]: () => any;
+}>();
+
+const model = defineModel<T['value']>({ required: true });
+
+function getKey(value: OptionValue): PropertyKey {
+	if (value === null) return 'null';
+	return value;
+}
 </script>
 
-<style lang="scss">
-.novjtcto {
-	> .label {
-		font-size: 0.85em;
-		padding: 0 0 8px 0;
-		user-select: none;
+<style lang="scss" module>
+.label {
+	font-size: 0.85em;
+	padding: 0 0 8px 0;
+	user-select: none;
 
-		&:empty {
-			display: none;
-		}
+	&:empty {
+		display: none;
 	}
+}
 
-	> .body {
-		display: flex;
-    gap: 10px;
-    flex-wrap: wrap;
-	}
+.body {
+	display: flex;
+	gap: 10px;
+	flex-wrap: wrap;
+}
 
-	> .caption {
-		font-size: 0.85em;
-		padding: 8px 0 0 0;
-		color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
+.caption {
+	font-size: 0.85em;
+	padding: 8px 0 0 0;
+	color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
 
-		&:empty {
-			display: none;
-		}
+	&:empty {
+		display: none;
 	}
+}
 
-	&.vertical {
-		> .body {
-			flex-direction: column;
-		}
+.optionContent {
+	display: flex;
+	align-items: center;
+	gap: 6px;
+}
+
+.optionCaption {
+	font-size: 0.85em;
+	padding: 2px 0 0 0;
+	color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
+}
+
+.optionContent.checked {
+	.optionCaption {
+		color: color(from var(--MI_THEME-accent) srgb r g b / 0.75);
 	}
 }
+
+.optionIcon {
+	flex-shrink: 0;
+}
+
+.vertical > .body {
+	flex-direction: column;
+}
 </style>
-- 
cgit v1.2.3-freya

