feat(stage-ui): new Combobox component
This commit is contained in:
@@ -1,15 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { FieldCheckbox, FieldSelect, Option, Select } from '@proj-airi/ui'
|
||||
import { FieldCheckbox, FieldSelect } from '@proj-airi/ui'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const settings = useSettings()
|
||||
|
||||
const { t, messages } = useI18n()
|
||||
const { allowVisibleOnAllWorkspaces } = storeToRefs(settings)
|
||||
|
||||
const dark = useDark()
|
||||
|
||||
const languages = computed(() => {
|
||||
return Object.keys(messages.value).map(lang => ({
|
||||
label: t(`settings.language.${lang}`),
|
||||
value: lang,
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,6 +48,7 @@ const dark = useDark()
|
||||
|
||||
<!-- Language Setting -->
|
||||
<FieldSelect
|
||||
v-model="settings.language"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
@@ -47,27 +57,8 @@ const dark = useDark()
|
||||
transition="all ease-in-out duration-250"
|
||||
:label="t('settings.language.title')"
|
||||
:description="t('settings.language.description')"
|
||||
>
|
||||
<Select
|
||||
v-model="settings.language"
|
||||
transition="all ease-in-out duration-250"
|
||||
cursor-pointer bg-transparent outline-none
|
||||
>
|
||||
<template #default="{ value }">
|
||||
<div>
|
||||
{{ value ? $t(`settings.language.${value}`) : t('settings.language.english') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #options="{ hide }">
|
||||
<Option value="en" :active="locale === 'en'" @click="hide()">
|
||||
{{ $t('settings.language.english') }}
|
||||
</Option>
|
||||
<Option value="zh-Hans" :active="locale === 'zh-Hans'" @click="hide()">
|
||||
{{ $t('settings.language.chinese') }}
|
||||
</Option>
|
||||
</template>
|
||||
</Select>
|
||||
</FieldSelect>
|
||||
:options="languages"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
|
||||
@@ -55,6 +55,12 @@ onMounted(async () => {
|
||||
if (!region.value) {
|
||||
region.value = 'eastasia' // Default region
|
||||
}
|
||||
if (!providers.value[providerId]?.region) {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = { region: region.value }
|
||||
else
|
||||
providers.value[providerId].region = region.value
|
||||
}
|
||||
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
})
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { FieldCheckbox, FieldSelect, Option, Select } from '@proj-airi/ui'
|
||||
import { FieldCheckbox, FieldSelect } from '@proj-airi/ui'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const settings = useSettings()
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { t, messages } = useI18n()
|
||||
const dark = useDark()
|
||||
|
||||
const languages = computed(() => {
|
||||
return Object.keys(messages.value).map(lang => ({
|
||||
label: t(`settings.language.options.${lang}`),
|
||||
value: lang,
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,6 +34,7 @@ const dark = useDark()
|
||||
|
||||
<!-- Language Setting -->
|
||||
<FieldSelect
|
||||
v-model="settings.language"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
@@ -34,27 +43,8 @@ const dark = useDark()
|
||||
transition="all ease-in-out duration-250"
|
||||
:label="t('settings.language.title')"
|
||||
:description="t('settings.language.description')"
|
||||
>
|
||||
<Select
|
||||
v-model="settings.language"
|
||||
transition="all ease-in-out duration-250"
|
||||
cursor-pointer bg-transparent outline-none
|
||||
>
|
||||
<template #default="{ value }">
|
||||
<div>
|
||||
{{ value ? $t(`settings.language.${value}`) : t('settings.language.english') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #options="{ hide }">
|
||||
<Option value="en" :active="locale === 'en'" @click="hide()">
|
||||
{{ $t('settings.language.english') }}
|
||||
</Option>
|
||||
<Option value="zh-Hans" :active="locale === 'zh-Hans'" @click="hide()">
|
||||
{{ $t('settings.language.chinese') }}
|
||||
</Option>
|
||||
</template>
|
||||
</Select>
|
||||
</FieldSelect>
|
||||
:options="languages"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
|
||||
@@ -55,6 +55,12 @@ onMounted(async () => {
|
||||
if (!region.value) {
|
||||
region.value = 'eastasia' // Default region
|
||||
}
|
||||
if (!providers.value[providerId]?.region) {
|
||||
if (!providers.value[providerId])
|
||||
providers.value[providerId] = { region: region.value }
|
||||
else
|
||||
providers.value[providerId].region = region.value
|
||||
}
|
||||
|
||||
await speechStore.loadVoicesForProvider(providerId)
|
||||
})
|
||||
|
||||
@@ -253,6 +253,12 @@ watch(dark, updateDropShadowFilter, { immediate: true })
|
||||
watch([model, themeColorsHue], updateDropShadowFilter)
|
||||
watch(offset, setScaleAndPosition)
|
||||
watch(() => props.scale, setScaleAndPosition)
|
||||
watch(modelFile, () => {
|
||||
if (modelFile.value) {
|
||||
loadingModel.value = true
|
||||
loadModel()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// TODO: This is hacky!
|
||||
function updateDropShadowFilterLoop() {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ComboboxAnchor,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxItemIndicator,
|
||||
ComboboxLabel,
|
||||
ComboboxRoot,
|
||||
ComboboxSeparator,
|
||||
ComboboxTrigger,
|
||||
ComboboxViewport,
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<{
|
||||
options: { groupLabel: string, children?: { label: string, value: any }[] }[]
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
function toDisplayValue(value: any): string {
|
||||
const option = props.options.flatMap(group => group.children).find(option => option?.value === value)
|
||||
return option ? option.label : props.placeholder || ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot
|
||||
class="relative w-full"
|
||||
>
|
||||
<ComboboxAnchor
|
||||
:class="[
|
||||
'min-w-[160px] w-full inline-flex items-center justify-between rounded-lg border px-[15px] leading-none h-[35px] gap-[5px] outline-none',
|
||||
'text-xs 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',
|
||||
'border-neutral-300 dark:border-neutral-800 border-solid border-2 focus:border-neutral-400 dark:focus:border-neutral-600',
|
||||
'shadow-sm focus:shadow-[0_0_0_2px] focus:shadow-black',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
]"
|
||||
>
|
||||
<ComboboxInput
|
||||
:class="[
|
||||
'!bg-transparent outline-none h-full selection:bg-grass5 placeholder-stone-400',
|
||||
'text-neutral-700 dark:text-neutral-200',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
]"
|
||||
:placeholder="props.placeholder"
|
||||
:display-value="(val) => toDisplayValue(val)"
|
||||
/>
|
||||
<ComboboxTrigger>
|
||||
<div
|
||||
i-solar:alt-arrow-down-linear
|
||||
:class="[
|
||||
'h-4 w-4',
|
||||
'text-neutral-700 dark:text-neutral-200',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
]"
|
||||
/>
|
||||
</ComboboxTrigger>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<ComboboxContent
|
||||
:avoid-collisions="true"
|
||||
:class="[
|
||||
'absolute z-10 w-full mt-1 min-w-[160px] overflow-hidden rounded-lg shadow-sm border will-change-[opacity,transform] max-h-50dvh',
|
||||
'data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade',
|
||||
'bg-white dark:bg-neutral-900',
|
||||
'border-neutral-300 dark:border-neutral-800 border-solid border-2 focus:border-neutral-400 dark:focus:border-neutral-600',
|
||||
]"
|
||||
>
|
||||
<ComboboxViewport class="p-[2px]">
|
||||
<ComboboxEmpty
|
||||
:class="[
|
||||
'font-medium py-2',
|
||||
'text-xs text-center text-neutral-700 dark:text-neutral-200',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
]"
|
||||
/>
|
||||
|
||||
<template
|
||||
v-for="(group, index) in options"
|
||||
:key="group.name"
|
||||
>
|
||||
<ComboboxGroup>
|
||||
<ComboboxSeparator
|
||||
v-if="index !== 0"
|
||||
class="m-[5px] h-[1px] bg-neutral-400"
|
||||
/>
|
||||
|
||||
<ComboboxLabel
|
||||
:class="[
|
||||
'px-[25px] text-xs leading-[25px]',
|
||||
'text-neutral-500 dark:text-neutral-400',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
]"
|
||||
>
|
||||
{{ group.groupLabel }}
|
||||
</ComboboxLabel>
|
||||
|
||||
<ComboboxItem
|
||||
v-for="option in group.children"
|
||||
:key="option.label"
|
||||
:text-value="option.label"
|
||||
:value="option.value"
|
||||
:class="[
|
||||
'leading-none rounded-[5px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none',
|
||||
'data-[highlighted]:bg-neutral-100 dark:data-[highlighted]:bg-neutral-800',
|
||||
'text-xs 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',
|
||||
]"
|
||||
>
|
||||
<ComboboxItemIndicator
|
||||
class="absolute left-0 w-[25px] inline-flex items-center justify-center opacity-30"
|
||||
>
|
||||
<div i-solar:check-circle-line-duotone />
|
||||
</ComboboxItemIndicator>
|
||||
<span>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
</ComboboxItem>
|
||||
</ComboboxGroup>
|
||||
</template>
|
||||
</ComboboxViewport>
|
||||
</ComboboxContent>
|
||||
</ComboboxRoot>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Combobox } from './Combobox.vue'
|
||||
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { Select } from '@proj-airi/ui'
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
label: string
|
||||
description?: string
|
||||
options?: { label: string, value: string | number }[]
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
layout?: 'horizontal' | 'vertical'
|
||||
}>()
|
||||
selectClass?: string | string[]
|
||||
}>(), {
|
||||
layout: 'horizontal',
|
||||
})
|
||||
|
||||
const modelValue = defineModel<string>({ required: false })
|
||||
</script>
|
||||
@@ -39,6 +42,7 @@ const modelValue = defineModel<string>({ required: false })
|
||||
:placeholder="props.placeholder"
|
||||
:disabled="props.disabled"
|
||||
:title="label"
|
||||
:class="props.selectClass"
|
||||
>
|
||||
<template #default="{ value }">
|
||||
{{ props.options?.find(option => option.value === value)?.label || props.placeholder }}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Dropdown as VDropdown } from 'floating-vue'
|
||||
import { provide, ref } from 'vue'
|
||||
|
||||
import UIOption from './Option.vue'
|
||||
import Combobox from '../Combobox/Combobox.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
options?: { label: string, value: string | number }[]
|
||||
@@ -28,94 +27,5 @@ provide('hide', handleHide)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDropdown
|
||||
auto-size
|
||||
auto-boundary-max-size
|
||||
w-full
|
||||
>
|
||||
<div
|
||||
min-w="[160px]" p="2.5" w-full
|
||||
class="focus:ring-2 focus:ring-black/10"
|
||||
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
|
||||
text="xs sm:sm dark:neutral-200 disabled:neutral-400 dark:disabled:neutral-600 neutral-700"
|
||||
bg="white dark:neutral-900 disabled:neutral-100 hover:neutral-50 dark:disabled:neutral-900 dark:hover:neutral-800 "
|
||||
cursor="disabled:not-allowed pointer"
|
||||
flex items-center gap-2 rounded-lg shadow-sm outline-none transition-colors duration-200 ease-in-out
|
||||
:class="[
|
||||
props.disabled ? 'pointer-events-none' : '',
|
||||
]"
|
||||
>
|
||||
<div class="flex-1 truncate">
|
||||
<slot :value="modelValue">
|
||||
{{ props.options?.find(item => item.value === modelValue)?.label || modelValue }}
|
||||
</slot>
|
||||
</div>
|
||||
<div i-solar:alt-arrow-down-linear class="h-3.5 w-3.5 text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
|
||||
<template #popper="{ hide }">
|
||||
<div class="min-w-[160px] flex flex-col gap-0.5 border border-neutral-200 rounded-lg bg-white p-1 shadow-lg dark:border-neutral-800 dark:bg-neutral-900 dark:bg-neutral-900">
|
||||
<slot name="options" :hide="hide">
|
||||
<template v-if="props.options && props.options.length">
|
||||
<UIOption
|
||||
v-for="option of props.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:label="option.label"
|
||||
:active="modelValue === option.value"
|
||||
@click="selectOption(option.value); hide()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<p v-else class="my-3 text-center text-neutral-500 dark:text-neutral-400">
|
||||
No data
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
</VDropdown>
|
||||
<Combobox v-model="modelValue" :options="[{ groupLabel: '', children: props.options }]" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.resize-observer[data-v-b329ee4c] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.resize-observer[data-v-b329ee4c] object {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.v-popper__popper {
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.v-popper__arrow-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-popper__inner {
|
||||
border: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './Checkbox'
|
||||
export * from './Combobox'
|
||||
export * from './Field'
|
||||
export * from './Input'
|
||||
export * from './Radio'
|
||||
|
||||
@@ -189,6 +189,32 @@ export function sharedUnoConfig() {
|
||||
}
|
||||
}],
|
||||
],
|
||||
theme: {
|
||||
/**
|
||||
* https://github.com/unocss/unocss/blob/1031312057a3bea1082b7d938eb2ad640f57613a/packages-presets/preset-wind4/src/theme/animate.ts
|
||||
* https://unocss.dev/presets/wind4#transformdirectives
|
||||
*/
|
||||
animation: {
|
||||
keyframes: {
|
||||
slideUpAndFade: '{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}',
|
||||
slideRightAndFade: '{from{opacity:0;transform:translateX(-2px)}to{opacity:1;transform:translateX(0)}}',
|
||||
slideDownAndFade: '{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}',
|
||||
slideLeftAndFade: '{from{opacity:0;transform:translateX(2px)}to{opacity:1;transform:translateX(0)}}',
|
||||
},
|
||||
durations: {
|
||||
slideUpAndFade: '400ms',
|
||||
slideRightAndFade: '400ms',
|
||||
slideDownAndFade: '400ms',
|
||||
slideLeftAndFade: '400ms',
|
||||
},
|
||||
timingFns: {
|
||||
slideUpAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideRightAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideDownAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideLeftAndFade: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user