@proj-airi/ui 0.9.0-alpha.3 → 0.9.0-alpha.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,11 +58,12 @@ import { Button } from '@proj-airi/ui'
58
58
  * [TransitionVertical](src/components/Animations/TransitionVertical.vue)
59
59
  * [Form](src/components/Form)
60
60
  * [Checkbox](src/components/Form/Checkbox)
61
+ * [Select](src/components/Form/Select)
61
62
  * [Field](src/components/Form/Field)
62
63
  * [Input](src/components/Form/Input)
63
64
  * [Radio](src/components/Form/Radio)
64
65
  * [Range](src/components/Form/Range)
65
- * [Select](src/components/Form/Select)
66
+ * [ComboboxSelect](src/components/Form/Select)
66
67
  * [Textarea](src/components/Form/Textarea)
67
68
 
68
69
  ## License
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proj-airi/ui",
3
3
  "type": "module",
4
- "version": "0.9.0-alpha.3",
4
+ "version": "0.9.0-alpha.32",
5
5
  "description": "A collection of UI components that used by Project AIRI",
6
6
  "author": {
7
7
  "name": "Moeru AI Project AIRI Team",
@@ -24,12 +24,12 @@
24
24
  "dependencies": {
25
25
  "@vueuse/core": "^14.2.1",
26
26
  "floating-vue": "^5.2.2",
27
- "reka-ui": "^2.8.2",
28
- "vue": "^3.5.25"
27
+ "reka-ui": "^2.9.2",
28
+ "vue": "^3.5.30"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@vue-macros/volar": "3.0.0-beta.8",
32
- "vue-tsc": "^3.1.8"
32
+ "vue-tsc": "^3.2.6"
33
33
  },
34
34
  "scripts": {
35
35
  "typecheck": "vue-tsc --noEmit"
@@ -1,12 +1,17 @@
1
1
  <script setup lang="ts">
2
2
  import { SwitchRoot, SwitchThumb } from 'reka-ui'
3
3
 
4
+ const props = defineProps<{
5
+ disabled?: boolean
6
+ }>()
7
+
4
8
  const modelValue = defineModel<boolean>({ required: true })
5
9
  </script>
6
10
 
7
11
  <template>
8
12
  <SwitchRoot
9
13
  v-model="modelValue"
14
+ :disabled="props.disabled"
10
15
  :class="[
11
16
  'duration-250 ease-in-out',
12
17
  'focus-within:outline-none',
@@ -14,8 +19,11 @@ const modelValue = defineModel<boolean>({ required: true })
14
19
  'is-interacting',
15
20
  'border-neutral-300 dark:border-neutral-700 data-[state=checked]:border-primary-200 data-[state=unchecked]:border-neutral-300 focus-within:border-neutral-800',
16
21
  'data-[state=checked]:bg-primary-400 data-[state=unchecked]:bg-neutral-300 data-[state=checked]:dark:bg-primary-400/80 dark:data-[state=unchecked]:bg-neutral-800',
22
+ 'data-[state=unchecked]:disabled:bg-neutral-400 data-[state=unchecked]:disabled:dark:bg-neutral-400/80',
23
+ 'data-[state=checked]:disabled:bg-primary-400 data-[state=checked]:disabled:dark:bg-primary-400/80',
17
24
  'relative h-7 w-12.5 rounded-full',
18
25
  'shadow-sm focus-within:shadow-none',
26
+ props.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
19
27
  ]"
20
28
  >
21
29
  <SwitchThumb
@@ -20,6 +20,8 @@ import {
20
20
  const props = defineProps<{
21
21
  options: { groupLabel: string, children?: { label: string, value: T }[] }[]
22
22
  placeholder?: string
23
+ contentMinWidth?: string | number
24
+ contentWidth?: string | number
23
25
  }>()
24
26
 
25
27
  const modelValue = defineModel<T>({ required: false })
@@ -28,13 +30,21 @@ function toDisplayValue(value: T): string {
28
30
  const option = props.options.flatMap(group => group.children).find(option => option?.value === value)
29
31
  return option ? option.label : props.placeholder || ''
30
32
  }
33
+
34
+ function toCssSize(value?: string | number): string | undefined {
35
+ if (value == null) {
36
+ return undefined
37
+ }
38
+
39
+ return typeof value === 'number' ? `${value}px` : value
40
+ }
31
41
  </script>
32
42
 
33
43
  <template>
34
- <ComboboxRoot v-model="modelValue" :class="['relative', 'w-full']">
44
+ <ComboboxRoot v-model="modelValue" :class="['relative', 'w-full', 'h-fit']">
35
45
  <ComboboxAnchor
36
46
  :class="[
37
- 'w-full inline-flex items-center justify-between rounded-xl border px-3 leading-none h-10 gap-[5px] outline-none',
47
+ 'w-full inline-flex items-center justify-between rounded-xl border px-3 leading-none h-9 gap-[5px] outline-none',
38
48
  'text-sm text-neutral-700 dark:text-neutral-200 data-[placeholder]:text-neutral-200',
39
49
  'bg-white dark:bg-neutral-900 disabled:bg-neutral-100 hover:bg-neutral-50 dark:disabled:bg-neutral-900 dark:hover:bg-neutral-700',
40
50
  'border-neutral-200 dark:border-neutral-800 border-solid border-2 focus:border-primary-300 dark:focus:border-primary-400/50',
@@ -76,12 +86,15 @@ function toDisplayValue(value: T): string {
76
86
  // Dialog/Drawer are not hidden behind the overlay or dismissed unexpectedly.
77
87
  // Read more at: https://github.com/moeru-ai/airi/issues/1136
78
88
  'z-[10010]',
79
- 'w-full min-w-[160px] overflow-hidden rounded-xl shadow-sm border will-change-[opacity,transform]',
89
+ 'w-full overflow-hidden rounded-xl shadow-sm border will-change-[opacity,transform]',
80
90
  'data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade',
81
91
  'bg-white dark:bg-neutral-900',
82
92
  'border-neutral-200 dark:border-neutral-800 border-solid border-2 focus:border-neutral-300 dark:focus:border-neutral-600',
83
93
  ]"
84
- :style="{ width: 'var(--reka-combobox-trigger-width)' }"
94
+ :style="{
95
+ width: toCssSize(props.contentWidth) ?? 'var(--reka-combobox-trigger-width)',
96
+ minWidth: toCssSize(props.contentMinWidth) ?? '160px',
97
+ }"
85
98
  >
86
99
  <ComboboxViewport :class="['p-[2px]', 'max-h-50dvh', 'overflow-y-auto']">
87
100
  <ComboboxEmpty
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import { provide, ref } from 'vue'
3
+
4
+ import { Combobox } from '../combobox'
5
+
6
+ const props = defineProps<{
7
+ options?: { label: string, value: string | number }[]
8
+ placeholder?: string
9
+ disabled?: boolean
10
+ title?: string
11
+ layout?: 'horizontal' | 'vertical'
12
+ contentMinWidth?: string | number
13
+ contentWidth?: string | number
14
+ }>()
15
+
16
+ const show = ref(false)
17
+ const modelValue = defineModel<string | number>({ required: false })
18
+
19
+ function selectOption(value: string | number) {
20
+ modelValue.value = value
21
+ }
22
+
23
+ function handleHide() {
24
+ show.value = false
25
+ }
26
+
27
+ provide('selectOption', selectOption)
28
+ provide('hide', handleHide)
29
+ </script>
30
+
31
+ <template>
32
+ <Combobox
33
+ v-model="modelValue"
34
+ :default-value="modelValue"
35
+ :options="[{ groupLabel: '', children: props.options }]"
36
+ :content-min-width="props.contentMinWidth"
37
+ :content-width="props.contentWidth"
38
+ />
39
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as ComboboxOption } from './combobox-option.vue'
2
+ export { default as ComboboxSelect } from './combobox-select.vue'
@@ -4,6 +4,7 @@ import { Checkbox } from '../checkbox'
4
4
  const props = defineProps<{
5
5
  label?: string
6
6
  description?: string
7
+ disabled?: boolean
7
8
  }>()
