@proj-airi/ui 0.9.0-alpha.33 → 0.9.0-alpha.36

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/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.33",
4
+ "version": "0.9.0-alpha.36",
5
5
  "description": "A collection of UI components that used by Project AIRI",
6
6
  "author": {
7
7
  "name": "Moeru AI Project AIRI Team",
@@ -16,19 +16,58 @@ import {
16
16
  ComboboxTrigger,
17
17
  ComboboxViewport,
18
18
  } from 'reka-ui'
19
+ import { computed } from 'vue'
19
20
 
20
- const props = defineProps<{
21
- options: { groupLabel: string, children?: { label: string, value: T }[] }[]
21
+ interface ComboboxOptionItem<T extends AcceptableValue> {
22
+ label: string
23
+ value: T
24
+ description?: string
25
+ disabled?: boolean
26
+ icon?: string
27
+ }
28
+
29
+ interface ComboboxOptionGroupItem<T extends AcceptableValue> {
30
+ groupLabel?: string
31
+ children?: ComboboxOptionItem<T>[]
32
+ }
33
+
34
+ const props = withDefaults(defineProps<{
35
+ options: ComboboxOptionItem<T>[] | ComboboxOptionGroupItem<T>[]
22
36
  placeholder?: string
37
+ disabled?: boolean
23
38
  contentMinWidth?: string | number
24
39
  contentWidth?: string | number
25
- }>()
40
+ }>(), {
41
+ disabled: false,
42
+ })
26
43
 
27
44
  const modelValue = defineModel<T>({ required: false })
28
45
 
46
+ const normalizedOptions = computed<ComboboxOptionGroupItem<T>[]>(() => {
47
+ if (!props.options.length) {
48
+ return []
49
+ }
50
+
51
+ const [firstOption] = props.options
52
+ if ('value' in firstOption) {
53
+ return [
54
+ {
55
+ groupLabel: '',
56
+ children: props.options as ComboboxOptionItem<T>[],
57
+ },
58
+ ]
59
+ }
60
+
61
+ return props.options as ComboboxOptionGroupItem<T>[]
62
+ })
63
+
64
+ const flattenedOptions = computed<ComboboxOptionItem<T>[]>(() =>
65
+ normalizedOptions.value.flatMap(group => group.children ?? []),
66
+ )
67
+
29
68
  function toDisplayValue(value: T): string {
30
- const option = props.options.flatMap(group => group.children).find(option => option?.value === value)
31
- return option ? option.label : props.placeholder || ''
69
+ const option = flattenedOptions.value.find(option => option.value === value)
70
+ return option?.label ?? props.placeholder ?? ''
32
71
  }
33
72
 
34
73
  function toCssSize(value?: string | number): string | undefined {
@@ -41,15 +80,20 @@ function toCssSize(value?: string | number): string | undefined {
41
80
  </script>
42
81
 
43
82
  <template>
44
- <ComboboxRoot v-model="modelValue" :class="['relative', 'w-full', 'h-fit']">
83
+ <ComboboxRoot
84
+ v-model="modelValue"
85
+ :disabled="props.disabled"
86
+ :class="['relative', 'w-full', 'h-fit']"
87
+ >
45
88
  <ComboboxAnchor
46
89
  :class="[
47
90
  'w-full inline-flex items-center justify-between rounded-xl border px-3 leading-none h-9 gap-[5px] outline-none',
48
91
  'text-sm text-neutral-700 dark:text-neutral-200 data-[placeholder]:text-neutral-200',
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',
92
+ 'bg-white dark:bg-neutral-900 hover:bg-neutral-50 dark:hover:bg-neutral-700',
50
93
  'border-neutral-200 dark:border-neutral-800 border-solid border-2 focus:border-primary-300 dark:focus:border-primary-400/50',
51
94
  'shadow-sm focus:shadow-[0_0_0_2px] focus:shadow-black',
52
95
  'transition-colors duration-200 ease-in-out',
96
+ props.disabled ? 'cursor-not-allowed bg-neutral-100 opacity-60 dark:bg-neutral-900' : 'cursor-pointer',
53
97
  ]"
54
98
  >
55
99
  <ComboboxInput
@@ -58,6 +102,7 @@ function toCssSize(value?: string | number): string | undefined {
58
102
  'text-neutral-700 dark:text-neutral-200',
59
103
  'transition-colors duration-200 ease-in-out',
60
104
  ]"
105
+ :disabled="props.disabled"
61
106
  :placeholder="props.placeholder"
62
107
  :display-value="(val) => toDisplayValue(val)"
63
108
  />
@@ -103,19 +148,22 @@ function toCssSize(value?: string | number): string | undefined {
103
148
  'text-xs text-neutral-700 dark:text-neutral-200',
104
149
  'transition-colors duration-200 ease-in-out',
105
150
  ]"
106
- />
151
+ >
152
+ <slot name="empty" />
153
+ </ComboboxEmpty>
107
154
 
108
155
  <template
109
- v-for="(group, index) in options"
110
- :key="group.groupLabel"
156
+ v-for="(group, groupIndex) in normalizedOptions"
157
+ :key="group.groupLabel || `group-${groupIndex}`"
111
158
  >
112
159
  <ComboboxGroup :class="['overflow-x-hidden']">
113
160
  <ComboboxSeparator
114
- v-if="index !== 0"
161
+ v-if="groupIndex !== 0"
115
162
  :class="['m-[5px]', 'h-[1px]', 'bg-neutral-400']"
116
163
  />
117
164
 
118
165
  <ComboboxLabel
166
+ v-if="group.groupLabel"
119
167
  :class="[
120
168
  'px-[25px] text-xs leading-[25px]',
121
169
  'text-neutral-500 dark:text-neutral-400',
@@ -126,26 +174,70 @@ function toCssSize(value?: string | number): string | undefined {
126
174
  </ComboboxLabel>
127
175
 
128
176
  <ComboboxItem
129
- v-for="option in group.children"
130
- :key="option.label"
177
+ v-for="(option, optionIndex) in group.children || []"
178
+ :key="`${group.groupLabel || groupIndex}-${option.label}-${optionIndex}`"
131
179
  :text-value="option.label"
132
180
  :value="option.value"
181
+ :disabled="option.disabled"
133
182
  :class="[
134
- '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',
183
+ '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',
135
184
  'data-[highlighted]:bg-neutral-100 dark:data-[highlighted]:bg-neutral-800',
136
185
  'text-sm text-neutral-700 dark:text-neutral-200 data-[disabled]:text-neutral-400 dark:data-[disabled]:text-neutral-600 data-[highlighted]:text-grass1',
137
186
  'transition-colors duration-200 ease-in-out',
138
- 'cursor-pointer',
187
+ option.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
139
188
  ]"
140
189
  >
141
190
  <ComboboxItemIndicator
142
- :class="['absolute', 'left-0', 'w-[25px]', 'inline-flex', 'items-center', 'justify-center', 'opacity-30']"
191
+ :class="[
192
+ 'col-start-1 row-start-1',
193
+ 'inline-flex items-center justify-center',
194
+ 'w-[1rem]',
195
+ 'opacity-30',
196
+ 'text-current',
197
+ ]"
143
198
  >
144
- <div i-solar:alt-arrow-right-outline />
199
+ <div i-solar:alt-arrow-right-outline class="size-4" />
145
200
  </ComboboxItemIndicator>
146
- <span :class="['line-clamp-1', 'overflow-hidden', 'text-ellipsis', 'whitespace-nowrap']">
147
- {{ option.label }}
148
- </span>
201
+
202
+ <div :class="['col-start-2', 'min-w-0', 'flex', 'items-center', 'gap-2', 'py-1']">
203
+ <slot
204
+ name="option"
205
+ v-bind="{ option }"
206
+ >
207
+ <span
208
+ v-if="option.icon"
209
+ :class="[
210
+ 'size-4 shrink-0',
211
+ 'text-current',
212
+ option.icon,
213
+ ]"
214
+ />
215
+
216
+ <div :class="['min-w-0', 'flex', 'flex-1', 'flex-col']">
217
+ <span
218
+ :class="[
219
+ 'line-clamp-1',
220
+ 'overflow-hidden',
221
+ 'text-ellipsis',
222
+ 'whitespace-nowrap',
223
+ ]"
224
+ >
225
+ {{ option.label }}
226
+ </span>
227
+
228
+ <span
229
+ v-if="option.description"
230
+ :class="[
231
+ 'line-clamp-2',
232
+ 'text-xs',
233
+ 'text-neutral-500 dark:text-neutral-400',
234
+ ]"
235
+ >
236
+ {{ option.description }}
237
+ </span>
238
+ </div>
239
+ </slot>
240
+ </div>
149
241
  </ComboboxItem>
