diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2026-01-14 14:02:50 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-14 14:02:50 +0900 |
| commit | b941c896aa5512240de9121a1850d55aa5f8b68b (patch) | |
| tree | 5d96055387b458f5295d791cc00fd5abf14b1752 /packages/frontend/src/components | |
| parent | Bump version to 2026.1.0-beta.0 (diff) | |
| download | misskey-b941c896aa5512240de9121a1850d55aa5f8b68b.tar.gz misskey-b941c896aa5512240de9121a1850d55aa5f8b68b.tar.bz2 misskey-b941c896aa5512240de9121a1850d55aa5f8b68b.zip | |
refactor(frontend): MkRadiosの指定をpropsから行うように (#16597)
* 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>
Diffstat (limited to 'packages/frontend/src/components')
| -rw-r--r-- | packages/frontend/src/components/MkDialog.vue | 3 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkForm.vue | 14 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkImageEffectorFxForm.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkMenu.vue | 15 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkRadio.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkRadios.vue | 185 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkSelect.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkServerSetupWizard.vue | 52 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUserAnnouncementEditDialog.vue | 26 |
9 files changed, 183 insertions, 126 deletions
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index bea0392d2d..4801b412f8 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -52,7 +52,8 @@ import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; -import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue'; +import type { MkSelectItem } from '@/components/MkSelect.vue'; +import type { OptionValue } from '@/types/option-value.js'; import { useMkSelect } from '@/composables/use-mkselect.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/components/MkForm.vue b/packages/frontend/src/components/MkForm.vue index 3d4724e6b7..1ece0ad4c3 100644 --- a/packages/frontend/src/components/MkForm.vue +++ b/packages/frontend/src/components/MkForm.vue @@ -26,9 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)"> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> </MkSelect> - <MkRadios v-else-if="v.type === 'radio'" v-model="values[k]"> + <MkRadios v-else-if="v.type === 'radio'" v-model="values[k]" :options="getRadioOptionsDef(v)"> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> - <option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option> </MkRadios> <MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter"> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> @@ -60,6 +59,7 @@ import MkButton from '@/components/MkButton.vue'; import MkRadios from '@/components/MkRadios.vue'; import { i18n } from '@/i18n.js'; import type { MkSelectItem } from '@/components/MkSelect.vue'; +import type { MkRadiosOption } from '@/components/MkRadios.vue'; import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js'; const props = defineProps<{ @@ -113,7 +113,13 @@ function getMkSelectDef(def: EnumFormItem): MkSelectItem[] { }); } -function getRadioKey(e: RadioFormItem['options'][number]) { - return typeof e.value === 'string' ? e.value : JSON.stringify(e.value); +function getRadioOptionsDef(def: RadioFormItem): MkRadiosOption[] { + return def.options.map<MkRadiosOption>((v) => { + if (typeof v === 'string') { + return { value: v, label: v }; + } else { + return { value: v.value, label: v.label }; + } + }); } </script> diff --git a/packages/frontend/src/components/MkImageEffectorFxForm.vue b/packages/frontend/src/components/MkImageEffectorFxForm.vue index 51485977a9..723b5f093e 100644 --- a/packages/frontend/src/components/MkImageEffectorFxForm.vue +++ b/packages/frontend/src/components/MkImageEffectorFxForm.vue @@ -28,13 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ v.label ?? k }}</template> <template v-if="v.caption != null" #caption>{{ v.caption }}</template> </MkRange> - <MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]"> + <MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]" :options="v.enum"> <template #label>{{ v.label ?? k }}</template> <template v-if="v.caption != null" #caption>{{ v.caption }}</template> - <option v-for="item in v.enum" :value="item.value"> - <i v-if="item.icon" :class="item.icon"></i> - <template v-else>{{ item.label }}</template> - </option> </MkRadios> <div v-else-if="v.type === 'seed'"> <MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1"> diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 22d5802596..b618dab6b2 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -323,9 +323,20 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent | PointerEvent | type: 'radioOption', text: key, action: () => { - item.ref = value; + if ('value' in item.ref) { + item.ref.value = value; + } else { + // @ts-expect-error リアクティビティは保たれる + item.ref = value; + } }, - active: computed(() => item.ref === value), + active: computed(() => { + if ('value' in item.ref) { + return item.ref.value === value; + } else { + return item.ref === value; + } + }), }; }); diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index a7d77dd118..19ba90052c 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -24,8 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup generic="T extends unknown"> +<script lang="ts" setup generic="T extends OptionValue | null"> import { computed } from 'vue'; +import type { OptionValue } from '@/types/option-value.js'; const props = defineProps<{ modelValue: T; @@ -52,7 +53,7 @@ function toggle(): void { align-items: center; text-align: left; cursor: pointer; - padding: 7px 10px; + padding: 8px 10px; min-width: 60px; background-color: var(--MI_THEME-panel); background-clip: padding-box !important; @@ -130,7 +131,6 @@ function toggle(): void { .label { margin-left: 8px; display: block; - line-height: 20px; cursor: pointer; } </style> 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(); +defineProps<{ + options: T[]; + vertical?: boolean; +}>(); + +type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>; - // なぜかFragmentになることがあるため - if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[]; +defineSlots<{ + label?: () => any; + caption?: () => any; +} & { + [K in `option-${SlotNames}`]: () => any; +}>(); - // vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる) - options = options.filter(vnode => vnode.type !== Comment); +const model = defineModel<T['value']>({ required: true }); - 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)] : []), - ]); - }, -}); +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; } +} + +.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); +} - &.vertical { - > .body { - flex-direction: column; - } +.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> diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index f130145e36..6f6957d504 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -export type OptionValue = string | number | null; +import type { OptionValue } from '@/types/option-value.js'; export type ItemOption<T extends OptionValue = OptionValue> = { type?: 'option'; diff --git a/packages/frontend/src/components/MkServerSetupWizard.vue b/packages/frontend/src/components/MkServerSetupWizard.vue index 20cb48fe1c..796c909be9 100644 --- a/packages/frontend/src/components/MkServerSetupWizard.vue +++ b/packages/frontend/src/components/MkServerSetupWizard.vue @@ -14,19 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-settings-question"></i></template> <div class="_gaps_s"> - <MkRadios v-model="q_use" :vertical="true"> - <option value="single"> - <div><i class="ti ti-user"></i> <b>{{ i18n.ts._serverSetupWizard._use.single }}</b></div> - <div>{{ i18n.ts._serverSetupWizard._use.single_description }}</div> - </option> - <option value="group"> - <div><i class="ti ti-lock"></i> <b>{{ i18n.ts._serverSetupWizard._use.group }}</b></div> - <div>{{ i18n.ts._serverSetupWizard._use.group_description }}</div> - </option> - <option value="open"> - <div><i class="ti ti-world"></i> <b>{{ i18n.ts._serverSetupWizard._use.open }}</b></div> - <div>{{ i18n.ts._serverSetupWizard._use.open_description }}</div> - </option> + <MkRadios + v-model="q_use" + :options="[ + { value: 'single', label: i18n.ts._serverSetupWizard._use.single, icon: 'ti ti-user', caption: i18n.ts._serverSetupWizard._use.single_description }, + { value: 'group', label: i18n.ts._serverSetupWizard._use.group, icon: 'ti ti-lock', caption: i18n.ts._serverSetupWizard._use.group_description }, + { value: 'open', label: i18n.ts._serverSetupWizard._use.open, icon: 'ti ti-world', caption: i18n.ts._serverSetupWizard._use.open_description }, + ]" + vertical + > </MkRadios> <MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo> @@ -40,10 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-users"></i></template> <div class="_gaps_s"> - <MkRadios v-model="q_scale" :vertical="true"> - <option value="small"><i class="ti ti-user"></i> {{ i18n.ts._serverSetupWizard._scale.small }}</option> - <option value="medium"><i class="ti ti-users"></i> {{ i18n.ts._serverSetupWizard._scale.medium }}</option> - <option value="large"><i class="ti ti-users-group"></i> {{ i18n.ts._serverSetupWizard._scale.large }}</option> + <MkRadios + v-model="q_scale" + :options="[ + { value: 'small', label: i18n.ts._serverSetupWizard._scale.small, icon: 'ti ti-user' }, + { value: 'medium', label: i18n.ts._serverSetupWizard._scale.medium, icon: 'ti ti-users' }, + { value: 'large', label: i18n.ts._serverSetupWizard._scale.large, icon: 'ti ti-users-group' }, + ]" + vertical + > </MkRadios> <MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo> @@ -57,9 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_s"> <div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div> - <MkRadios v-model="q_federation" :vertical="true"> - <option value="yes">{{ i18n.ts.yes }}</option> - <option value="no">{{ i18n.ts.no }}</option> + <MkRadios + v-model="q_federation" + :options="[ + { value: 'yes', label: i18n.ts.yes }, + { value: 'no', label: i18n.ts.no }, + ]" + vertical + > </MkRadios> <MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo> @@ -212,9 +218,9 @@ const props = withDefaults(defineProps<{ }); const q_name = ref(''); -const q_use = ref('single'); -const q_scale = ref('small'); -const q_federation = ref('yes'); +const q_use = ref<'single' | 'group' | 'open'>('single'); +const q_scale = ref<'small' | 'medium' | 'large'>('small'); +const q_federation = ref<'yes' | 'no'>('no'); const q_remoteContentsCleaning = ref(true); const q_adminName = ref(''); const q_adminEmail = ref(''); diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 8ec48dcc3f..5e16460104 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -22,18 +22,26 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTextarea v-model="text"> <template #label>{{ i18n.ts.text }}</template> </MkTextarea> - <MkRadios v-model="icon"> + <MkRadios + v-model="icon" + :options="[ + { value: 'info', icon: 'ti ti-info-circle' }, + { value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' }, + { value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' }, + { value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' }, + ]" + > <template #label>{{ i18n.ts.icon }}</template> - <option value="info"><i class="ti ti-info-circle"></i></option> - <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option> - <option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option> - <option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option> </MkRadios> - <MkRadios v-model="display"> + <MkRadios + v-model="display" + :options="[ + { value: 'normal', label: i18n.ts.normal }, + { value: 'banner', label: i18n.ts.banner }, + { value: 'dialog', label: i18n.ts.dialog }, + ]" + > <template #label>{{ i18n.ts.display }}</template> - <option value="normal">{{ i18n.ts.normal }}</option> - <option value="banner">{{ i18n.ts.banner }}</option> - <option value="dialog">{{ i18n.ts.dialog }}</option> </MkRadios> <MkSwitch v-model="needConfirmationToRead"> {{ i18n.ts._announcement.needConfirmationToRead }} |