8
9
 
9
10
  const modelValue = defineModel<boolean>({ required: true })
@@ -24,7 +25,7 @@ const modelValue = defineModel<boolean>({ required: true })
24
25
  </slot>
25
26
  </div>
26
27
  </div>
27
- <Checkbox v-model="modelValue" />
28
+ <Checkbox v-model="modelValue" :disabled="props.disabled" />
28
29
  </div>
29
30
  </label>
30
31
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { Select } from '../select'
2
+ import { ComboboxSelect } from '../combobox-select'
3
3
 
4
4
  const props = withDefaults(defineProps<{
5
5
  label: string
@@ -21,29 +21,28 @@ const modelValue = defineModel<string>({ required: false })
21
21
  <div
22
22
  :class="[
23
23
  'items-center',
24
- 'justify-center',
25
- props.layout === 'horizontal' ? 'grid grid-cols-3 gap-2' : 'grid grid-cols-2 gap-2',
24
+ props.layout === 'horizontal' ? 'grid grid-cols-4 gap-2' : 'grid grid-rows-2 gap-2',
26
25
  ]"
27
26
  >
28
27
  <div
29
28
  :class="[
30
29
  'w-full',
31
- props.layout === 'horizontal' ? 'col-span-2' : 'row-span-1',
30
+ props.layout === 'horizontal' ? 'col-span-3' : 'row-span-2',
32
31
  ]"