150
242
  </ComboboxGroup>
151
243
  </template>
@@ -1,10 +1,14 @@
1
1
  <script setup lang="ts">
2
- import { provide, ref } from 'vue'
3
-
4
2
  import { Combobox } from '../combobox'
5
3
 
6
4
  const props = defineProps<{
7
- options?: { label: string, value: string | number }[]
5
+ options?: {
6
+ label: string
7
+ value: string | number
8
+ description?: string
9
+ disabled?: boolean
10
+ icon?: string
11
+ }[]
8
12
  placeholder?: string
9
13
  disabled?: boolean
10
14
  title?: string
@@ -13,27 +17,33 @@ const props = defineProps<{
13
17
  contentWidth?: string | number
14
18
  }>()
15
19
 
16
- const show = ref(false)
17
20
  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
21
  </script>
30
22
 
31
23
  <template>
32
24
  <Combobox
33
25
  v-model="modelValue"
34
- :default-value="modelValue"
35
26
  :options="[{ groupLabel: '', children: props.options }]"
27
+ :disabled="props.disabled"
36
28
  :content-min-width="props.contentMinWidth"
37
29
  :content-width="props.contentWidth"
38
- />
30
+ :placeholder="props.placeholder"
31
+ >
32
+ <template
33
+ v-if="$slots.option"
34
+ #option="{ option }"
35
+ >
36
+ <slot
37
+ name="option"
38
+ v-bind="{ option }"
39
+ />
40
+ </template>
41
+
42
+ <template
43
+ v-if="$slots.empty"
44
+ #empty
45
+ >
46
+ <slot name="empty" />
47
+ </template>
48
+ </Combobox>
39
49
  </template>
@@ -4,11 +4,19 @@ import { ComboboxSelect } from '../combobox-select'
4
4
  const props = withDefaults(defineProps<{
5
5
  label: string
6
6
  description?: string
7
- options?: { label: string, value: string | number }[]
7
+ options?: {
8
+ label: string
9
+ value: string | number
10
+ description?: string
11
+ disabled?: boolean
12
+ icon?: string
13
+ }[]
8
14
  placeholder?: string
9
15
  disabled?: boolean
10
16
  layout?: 'horizontal' | 'vertical'
11
17
  selectClass?: string | string[]
18
+ contentMinWidth?: string | number
19
+ contentWidth?: string | number
12
20
  }>(), {
13
21
  layout: 'horizontal',
14
22
  })
@@ -27,7 +35,7 @@ const modelValue = defineModel<string>({ required: false })
27
35
  <div
28
36
  :class="[
29
37
  'w-full',
30
- props.layout === 'horizontal' ? 'col-span-3' : 'row-span-2',
38
+ props.layout === 'horizontal' ? 'col-span-2' : 'row-span-2',
31
39
  ]"
