summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2026-01-14 14:02:50 +0900
committerGitHub <noreply@github.com>2026-01-14 14:02:50 +0900
commitb941c896aa5512240de9121a1850d55aa5f8b68b (patch)
tree5d96055387b458f5295d791cc00fd5abf14b1752 /packages/frontend/src/components
parentBump version to 2026.1.0-beta.0 (diff)
downloadmisskey-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.vue3
-rw-r--r--packages/frontend/src/components/MkForm.vue14
-rw-r--r--packages/frontend/src/components/MkImageEffectorFxForm.vue6
-rw-r--r--packages/frontend/src/components/MkMenu.vue15
-rw-r--r--packages/frontend/src/components/MkRadio.vue6
-rw-r--r--packages/frontend/src/components/MkRadios.vue185
-rw-r--r--packages/frontend/src/components/MkSelect.vue2
-rw-r--r--packages/frontend/src/components/MkServerSetupWizard.vue52
-rw-r--r--packages/frontend/src/components/MkUserAnnouncementEditDialog.vue26
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 }}