33
32
  >
34
- <div :class="['flex', 'items-center', 'gap-1', 'break-words', 'text-sm', 'font-medium']">
33
+ <div :class="['flex', 'items-center', 'gap-1', 'break-words', 'text-sm', 'font-medium', 'text-left']">
35
34
  <slot name="label">
36
35
  {{ props.label }}
37
36
  </slot>
38
37
  </div>
39
- <div :class="['break-words', 'text-xs', 'text-neutral-500', 'dark:text-neutral-400']">
38
+ <div :class="['break-words', 'text-xs', 'text-neutral-500', 'dark:text-neutral-400', 'text-left']">
40
39
  <slot name="description">
41
40
  {{ props.description }}
42
41
  </slot>
43
42
  </div>
44
43
  </div>
45
44
  <slot>
46
- <Select
45
+ <ComboboxSelect
47
46
  v-model="modelValue"
48
47
  :options="props.options?.filter(option => option.label && option.value) || []"
49
48
  :placeholder="props.placeholder"
@@ -59,7 +58,7 @@ const modelValue = defineModel<string>({ required: false })
59
58
  <template #default="{ value }">
60
59
  {{ props.options?.find(option => option.value === value)?.label || props.placeholder }}
61
60
  </template>
62
- </Select>
61
+ </ComboboxSelect>
63
62
  </slot>
64
63
  </div>
65
64
  </label>
@@ -1,7 +1,7 @@
1
1
  export { default as FieldCheckbox } from './field-checkbox.vue'
2
+ export { default as FieldCombobox } from './field-combobox.vue'
2
3
  export { default as FieldInput } from './field-input.vue'
3
4
  export { default as FieldKeyValues } from './field-key-values.vue'
4
5
  export { default as FieldRange } from './field-range.vue'
5
- export { default as FieldSelect } from './field-select.vue'
6
6
  export { default as FieldTextArea } from './field-text-area.vue'
7
7
  export { default as FieldValues } from './field-values.vue'
@@ -1,5 +1,6 @@
1
1
  export * from './checkbox'
2
2
  export * from './combobox'
3
+ export * from './combobox-select'
3
4
  export * from './field'
4
5
  export * from './input'
5
6
  export * from './radio'
@@ -1,10 +1,14 @@
1
1
  <script setup lang="ts">
2
+ import { useSlots } from 'vue'
3
+
2
4
  import BasicInputFile from './basic-input-file.vue'