32
40
  >
33
41
  <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 })
47
55
  :options="props.options?.filter(option => option.label && option.value) || []"
48
56
  :placeholder="props.placeholder"
49
57
  :disabled="props.disabled"
58
+ :content-min-width="props.contentMinWidth"
59
+ :content-width="props.contentWidth"
50
60
  :title="label"
51
61
  :class="[
52
62
  ...(props.selectClass
53
63
  ? (typeof props.selectClass === 'string' ? [props.selectClass] : props.selectClass)
54
64
  : []),
55
- props.layout === 'horizontal' ? 'col-span-1' : 'row-span-2',
65
+ props.layout === 'horizontal' ? 'col-span-2' : 'row-span-2',
56
66
  ]"
57
67
  >
58
- <template #default="{ value }">
59
- {{ props.options?.find(option => option.value === value)?.label || props.placeholder }}
68
+ <template
69
+ v-if="$slots.option"
70
+ #option="{ option }"
71
+ >
72
+ <slot
73
+ name="option"
74
+ v-bind="{ option }"
75
+ />
76
+ </template>
77
+
78
+ <template
79
+ v-if="$slots.empty"
80
+ #empty
81
+ >
82
+ <slot name="empty" />
60
83
  </template>
61
84
  </ComboboxSelect>
62
85
  </slot>
@@ -36,9 +36,9 @@ const modelValue = defineModel<number>({ required: true })
36
36
  <div :class="['flex', 'flex-row', 'items-center', 'gap-2']">
37
37
  <Range
38
38
  v-model="modelValue"
39
- :min="min || 0"
40
- :max="max || 1"
41
- :step="step || 0.01"
39
+ :min="min ?? 0"
40
+ :max="max ?? 1"
41
+ :step="step ?? 0.01"
42
42
  :class="['w-full']"
43
43
  />
44
44
  </div>
@@ -1,9 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch } from 'vue'
3
3
 
4
- const props = defineProps<{
4
+ const props = withDefaults(defineProps<{
5
5
  defaultHeight?: string
6
- }>()
6
+ submitOnEnter?: boolean
7
+ }>(), {
8
+ submitOnEnter: true,
9
+ })
7
10
 
8
11
  const events = defineEmits<{
9
12
  (event: 'submit', message: string): void
@@ -18,6 +21,9 @@ const textareaRef = ref<HTMLTextAreaElement>()
18
21
  const textareaHeight = ref('auto')
19
22
 
20
23
  function onKeyDown(e: KeyboardEvent) {
24
+ if (!props.submitOnEnter)
25
+ return
26
+
21
27
  if (e.code === 'Enter' && !e.shiftKey) { // just block Enter is enough, Shift+Enter by default generates a newline
22
28
  e.preventDefault()
23
29
  events('submit', input.value)