refactor(stage-ui): live2d model settings with standard components
This commit is contained in:
@@ -3,9 +3,9 @@ import type { ModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
import { defaultModelParameters, useLive2d } from '@proj-airi/stage-ui-live2d'
|
||||
import { OPFSCache } from '@proj-airi/stage-ui-live2d/utils/opfs-loader'
|
||||
import { Button, Checkbox, FieldRange, SelectTab } from '@proj-airi/ui'
|
||||
import { Button, Checkbox, FieldCheckbox, FieldCombobox, FieldRange, SelectTab } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSettings } from '../../../../stores/settings'
|
||||
@@ -44,44 +44,36 @@ const {
|
||||
} = storeToRefs(live2d)
|
||||
|
||||
const selectedRuntimeMotion = ref<string>('')
|
||||
const selectedRuntimeMotionName = ref<string>('')
|
||||
const runtimeMotions = ref<Array<{ name: string, fullPath: string, displayPath: string, group: string, index: number }>>([])
|
||||
const showMotionSelector = ref(false)
|
||||
const runtimeMotions = ref<Array<{ name: string, displayPath: string, group: string, index: number }>>([])
|
||||
const canExtractColors = computed(() => props.runtimeSnapshot.canCapturePreview)
|
||||
const runtimeMotionOptions = computed(() => runtimeMotions.value.map(motion => ({
|
||||
label: motion.name,
|
||||
value: motion.displayPath,
|
||||
description: motion.displayPath,
|
||||
})))
|
||||
const fpsOptions = computed(() => [
|
||||
{ value: 0, label: t('settings.live2d.fps.options.unlimited') },
|
||||
{ value: 60, label: '60' },
|
||||
{ value: 30, label: '30' },
|
||||
])
|
||||
|
||||
// Get available runtime motions from the model
|
||||
watch(() => live2d.availableMotions, (motions) => {
|
||||
runtimeMotions.value = motions.map(m => ({
|
||||
name: m.fileName.split('/').pop() || m.fileName,
|
||||
displayPath: m.fileName,
|
||||
group: m.motionName,
|
||||
index: m.motionIndex,
|
||||
}))
|
||||
|
||||
console.info('Available motions:', runtimeMotions.value)
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
// Listen for available motions updates
|
||||
watch(() => live2d.availableMotions, (motions) => {
|
||||
// Show all motions with their full paths
|
||||
runtimeMotions.value = motions.map(m => ({
|
||||
name: m.fileName.split('/').pop() || m.fileName,
|
||||
fullPath: m.fileName, // Full path like "hiyori_free_zh/runtime/motions/idle.motion3.json"
|
||||
displayPath: m.fileName, // Show full path for clarity
|
||||
group: m.motionName,
|
||||
index: m.motionIndex,
|
||||
}))
|
||||
|
||||
console.info('Available motions:', runtimeMotions.value)
|
||||
}, { immediate: true })
|
||||
|
||||
// Restore selected motion
|
||||
const savedPath = localStorage.getItem('selected-runtime-motion')
|
||||
const savedName = localStorage.getItem('selected-runtime-motion-name')
|
||||
if (savedPath) {
|
||||
selectedRuntimeMotion.value = savedPath
|
||||
}
|
||||
if (savedName) {
|
||||
selectedRuntimeMotionName.value = savedName
|
||||
}
|
||||
|
||||
// Add click outside handler
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
// Function to reset all parameters to default values
|
||||
@@ -101,12 +93,17 @@ async function clearModelCache() {
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime motion selection handlers
|
||||
function handleMotionSelect(motion: any) {
|
||||
selectedRuntimeMotion.value = motion.displayPath // Store full path
|
||||
selectedRuntimeMotionName.value = motion.name // Store just the filename for display
|
||||
function handleMotionSelect(selectedMotionPath: string | number | undefined) {
|
||||
if (typeof selectedMotionPath !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
const motion = runtimeMotions.value.find(item => item.displayPath === selectedMotionPath)
|
||||
if (!motion) {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('selected-runtime-motion', motion.displayPath)
|
||||
localStorage.setItem('selected-runtime-motion-name', motion.name)
|
||||
localStorage.setItem('selected-runtime-motion-group', motion.group)
|
||||
localStorage.setItem('selected-runtime-motion-index', motion.index.toString())
|
||||
|
||||
@@ -116,29 +113,11 @@ function handleMotionSelect(motion: any) {
|
||||
// Set the current motion to the selected runtime motion
|
||||
currentMotion.value = { group: motion.group, index: motion.index }
|
||||
|
||||
showMotionSelector.value = false
|
||||
|
||||
console.info('✅ Selected runtime motion:', motion.name)
|
||||
console.info('Selected runtime motion:', motion.name)
|
||||
console.info('Full path:', motion.displayPath)
|
||||
console.info('Group:', motion.group, 'Index:', motion.index)
|
||||
}
|
||||
|
||||
function toggleMotionSelector() {
|
||||
showMotionSelector.value = !showMotionSelector.value
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('[data-motion-selector]')) {
|
||||
showMotionSelector.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
// async function patchMotionMap(source: File, motionMap: Record<string, string>): Promise<File> {
|
||||
// if (!Object.keys(motionMap).length)
|
||||
// return source
|
||||
@@ -296,61 +275,25 @@ onUnmounted(() => {
|
||||
size="sm"
|
||||
:expand="false"
|
||||
>
|
||||
<div :class="['flex', 'items-center', 'justify-between']">
|
||||
<span :class="['text-sm', 'text-neutral-600', 'dark:text-neutral-400']">
|
||||
{{ t('settings.live2d.focus.button-disable.title') }}
|
||||
</span>
|
||||
<Checkbox v-model="live2dDisableFocus" />
|
||||
</div>
|
||||
<FieldCheckbox
|
||||
v-model="live2dDisableFocus"
|
||||
:label="t('settings.live2d.focus.button-disable.title')"
|
||||
placement="right"
|
||||
/>
|
||||
|
||||
<div flex items-center justify-between>
|
||||
<span text-sm text-neutral-600 dark:text-neutral-400>Idle Animation</span>
|
||||
<div data-motion-selector relative flex flex-col items-end gap-1>
|
||||
<button
|
||||
|
||||
:title="selectedRuntimeMotion"
|
||||
flex items-center gap-2 border rounded bg-neutral-100 px-4 py-2 text-sm text-neutral-700 font-medium transition-colors dark:border-neutral-700 dark:bg-neutral-800 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-700
|
||||
@click="toggleMotionSelector"
|
||||
>
|
||||
<span max-w-32 truncate>{{ selectedRuntimeMotionName || 'Select Motion' }}</span>
|
||||
<div
|
||||
:class="showMotionSelector ? 'i-solar:alt-arrow-up-line-duotone' : 'i-solar:alt-arrow-down-line-duotone'"
|
||||
text-xs transition-transform
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div
|
||||
v-if="showMotionSelector"
|
||||
|
||||
bg="white dark:neutral-800"
|
||||
border="1 neutral-200 dark:neutral-700"
|
||||
absolute right-0 top-10 z-50 max-h-80 min-w-64 overflow-y-auto rounded-lg shadow-lg
|
||||
>
|
||||
<div v-if="runtimeMotions.length === 0" p-4 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
No motions available
|
||||
</div>
|
||||
<button
|
||||
v-for="motion in runtimeMotions"
|
||||
:key="motion.fullPath"
|
||||
w-full px-4 py-2.5 text-left
|
||||
hover:bg="neutral-100 dark:neutral-700"
|
||||
transition-colors
|
||||
:class="{
|
||||
'bg-neutral-100 dark:bg-neutral-700': selectedRuntimeMotion === motion.displayPath,
|
||||
}"
|
||||
@click="handleMotionSelect(motion)"
|
||||
>
|
||||
<div text-sm text-neutral-900 font-medium dark:text-neutral-100>
|
||||
{{ motion.name }}
|
||||
</div>
|
||||
<div truncate text-xs text-neutral-500 dark:text-neutral-400>
|
||||
{{ motion.displayPath }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FieldCombobox
|
||||
v-model="selectedRuntimeMotion"
|
||||
label="Idle Animation"
|
||||
:options="runtimeMotionOptions"
|
||||
placeholder="Select Motion"
|
||||
:select-class="['w-full']"
|
||||
:content-min-width="256"
|
||||
@update:model-value="handleMotionSelect"
|
||||
>
|
||||
<template #empty>
|
||||
No motions available
|
||||
</template>
|
||||
</FieldCombobox>
|
||||
|
||||
<div :class="['mt-4', 'flex', 'items-center', 'justify-between']">
|
||||
<div :class="['flex', 'flex-col', 'gap-1']">
|
||||
@@ -379,21 +322,23 @@ onUnmounted(() => {
|
||||
<Checkbox v-model="live2dShadowEnabled" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
mt-4 w-full border rounded bg-neutral-100 px-4 py-2 text-sm text-neutral-700 font-medium transition-colors dark:border-neutral-700 dark:bg-neutral-800 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-700
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="mt-4 w-full"
|
||||
@click="resetToDefaultParameters"
|
||||
>
|
||||
Reset To Default Parameters
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
mt-2 w-full border rounded bg-neutral-100 px-4 py-2 text-sm text-neutral-700 font-medium transition-colors dark:border-neutral-700 dark:bg-neutral-800 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-700
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="mt-2 w-full"
|
||||
:disabled="clearingCache"
|
||||
:loading="clearingCache"
|
||||
@click="clearModelCache"
|
||||
>
|
||||
{{ clearingCache ? 'Clearing...' : 'Clear Model Cache' }}
|
||||
</button>
|
||||
Clear Model Cache
|
||||
</Button>
|
||||
|
||||
<!-- Head Rotation -->
|
||||
<div mb-2 mt-4 text-xs text-neutral-500 font-semibold dark:text-neutral-400>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { provide, ref } from 'vue'
|
||||
|
||||
import { Combobox } from '../combobox'
|
||||
|
||||
const props = defineProps<{
|
||||
options?: { label: string, value: string | number }[]
|
||||
options?: {
|
||||
label: string
|
||||
value: string | number
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
icon?: string
|
||||
}[]
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
title?: string
|
||||
@@ -13,27 +17,33 @@ const props = defineProps<{
|
||||
contentWidth?: string | number
|
||||
}>()
|
||||
|
||||
const show = ref(false)
|
||||
const modelValue = defineModel<string | number>({ required: false })
|
||||
|
||||
function selectOption(value: string | number) {
|
||||
modelValue.value = value
|
||||
}
|
||||
|
||||
function handleHide() {
|
||||
show.value = false
|
||||
}
|
||||
|
||||
provide('selectOption', selectOption)
|
||||
provide('hide', handleHide)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Combobox
|
||||
v-model="modelValue"
|
||||
:default-value="modelValue"
|
||||
:options="[{ groupLabel: '', children: props.options }]"
|
||||
:disabled="props.disabled"
|
||||
:content-min-width="props.contentMinWidth"
|
||||
:content-width="props.contentWidth"
|
||||
/>
|
||||
:placeholder="props.placeholder"
|
||||
>
|
||||
<template
|
||||
v-if="$slots.option"
|
||||
#option="{ option }"
|
||||
>
|
||||
<slot
|
||||
name="option"
|
||||
v-bind="{ option }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="$slots.empty"
|
||||
#empty
|
||||
>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
@@ -16,19 +16,58 @@ import {
|
||||
ComboboxTrigger,
|
||||
ComboboxViewport,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
options: { groupLabel: string, children?: { label: string, value: T }[] }[]
|
||||
interface ComboboxOptionItem<T extends AcceptableValue> {
|
||||
label: string
|
||||
value: T
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
icon?: string
|
||||
}
|
||||
|
||||
interface ComboboxOptionGroupItem<T extends AcceptableValue> {
|
||||
groupLabel?: string
|
||||
children?: ComboboxOptionItem<T>[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
options: ComboboxOptionItem<T>[] | ComboboxOptionGroupItem<T>[]
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
contentMinWidth?: string | number
|
||||
contentWidth?: string | number
|
||||
}>()
|
||||
}>(), {
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const modelValue = defineModel<T>({ required: false })
|
||||
|
||||
const normalizedOptions = computed<ComboboxOptionGroupItem<T>[]>(() => {
|
||||
if (!props.options.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const [firstOption] = props.options
|
||||
if ('value' in firstOption) {
|
||||
return [
|
||||
{
|
||||
groupLabel: '',
|
||||
children: props.options as ComboboxOptionItem<T>[],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return props.options as ComboboxOptionGroupItem<T>[]
|
||||
})
|
||||
|
||||
const flattenedOptions = computed<ComboboxOptionItem<T>[]>(() =>
|
||||
normalizedOptions.value.flatMap(group => group.children ?? []),
|
||||
)
|
||||
|
||||
function toDisplayValue(value: T): string {
|
||||
const option = props.options.flatMap(group => group.children).find(option => option?.value === value)
|
||||
return option ? option.label : props.placeholder || ''
|
||||
const option = flattenedOptions.value.find(option => option.value === value)
|
||||
return option?.label ?? props.placeholder ?? ''
|
||||
}
|
||||
|
||||
function toCssSize(value?: string | number): string | undefined {
|
||||
@@ -41,15 +80,20 @@ function toCssSize(value?: string | number): string | undefined {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot v-model="modelValue" :class="['relative', 'w-full', 'h-fit']">
|
||||
<ComboboxRoot
|
||||
v-model="modelValue"
|
||||
:disabled="props.disabled"
|
||||
:class="['relative', 'w-full', 'h-fit']"
|
||||
>
|
||||
<ComboboxAnchor
|
||||
:class="[
|
||||
'w-full inline-flex items-center justify-between rounded-xl border px-3 leading-none h-9 gap-[5px] outline-none',
|
||||
'text-sm text-neutral-700 dark:text-neutral-200 data-[placeholder]:text-neutral-200',
|
||||
'bg-white dark:bg-neutral-900 disabled:bg-neutral-100 hover:bg-neutral-50 dark:disabled:bg-neutral-900 dark:hover:bg-neutral-700',
|
||||
'bg-white dark:bg-neutral-900 hover:bg-neutral-50 dark:hover:bg-neutral-700',
|
||||
'border-neutral-200 dark:border-neutral-800 border-solid border-2 focus:border-primary-300 dark:focus:border-primary-400/50',
|
||||
'shadow-sm focus:shadow-[0_0_0_2px] focus:shadow-black',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
props.disabled ? 'cursor-not-allowed bg-neutral-100 opacity-60 dark:bg-neutral-900' : 'cursor-pointer',
|
||||
]"
|
||||
>
|
||||
<ComboboxInput
|
||||
@@ -58,6 +102,7 @@ function toCssSize(value?: string | number): string | undefined {
|
||||
'text-neutral-700 dark:text-neutral-200',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
]"
|
||||
:disabled="props.disabled"
|
||||
:placeholder="props.placeholder"
|
||||
:display-value="(val) => toDisplayValue(val)"
|
||||
/>
|
||||
@@ -103,19 +148,22 @@ function toCssSize(value?: string | number): string | undefined {
|
||||
'text-xs text-neutral-700 dark:text-neutral-200',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
]"
|
||||
/>
|
||||
>
|
||||
<slot name="empty" />
|
||||
</ComboboxEmpty>
|
||||
|
||||
<template
|
||||
v-for="(group, index) in options"
|
||||
:key="group.groupLabel"
|
||||
v-for="(group, groupIndex) in normalizedOptions"
|
||||
:key="group.groupLabel || `group-${groupIndex}`"
|
||||
>
|
||||
<ComboboxGroup :class="['overflow-x-hidden']">
|
||||
<ComboboxSeparator
|
||||
v-if="index !== 0"
|
||||
v-if="groupIndex !== 0"
|
||||
:class="['m-[5px]', 'h-[1px]', 'bg-neutral-400']"
|
||||
/>
|
||||
|
||||
<ComboboxLabel
|
||||
v-if="group.groupLabel"
|
||||
:class="[
|
||||
'px-[25px] text-xs leading-[25px]',
|
||||
'text-neutral-500 dark:text-neutral-400',
|
||||
@@ -126,26 +174,70 @@ function toCssSize(value?: string | number): string | undefined {
|
||||
</ComboboxLabel>
|
||||
|
||||
<ComboboxItem
|
||||
v-for="option in group.children"
|
||||
:key="option.label"
|
||||
v-for="(option, optionIndex) in group.children || []"
|
||||
:key="`${group.groupLabel || groupIndex}-${option.label}-${optionIndex}`"
|
||||
:text-value="option.label"
|
||||
:value="option.value"
|
||||
:disabled="option.disabled"
|
||||
:class="[
|
||||
'leading-normal rounded-lg flex items-center h-8 pr-[0.5rem] pl-[1.5rem] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none',
|
||||
'leading-normal rounded-lg grid grid-cols-[1rem_minmax(0,1fr)] items-center gap-2 min-h-8 px-2 relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none',
|
||||
'data-[highlighted]:bg-neutral-100 dark:data-[highlighted]:bg-neutral-800',
|
||||
'text-sm text-neutral-700 dark:text-neutral-200 data-[disabled]:text-neutral-400 dark:data-[disabled]:text-neutral-600 data-[highlighted]:text-grass1',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
'cursor-pointer',
|
||||
option.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||
]"
|
||||
>
|
||||
<ComboboxItemIndicator
|
||||
:class="['absolute', 'left-0', 'w-[25px]', 'inline-flex', 'items-center', 'justify-center', 'opacity-30']"
|
||||
:class="[
|
||||
'col-start-1 row-start-1',
|
||||
'inline-flex items-center justify-center',
|
||||
'w-[1rem]',
|
||||
'opacity-30',
|
||||
'text-current',
|
||||
]"
|
||||
>
|
||||
<div i-solar:alt-arrow-right-outline />
|
||||
<div i-solar:alt-arrow-right-outline class="size-4" />
|
||||
</ComboboxItemIndicator>
|
||||
<span :class="['line-clamp-1', 'overflow-hidden', 'text-ellipsis', 'whitespace-nowrap']">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
|
||||
<div :class="['col-start-2', 'min-w-0', 'flex', 'items-center', 'gap-2', 'py-1']">
|
||||
<slot
|
||||
name="option"
|
||||
v-bind="{ option }"
|
||||
>
|
||||
<span
|
||||
v-if="option.icon"
|
||||
:class="[
|
||||
'size-4 shrink-0',
|
||||
'text-current',
|
||||
option.icon,
|
||||
]"
|
||||
/>
|
||||
|
||||
<div :class="['min-w-0', 'flex', 'flex-1', 'flex-col']">
|
||||
<span
|
||||
:class="[
|
||||
'line-clamp-1',
|
||||
'overflow-hidden',
|
||||
'text-ellipsis',
|
||||
'whitespace-nowrap',
|
||||
]"
|
||||
>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="option.description"
|
||||
:class="[
|
||||
'line-clamp-2',
|
||||
'text-xs',
|
||||
'text-neutral-500 dark:text-neutral-400',
|
||||
]"
|
||||
>
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</ComboboxItem>
|
||||
</ComboboxGroup>
|
||||
</template>
|
||||
|
||||
@@ -4,11 +4,19 @@ import { ComboboxSelect } from '../combobox-select'
|
||||
const props = withDefaults(defineProps<{
|
||||
label: string
|
||||
description?: string
|
||||
options?: { label: string, value: string | number }[]
|
||||
options?: {
|
||||
label: string
|
||||
value: string | number
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
icon?: string
|
||||
}[]
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
layout?: 'horizontal' | 'vertical'
|
||||
selectClass?: string | string[]
|
||||
contentMinWidth?: string | number
|
||||
contentWidth?: string | number
|
||||
}>(), {
|
||||
layout: 'horizontal',
|
||||
})
|
||||
@@ -27,7 +35,7 @@ const modelValue = defineModel<string>({ required: false })
|
||||
<div
|
||||
:class="[
|
||||
'w-full',
|
||||
props.layout === 'horizontal' ? 'col-span-3' : 'row-span-2',
|
||||
props.layout === 'horizontal' ? 'col-span-2' : 'row-span-2',
|
||||
]"
|
||||
>
|
||||
<div :class="['flex', 'items-center', 'gap-1', 'break-words', 'text-sm', 'font-medium', 'text-left']">
|
||||
@@ -47,16 +55,31 @@ const modelValue = defineModel<string>({ required: false })
|
||||
:options="props.options?.filter(option => option.label && option.value) || []"
|
||||
:placeholder="props.placeholder"
|
||||
:disabled="props.disabled"
|
||||
:content-min-width="props.contentMinWidth"
|
||||
:content-width="props.contentWidth"
|
||||
:title="label"
|
||||
:class="[
|
||||
...(props.selectClass
|
||||
? (typeof props.selectClass === 'string' ? [props.selectClass] : props.selectClass)
|
||||
: []),
|
||||
props.layout === 'horizontal' ? 'col-span-1' : 'row-span-2',
|
||||
props.layout === 'horizontal' ? 'col-span-2' : 'row-span-2',
|
||||
]"
|
||||
>
|
||||
<template #default="{ value }">
|
||||
{{ props.options?.find(option => option.value === value)?.label || props.placeholder }}
|
||||
<template
|
||||
v-if="$slots.option"
|
||||
#option="{ option }"
|
||||
>
|
||||
<slot
|
||||
name="option"
|
||||
v-bind="{ option }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="$slots.empty"
|
||||
#empty
|
||||
>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
</ComboboxSelect>
|
||||
</slot>
|
||||
|
||||
Reference in New Issue
Block a user