3
5
 
4
6
  defineProps<{
5
7
  accept?: string
6
8
  multiple?: boolean
7
9
  }>()
10
+
11
+ const slots = useSlots()
8
12
  </script>
9
13
 
10
14
  <template>
@@ -28,7 +32,9 @@ defineProps<{
28
32
  :multiple="multiple"
29
33
  >
30
34
  <template #default="{ isDragging }">
35
+ <slot v-if="slots.default" :is-dragging="isDragging" />
31
36
  <div
37
+ v-else
32
38
  class="flex flex-col items-center"
33
39
  :class="[
34
40
  isDragging ? 'text-primary-500 dark:text-primary-400' : 'text-neutral-400 dark:text-neutral-500',
@@ -56,7 +56,8 @@ function handleInput(e: Event) {
56
56
  :min="scaledMin"
57
57
  :max="scaledMax"
58
58
  :step="scaledStep"
59
- class="slider-progress form_input-round-range"
59
+ :disabled="props.disabled"
60
+ :class="['slider-progress', 'form_input-round-range', props.disabled ? 'opacity-60 pointer-events-none' : '']"
60
61
  @input="handleInput"
61
62
  >
62
63
  </template>
@@ -115,6 +116,20 @@ https://toughengineer.github.io/demo/slider-styler*/
115
116
  outline: none;
116
117
  }
117
118
 
119
+ .form_input-round-range:disabled {
120
+ opacity: 0.6;
121
+ cursor: not-allowed;
122
+ }
123
+
124
+ .form_input-round-range:disabled::-webkit-slider-thumb,
125
+ .form_input-round-range:disabled::-webkit-slider-runnable-track,
126
+ .form_input-round-range:disabled::-moz-range-thumb,
127
+ .form_input-round-range:disabled::-moz-range-track,
128
+ .form_input-round-range:disabled::-ms-thumb,
129
+ .form_input-round-range:disabled::-ms-track {
130
+ cursor: not-allowed;
131
+ }
132
+
118
133
  /*webkit*/
119
134
  .form_input-round-range::-webkit-slider-thumb {
120
135
  appearance: none;
@@ -1,2 +1,2 @@
1
- export { default as Option } from './option.vue'
1
+ export { default as SelectOption } from './select-option.vue'
2
2
  export { default as Select } from './select.vue'
@@ -0,0 +1,89 @@
1
+ <script setup lang="ts" generic="T extends AcceptableValue">
2
+ import type { AcceptableValue } from 'reka-ui'
3
+
4
+ import {
5
+ SelectItem,
6
+ SelectItemIndicator,
7
+ SelectItemText,
8
+ } from 'reka-ui'
9
+
10
+ interface SelectOptionItem<T extends AcceptableValue> {
11
+ label: string
12
+ value: T
13
+ description?: string
14
+ disabled?: boolean
15
+ icon?: string
16
+ }
17
+
18
+ const props = defineProps<{
19
+ option: SelectOptionItem<T>
20
+ }>()
21
+ </script>
22
+
23
+ <template>
24
+ <SelectItem
25
+ :value="props.option.value"
26
+ :disabled="props.option.disabled"
27
+ :text-value="props.option.label"
28
+ :class="[
29
+ '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',
30
+ 'data-[highlighted]:bg-neutral-100 dark:data-[highlighted]:bg-neutral-800',
31
+ 'text-sm text-neutral-700 dark:text-neutral-200 data-[disabled]:text-neutral-400 dark:data-[disabled]:text-neutral-600',
32
+ 'transition-colors duration-200 ease-in-out',
33
+ props.option.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
34
+ ]"
35
+ >
36
+ <SelectItemIndicator
37
+ :class="[
38
+ 'col-start-1 row-start-1',
39
+ 'inline-flex items-center justify-center',
40
+ 'w-[1rem]',
41
+ 'opacity-30',
42
+ 'text-current',
43
+ ]"
44
+ >
45
+ <div i-solar:alt-arrow-right-outline class="size-4" />
46
+ </SelectItemIndicator>
47
+
48
+ <SelectItemText :class="['sr-only']">
49
+ {{ props.option.label }}
50
+ </SelectItemText>
51
+
52
+ <div :class="['col-start-2', 'min-w-0', 'flex', 'items-center', 'gap-2', 'py-1']">
53
+ <slot v-bind="{ option: props.option }">
54
+ <span
55
+ v-if="props.option.icon"
56
+ :class="[
57
+ 'size-4 shrink-0',
58
+ 'text-current',
59
+ props.option.icon,
60
+ ]"
61
+ />
62
+
63
+ <div :class="['min-w-0 flex flex-1 flex-col']">
64
+ <span
65
+ :class="[
66
+ 'line-clamp-1',
67
+ 'overflow-hidden',
68
+ 'text-ellipsis',
69
+ 'whitespace-nowrap',
70
+ ]"
71
+ >
72
+ {{ props.option.label }}
73
+ </span>
74
+
75
+ <span
76
+ v-if="props.option.description"
77
+ :class="[
78
+ 'line-clamp-2',
79
+ 'text-xs',
80
+ 'text-neutral-500 dark:text-neutral-400',
81
+ ]"
82
+ >
83
+ {{ props.option.description }}
84
+ </span>
85
+ </div>
86
+ </slot>
87
+ </div>
88
+ </SelectItem>
89
+ </template>
@@ -1,31 +1,242 @@
1
- <script setup lang="ts">
2
- import { provide, ref } from 'vue'
1
+ <script setup lang="ts" generic="T extends AcceptableValue">
2
+ import type { AcceptableValue } from 'reka-ui'
3
3
 
