@@ -3,99 +3,128 @@ SPDX-FileCopyrightText: syuilo and misskey-project
33SPDX-License-Identifier: AGPL-3.0-only
44-->
55
6+ <template >
7+ <div :class =" { [$style.vertical]: vertical }" >
8+ <div :class =" $style.label" >
9+ <slot name =" label" ></slot >
10+ </div >
11+ <div :class =" $style.body" >
12+ <MkRadio
13+ v-for =" option in options"
14+ :key =" getKey(option.value)"
15+ v-model =" model"
16+ :disabled =" option.disabled"
17+ :value =" option.value"
18+ >
19+ <div :class =" [$style.optionContent, { [$style.checked]: model === option.value }]" >
20+ <i v-if =" option.icon" :class =" [$style.optionIcon, option.icon]" :style =" option.iconStyle" ></i >
21+ <div >
22+ <slot v-if =" option.slotId != null" :name =" `option-${option.slotId as SlotNames}`" ></slot >
23+ <template v-else >
24+ <div :style =" option.labelStyle" >{{ option.label ?? option.value }}</div >
25+ <div v-if =" option.caption" :class =" $style.optionCaption" >{{ option.caption }}</div >
26+ </template >
27+ </div >
28+ </div >
29+ </MkRadio >
30+ </div >
31+ <div :class =" $style.caption" >
32+ <slot name =" caption" ></slot >
33+ </div >
34+ </div >
35+ </template >
36+
637<script lang="ts">
7- import { Comment , defineComponent , h , ref , watch } from ' vue' ;
38+ import type { StyleValue } from ' vue' ;
39+ import type { OptionValue } from ' @/types/option-value.js' ;
40+
41+ export type MkRadiosOption <T = OptionValue , S = string > = {
42+ value: T ;
43+ slotId? : S ;
44+ label? : string ;
45+ labelStyle? : StyleValue ;
46+ icon? : string ;
47+ iconStyle? : StyleValue ;
48+ caption? : string ;
49+ disabled? : boolean ;
50+ };
51+ </script >
52+
53+ <script setup lang="ts" generic =" const T extends MkRadiosOption " >
854import MkRadio from ' ./MkRadio.vue' ;
9- import type { VNode } from ' vue' ;
10-
11- export default defineComponent ({
12- props: {
13- modelValue: {
14- required: false ,
15- },
16- vertical: {
17- type: Boolean ,
18- default: false ,
19- },
20- },
21- setup(props , context ) {
22- const value = ref (props .modelValue );
23- watch (value , () => {
24- context .emit (' update:modelValue' , value .value );
25- });
26- watch (() => props .modelValue , v => {
27- value .value = v ;
28- });
29- if (! context .slots .default ) return null ;
30- let options = context .slots .default ();
31- const label = context .slots .label && context .slots .label ();
32- const caption = context .slots .caption && context .slots .caption ();
33-
34- // なぜかFragmentになることがあるため
35- if (options .length === 1 && options [0 ].props == null ) options = options [0 ].children as VNode [];
36-
37- // vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
38- options = options .filter (vnode => vnode .type !== Comment );
39-
40- return () => h (' div' , {
41- class: [
42- ' novjtcto' ,
43- ... (props .vertical ? [' vertical' ] : []),
44- ],
45- }, [
46- ... (label ? [h (' div' , {
47- class: ' label' ,
48- }, label )] : []),
49- h (' div' , {
50- class: ' body' ,
51- }, options .map (option => h (MkRadio , {
52- key: option .key as string ,
53- value: option .props ?.value ,
54- disabled: option .props ?.disabled ,
55- modelValue: value .value ,
56- ' onUpdate:modelValue' : _v => value .value = _v ,
57- }, () => option .children )),
58- ),
59- ... (caption ? [h (' div' , {
60- class: ' caption' ,
61- }, caption )] : []),
62- ]);
63- },
64- });
55+
56+ defineProps <{
57+ options: T [];
58+ vertical? : boolean ;
59+ }>();
60+
61+ type SlotNames = NonNullable <T extends MkRadiosOption <any , infer U > ? U : never >;
62+
63+ defineSlots <{
64+ label? : () => any ;
65+ caption? : () => any ;
66+ } & {
67+ [K in ` option-${SlotNames } ` ]: () => any ;
68+ }>();
69+
70+ const model = defineModel <T [' value' ]>({ required: true });
71+
72+ function getKey(value : OptionValue ): PropertyKey {
73+ if (value === null ) return ' null' ;
74+ return value ;
75+ }
6576 </script >
6677
67- <style lang="scss">
68- .novjtcto {
69- > .label {
70- font-size : 0.85em ;
71- padding : 0 0 8px 0 ;
72- user-select : none ;
78+ <style lang="scss" module>
79+ .label {
80+ font-size : 0.85em ;
81+ padding : 0 0 8px 0 ;
82+ user-select : none ;
7383
74- & :empty {
75- display : none ;
76- }
84+ & :empty {
85+ display : none ;
7786 }
87+ }
7888
79- > .body {
80- display : flex ;
81- gap : 10px ;
82- flex-wrap : wrap ;
83- }
89+ .body {
90+ display : flex ;
91+ gap : 10px ;
92+ flex-wrap : wrap ;
93+ }
8494
85- > .caption {
86- font-size : 0.85em ;
87- padding : 8px 0 0 0 ;
88- color : color (from var (--MI_THEME-fg ) srgb r g b / 0.75 );
95+ .caption {
96+ font-size : 0.85em ;
97+ padding : 8px 0 0 0 ;
98+ color : color (from var (--MI_THEME-fg ) srgb r g b / 0.75 );
8999
90- & :empty {
91- display : none ;
92- }
100+ & :empty {
101+ display : none ;
93102 }
103+ }
94104
95- & .vertical {
96- > .body {
97- flex-direction : column ;
98- }
105+ .optionContent {
106+ display : flex ;
107+ align-items : center ;
108+ gap : 6px ;
109+ }
110+
111+ .optionCaption {
112+ font-size : 0.85em ;
113+ padding : 2px 0 0 0 ;
114+ color : color (from var (--MI_THEME-fg ) srgb r g b / 0.75 );
115+ }
116+
117+ .optionContent.checked {
118+ .optionCaption {
119+ color : color (from var (--MI_THEME-accent ) srgb r g b / 0.75 );
99120 }
100121}
122+
123+ .optionIcon {
124+ flex-shrink : 0 ;
125+ }
126+
127+ .vertical > .body {
128+ flex-direction : column ;
129+ }
101130 </style >
0 commit comments