4
- import { Combobox } from '../combobox'
4
+ import {
5
+ SelectArrow,
6
+ SelectContent,
7
+ SelectGroup,
8
+ SelectIcon,
9
+ SelectLabel,
10
+ SelectPortal,
11
+ SelectRoot,
12
+ SelectSeparator,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ SelectViewport,
16
+ } from 'reka-ui'
17
+ import { computed } from 'vue'
5
18
 
6
- const props = defineProps<{
7
- options?: { label: string, value: string | number }[]
19
+ import SelectOption from './select-option.vue'
20
+
21
+ interface SelectOptionItem<T extends AcceptableValue> {
22
+ label: string
23
+ value: T
24
+ description?: string
25
+ disabled?: boolean
26
+ icon?: string
27
+ }
28
+
29
+ interface SelectOptionGroupItem<T extends AcceptableValue> {
30
+ groupLabel?: string
31
+ children?: SelectOptionItem<T>[]
32
+ }
33
+
34
+ const props = withDefaults(defineProps<{
35
+ options: SelectOptionItem<T>[] | SelectOptionGroupItem<T>[]
8
36
  placeholder?: string
9
37
  disabled?: boolean
10
- title?: string
11
- layout?: 'horizontal' | 'vertical'
12
- }>()
38
+ by?: string | ((a: T, b: T) => boolean)
39
+ contentMinWidth?: string | number
40
+ contentWidth?: string | number
41
+ shape?: 'rounded' | 'default'
42
+ variant?: 'blurry' | 'default'
43
+ }>(), {
44
+ placeholder: 'Select an option',
45
+ disabled: false,
46
+ by: undefined,
47
+ contentMinWidth: 160,
48
+ contentWidth: undefined,
49
+ shape: 'default',
50
+ variant: 'default',
51
+ })
13
52
 
14
- const show = ref(false)
15
- const modelValue = defineModel<string | number>({ required: false })
53
+ const modelValue = defineModel<T>({ required: false })
16
54
 
17
- function selectOption(value: string | number) {
18
- modelValue.value = value
19
- }
55
+ const normalizedOptions = computed<SelectOptionGroupItem<T>[]>(() => {
56
+ if (!props.options.length) {
57
+ return []
58
+ }
59
+
60
+ const [firstOption] = props.options
61
+ if ('value' in firstOption) {
62
+ return [
63
+ {
64
+ groupLabel: '',
65
+ children: props.options as SelectOptionItem<T>[],
66
+ },
67
+ ]
68
+ }
69
+
70
+ return props.options as SelectOptionGroupItem<T>[]
71
+ })
72
+
73
+ const flattenedOptions = computed<SelectOptionItem<T>[]>(() =>
74
+ normalizedOptions.value.flatMap(group => group.children ?? []),
75
+ )
20
76
 
21
- function handleHide() {
22
- show.value = false
77
+ const selectedOption = computed<SelectOptionItem<T> | undefined>(() =>
78
+ flattenedOptions.value.find(option => isSelectedOption(option.value, modelValue.value)),
79
+ )
80
+
81
+ function isSelectedOption(a: T, b: T | undefined): boolean {
82
+ if (b == null) {
83
+ return false
84
+ }
85
+
86
+ if (typeof props.by === 'function') {
87
+ return props.by(a, b)
88
+ }
89
+
90
+ if (typeof props.by === 'string') {
91
+ return (a as Record<string, unknown> | null)?.[props.by] === (b as Record<string, unknown> | null)?.[props.by]
92
+ }
93
+
94
+ return a === b
23
95
  }
24
96
 
25
- provide('selectOption', selectOption)
26
- provide('hide', handleHide)
97
+ function toCssSize(value?: string | number): string | undefined {
98
+ if (value == null) {
99
+ return undefined
100
+ }
101
+
102
+ return typeof value === 'number' ? `${value}px` : value
103
+ }
27
104
  </script>
28
105
 
29
106
  <template>
30
- <Combobox v-model="modelValue" :default-value="modelValue" :options="[{ groupLabel: '', children: props.options }]" />
107
+ <SelectRoot
108
+ v-model="modelValue"
109
+ :by="props.by"
110
+ :disabled="props.disabled"
111
+ >
112
+ <SelectTrigger
113
+ :class="[
114
+ 'group',
115
+ 'w-full inline-flex items-center justify-between border px-1 leading-none h-fit gap-[5px] outline-none',
116
+ props.shape === 'rounded' ? 'rounded-full' : 'rounded-lg',
117
+ 'text-sm text-neutral-700 dark:text-neutral-200 data-[placeholder]:text-neutral-400 dark:data-[placeholder]:text-neutral-500',
118
+ props.variant === 'default' ? 'bg-white dark:bg-neutral-900 disabled:bg-neutral-100 hover:bg-neutral-50 dark:disabled:bg-neutral-900 dark:hover:bg-neutral-700' : '',
119
+ props.variant === 'blurry' ? 'bg-neutral-50/70 dark:bg-neutral-800/70 disabled:bg-neutral-100 hover:bg-neutral-100 dark:disabled:bg-neutral-900 dark:hover:bg-neutral-800' : '',
120
+ props.variant === 'blurry' ? 'backdrop-blur-md' : '',
121
+ 'border-2 border-solid focus:border-primary-300 dark:focus:border-primary-400/50',
122
+ props.variant === 'default' ? 'border-neutral-200 dark:border-neutral-800' : '',
123
+ props.variant === 'blurry' ? 'border-neutral-100/60 dark:border-neutral-800/30' : '',
124
+ 'shadow-sm focus:shadow-[0_0_0_2px] focus:shadow-black/10 dark:focus:shadow-black/30',
125
+ 'transition-colors duration-200 ease-in-out',
126
+ props.disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
127
+ ]"
128
+ >
129
+ <div :class="['min-w-0 flex-1 text-left']">
130
+ <slot
131
+ v-if="$slots.value"
132
+ name="value"
133
+ v-bind="{ option: selectedOption, value: modelValue, placeholder: props.placeholder }"
134
+ >
135
+ <span
136
+ :class="[
137
+ 'block truncate',
138
+ selectedOption ? 'text-neutral-700 dark:text-neutral-200' : 'text-neutral-400 dark:text-neutral-500',
139
+ ]"
140
+ >
141
+ {{ selectedOption?.label ?? props.placeholder }}
142
+ </span>
143
+ </slot>
144
+ <SelectValue
145
+ v-else
146
+ v-model="modelValue"
147
+ :placeholder="props.placeholder"
148
+ />
149
+ </div>
150
+ <SelectIcon as-child>
151
+ <div
152
+ i-solar:alt-arrow-down-linear
153
+ :class="[
154
+ 'h-4 w-4 shrink-0',
155
+ 'text-neutral-700 dark:text-neutral-200',
156
+ 'transition-transform duration-200 ease-in-out',
157
+ 'group-data-[state=open]:rotate-180',
158
+ ]"
159
+ />
160
+ </SelectIcon>
161
+ </SelectTrigger>
162
+
163
+ <SelectPortal>
164
+ <SelectContent
165
+ position="popper"
166
+ side="bottom"
167
+ align="start"
168
+ :side-offset="4"
169
+ :avoid-collisions="true"
170
+ :class="[
171
+ // NOTICE: DialogContent/DialogOverlay use z-[9999], and DrawerContent uses z-[1000].
172
+ // SelectContent must render above these layers so that dropdowns inside
173
+ // Dialog/Drawer are not hidden behind the overlay or dismissed unexpectedly.
174
+ // Read more at: https://github.com/moeru-ai/airi/issues/1136
175
+ 'z-[10010]',
176
+ 'overflow-hidden rounded-xl shadow-sm border will-change-[opacity,transform]',
177
+ 'data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade',
178
+ 'bg-white dark:bg-neutral-900',
179
+ 'border-neutral-200 dark:border-neutral-800 border-solid border-2',
180
+ ]"
181
+ :style="{
182
+ width: toCssSize(props.contentWidth) ?? 'var(--reka-select-trigger-width)',
183
+ minWidth: toCssSize(props.contentMinWidth),
184
+ }"
185
+ >
186
+ <SelectViewport
187
+ :class="[
188
+ 'p-[2px]',
189
+ 'max-h-50dvh',
190
+ 'overflow-y-auto',
191
+ ]"
192
+ >
193
+ <template
194
+ v-for="(group, groupIndex) in normalizedOptions"
195
+ :key="group.groupLabel || `group-${groupIndex}`"
196
+ >
197
+ <SelectGroup :class="['overflow-x-hidden']">
198
+ <SelectSeparator
199
+ v-if="groupIndex !== 0"
200
+ :class="['m-[5px]', 'h-[1px]', 'bg-neutral-200 dark:bg-neutral-800']"
201
+ />
202
+
203
+ <SelectLabel
204
+ v-if="group.groupLabel"
205
+ :class="[
206
+ 'px-[25px] text-xs leading-[25px]',
207
+ 'text-neutral-500 dark:text-neutral-400',
208
+ 'transition-colors duration-200 ease-in-out',
209
+ ]"
210
+ >
211
+ {{ group.groupLabel }}
212
+ </SelectLabel>
213
+
214
+ <SelectOption
215
+ v-for="(option, optionIndex) in group.children || []"
216
+ :key="`${group.groupLabel || groupIndex}-${option.label}-${optionIndex}`"
217
+ :option="option"
218
+ >
219
+ <template
220
+ v-if="$slots.option"
221
+ #default="{ option: slotOption }"
222
+ >
223
+ <slot
224
+ name="option"
225
+ v-bind="{ option: slotOption }"
226
+ />
227
+ </template>
228
+ </SelectOption>
229
+ </SelectGroup>
230
+ </template>
231
+ </SelectViewport>
232
+
233
+ <SelectArrow
234
+ :class="[
235
+ 'fill-white dark:fill-neutral-900',
236
+ 'stroke-neutral-200 dark:stroke-neutral-800',
237
+ ]"
238
+ />
239
+ </SelectContent>
240
+ </SelectPortal>
241
+ </SelectRoot>
31
242
  </template>