@saasmakers/ui 2.0.58 → 2.0.60
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/app/components/bases/BaseTag.vue +9 -9
- package/app/components/bases/BaseTags.vue +1 -1
- package/app/components/fields/FieldCombobox.vue +430 -0
- package/app/types/bases.d.ts +2 -2
- package/app/types/fields.d.ts +19 -0
- package/app/types/global.d.ts +1 -0
- package/package.json +1 -1
- package/uno.config.ts +1 -0
|
@@ -31,16 +31,16 @@ function onClick(event: MouseEvent) {
|
|
|
31
31
|
'px-0.5 py-px text-3xs': size === 'xs',
|
|
32
32
|
'px-1 py-px text-3xs': size === 'sm',
|
|
33
33
|
'px-1.5 py-0.5 text-2xs': size === 'base',
|
|
34
|
-
'bg-transparent text-gray-900 dark:text-gray-100': variant === 'ghost' && !active,
|
|
35
|
-
'hover:bg-gray-500/10': variant === 'ghost' && !active && clickable,
|
|
36
|
-
'bg-transparent text-indigo-800 dark:text-indigo-300': variant === 'ghost' &&
|
|
37
|
-
'bg-transparent text-orange-800 dark:text-orange-300': variant === 'ghost' &&
|
|
38
|
-
'bg-transparent text-green-800 dark:text-green-300': variant === 'ghost' &&
|
|
34
|
+
'bg-transparent text-gray-900 dark:text-gray-100': variant === 'ghost' && !active && color === 'gray',
|
|
35
|
+
'hover:bg-gray-500/10': variant === 'ghost' && !active && color === 'gray' && clickable,
|
|
36
|
+
'bg-transparent text-indigo-800 dark:text-indigo-300': variant === 'ghost' && color === 'indigo',
|
|
37
|
+
'bg-transparent text-orange-800 dark:text-orange-300': variant === 'ghost' && color === 'orange',
|
|
38
|
+
'bg-transparent text-green-800 dark:text-green-300': variant === 'ghost' && color === 'green',
|
|
39
39
|
'bg-transparent text-gray-700 dark:text-gray-300': variant === 'ghost' && active && color === 'gray',
|
|
40
|
-
'bg-orange-500/15 text-orange-800 dark:text-orange-300': variant === 'filled' && color === 'orange'
|
|
41
|
-
'bg-indigo-500/15 text-indigo-800 dark:text-indigo-300': variant === 'filled' && color === 'indigo'
|
|
42
|
-
'bg-green-500/15 text-green-800 dark:text-green-300': variant === 'filled' && color === 'green'
|
|
43
|
-
'bg-gray-500/20 text-gray-700 dark:text-gray-300': variant === 'filled' &&
|
|
40
|
+
'bg-orange-500/15 text-orange-800 dark:text-orange-300': variant === 'filled' && color === 'orange',
|
|
41
|
+
'bg-indigo-500/15 text-indigo-800 dark:text-indigo-300': variant === 'filled' && color === 'indigo',
|
|
42
|
+
'bg-green-500/15 text-green-800 dark:text-green-300': variant === 'filled' && color === 'green',
|
|
43
|
+
'bg-gray-500/20 text-gray-700 dark:text-gray-300': variant === 'filled' && color === 'gray',
|
|
44
44
|
}"
|
|
45
45
|
:type="clickable ? 'button' : undefined"
|
|
46
46
|
@click="onClick"
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
<script lang="ts" setup generic="Item extends object">
|
|
2
|
+
import type { FieldCombobox } from '../../types/fields'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<Omit<FieldCombobox<Item>, 'modelValue'>>(), {
|
|
5
|
+
allowCreate: false,
|
|
6
|
+
border: 'full',
|
|
7
|
+
description: '',
|
|
8
|
+
disabled: false,
|
|
9
|
+
hideError: false,
|
|
10
|
+
itemKey: 'id',
|
|
11
|
+
itemLabel: 'name',
|
|
12
|
+
items: () => [],
|
|
13
|
+
label: '',
|
|
14
|
+
labelIcon: undefined,
|
|
15
|
+
placeholder: '',
|
|
16
|
+
required: false,
|
|
17
|
+
size: 'base',
|
|
18
|
+
uppercase: true,
|
|
19
|
+
validation: undefined,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
create: [label: string]
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n()
|
|
27
|
+
const id = useId()
|
|
28
|
+
const listId = `${id}-list`
|
|
29
|
+
const modelValue = defineModel<Item[]>({ default: () => [] })
|
|
30
|
+
const inputRef = ref<HTMLInputElement>()
|
|
31
|
+
const opened = ref(false)
|
|
32
|
+
const searchRaw = ref('')
|
|
33
|
+
const highlightedIndex = ref(0)
|
|
34
|
+
|
|
35
|
+
const filteredItems = computed(() => {
|
|
36
|
+
const selectedKeys = new Set(modelValue.value.map(item => keyOf(item)))
|
|
37
|
+
const availableItems = props.items.filter(item => !selectedKeys.has(keyOf(item)))
|
|
38
|
+
const searchQuery = normalizeText(searchRaw.value.trim())
|
|
39
|
+
|
|
40
|
+
return searchQuery
|
|
41
|
+
? availableItems.filter(item => normalizeText(labelOf(item)).includes(searchQuery))
|
|
42
|
+
: availableItems
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const canCreate = computed(() => {
|
|
46
|
+
const trimmed = searchRaw.value.trim()
|
|
47
|
+
|
|
48
|
+
if (props.allowCreate && trimmed) {
|
|
49
|
+
const normalizedInput = normalizeText(trimmed)
|
|
50
|
+
|
|
51
|
+
return !props.items.some(item => normalizeText(labelOf(item)) === normalizedInput)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const createIndex = computed(() => {
|
|
58
|
+
return canCreate.value ? filteredItems.value.length : -1
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const optionsCount = computed(() => {
|
|
62
|
+
return filteredItems.value.length + (canCreate.value ? 1 : 0)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
watch([filteredItems, canCreate], () => {
|
|
66
|
+
const lastIndex = Math.max(0, optionsCount.value - 1)
|
|
67
|
+
|
|
68
|
+
if (highlightedIndex.value > lastIndex) {
|
|
69
|
+
highlightedIndex.value = lastIndex
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
function keyOf(item: Item) {
|
|
74
|
+
return typeof props.itemKey === 'function' ? props.itemKey(item) : String(readField(item, props.itemKey))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function labelOf(item: Item) {
|
|
78
|
+
return typeof props.itemLabel === 'function'
|
|
79
|
+
? props.itemLabel(item)
|
|
80
|
+
: String(readField(item, props.itemLabel) ?? '')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onArrowDown() {
|
|
84
|
+
highlightedIndex.value = Math.min(highlightedIndex.value + 1, optionsCount.value - 1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onArrowUp() {
|
|
88
|
+
highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function onBackspace() {
|
|
92
|
+
if (searchRaw.value === '' && modelValue.value.length > 0) {
|
|
93
|
+
modelValue.value = modelValue.value.slice(0, -1)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onClose() {
|
|
98
|
+
if (opened.value) {
|
|
99
|
+
opened.value = false
|
|
100
|
+
searchRaw.value = ''
|
|
101
|
+
highlightedIndex.value = 0
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function onCreate() {
|
|
106
|
+
const label = searchRaw.value.trim()
|
|
107
|
+
|
|
108
|
+
if (label) {
|
|
109
|
+
// The parent owns creation (idempotency, colors, sync); we never mutate items ourselves.
|
|
110
|
+
emit('create', label)
|
|
111
|
+
|
|
112
|
+
searchRaw.value = ''
|
|
113
|
+
highlightedIndex.value = 0
|
|
114
|
+
|
|
115
|
+
nextTick(() => inputRef.value?.focus())
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function onEnter() {
|
|
120
|
+
if (optionsCount.value > 0) {
|
|
121
|
+
if (highlightedIndex.value === createIndex.value) {
|
|
122
|
+
onCreate()
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const item = filteredItems.value[highlightedIndex.value]
|
|
126
|
+
|
|
127
|
+
if (item) {
|
|
128
|
+
onSelect(item)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function onHighlight(index: number) {
|
|
135
|
+
highlightedIndex.value = index
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function onOpen() {
|
|
139
|
+
if (!props.disabled && !opened.value) {
|
|
140
|
+
opened.value = true
|
|
141
|
+
highlightedIndex.value = 0
|
|
142
|
+
|
|
143
|
+
nextTick(() => {
|
|
144
|
+
inputRef.value?.focus()
|
|
145
|
+
inputRef.value?.scrollIntoView({ block: 'nearest' })
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onRemove(item: Item) {
|
|
151
|
+
const removedKey = keyOf(item)
|
|
152
|
+
|
|
153
|
+
modelValue.value = modelValue.value.filter(current => keyOf(current) !== removedKey)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onSelect(item: Item) {
|
|
157
|
+
modelValue.value = [...modelValue.value, item]
|
|
158
|
+
searchRaw.value = ''
|
|
159
|
+
highlightedIndex.value = 0
|
|
160
|
+
|
|
161
|
+
nextTick(() => inputRef.value?.focus())
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readField(item: Item, field: string) {
|
|
165
|
+
return (item as Record<string, unknown>)[field]
|
|
166
|
+
}
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<template>
|
|
170
|
+
<div class="flex flex-col">
|
|
171
|
+
<FieldLabel
|
|
172
|
+
v-if="label"
|
|
173
|
+
:disabled="disabled"
|
|
174
|
+
:for-field="id"
|
|
175
|
+
has-margin-bottom
|
|
176
|
+
:icon="labelIcon"
|
|
177
|
+
:label="label"
|
|
178
|
+
:required="required"
|
|
179
|
+
:size="size"
|
|
180
|
+
/>
|
|
181
|
+
|
|
182
|
+
<div
|
|
183
|
+
class="overflow-hidden rounded-lg bg-white dark:bg-gray-900"
|
|
184
|
+
:class="{
|
|
185
|
+
'text-xs': size === 'sm',
|
|
186
|
+
'text-sm': size === 'base',
|
|
187
|
+
'text-base': size === 'lg',
|
|
188
|
+
'base-box': border === 'full',
|
|
189
|
+
'base-border-b': border === 'bottom',
|
|
190
|
+
'border-0': border === 'none',
|
|
191
|
+
'base-border-active': opened && border !== 'none',
|
|
192
|
+
'base-border-interactive-focus-within': !opened && !disabled && border !== 'none',
|
|
193
|
+
'field-disabled': disabled,
|
|
194
|
+
'field-uppercase': uppercase,
|
|
195
|
+
}"
|
|
196
|
+
>
|
|
197
|
+
<div
|
|
198
|
+
class="flex flex-wrap select-none items-center gap-1.5 p-1.5"
|
|
199
|
+
:class="{
|
|
200
|
+
'min-h-10': size === 'sm',
|
|
201
|
+
'min-h-12': size === 'base',
|
|
202
|
+
'min-h-14': size === 'lg',
|
|
203
|
+
}"
|
|
204
|
+
>
|
|
205
|
+
<button
|
|
206
|
+
v-for="item in modelValue"
|
|
207
|
+
:key="keyOf(item)"
|
|
208
|
+
:aria-label="t('remove', { label: labelOf(item) })"
|
|
209
|
+
class="min-h-8 inline-flex items-center gap-1 border base-border rounded-md bg-gray-100 py-1 pl-2 pr-1.5 text-gray-900 font-medium tracking-tight hover:border-gray-300 dark:bg-gray-800 dark:text-gray-100 dark:hover:border-gray-700"
|
|
210
|
+
type="button"
|
|
211
|
+
@mousedown.prevent
|
|
212
|
+
@click="onRemove(item)"
|
|
213
|
+
>
|
|
214
|
+
<span class="truncate">{{ labelOf(item) }}</span>
|
|
215
|
+
|
|
216
|
+
<BaseIcon
|
|
217
|
+
color="gray"
|
|
218
|
+
:icon="getLayerIcon('close')"
|
|
219
|
+
size="2xs"
|
|
220
|
+
/>
|
|
221
|
+
</button>
|
|
222
|
+
|
|
223
|
+
<button
|
|
224
|
+
v-if="!opened"
|
|
225
|
+
:aria-label="t('add')"
|
|
226
|
+
class="min-h-8 inline-flex items-center gap-1 border base-border base-border-hover rounded-md border-dashed py-1 pl-1.5 pr-2 text-gray-600 font-medium tracking-tight dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100"
|
|
227
|
+
:disabled="disabled"
|
|
228
|
+
type="button"
|
|
229
|
+
@click="onOpen"
|
|
230
|
+
>
|
|
231
|
+
<BaseIcon
|
|
232
|
+
:icon="getLayerIcon('plus')"
|
|
233
|
+
size="xs"
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<span>{{ t('add') }}</span>
|
|
237
|
+
</button>
|
|
238
|
+
|
|
239
|
+
<input
|
|
240
|
+
v-else
|
|
241
|
+
:id="id"
|
|
242
|
+
ref="inputRef"
|
|
243
|
+
v-model="searchRaw"
|
|
244
|
+
:aria-activedescendant="opened && optionsCount > 0 ? highlightedIndex === createIndex ? `${id}-create` : `${id}-option-${highlightedIndex}` : undefined"
|
|
245
|
+
aria-autocomplete="list"
|
|
246
|
+
:aria-controls="optionsCount > 0 ? listId : undefined"
|
|
247
|
+
:aria-expanded="opened"
|
|
248
|
+
autocomplete="off"
|
|
249
|
+
class="min-w-[6rem] flex-1 select-text appearance-none bg-transparent px-2 text-gray-900 font-medium tracking-tight outline-none dark:text-gray-100 placeholder-gray-600 dark:placeholder-gray-400"
|
|
250
|
+
:placeholder="placeholder || (allowCreate ? t('searchOrCreate') : t('search'))"
|
|
251
|
+
role="combobox"
|
|
252
|
+
spellcheck="false"
|
|
253
|
+
@blur="onClose"
|
|
254
|
+
@keydown.backspace="onBackspace"
|
|
255
|
+
@keydown.down.prevent="onArrowDown"
|
|
256
|
+
@keydown.enter.prevent="onEnter"
|
|
257
|
+
@keydown.escape.stop="onClose"
|
|
258
|
+
@keydown.up.prevent="onArrowUp"
|
|
259
|
+
>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div
|
|
263
|
+
v-if="opened && optionsCount > 0"
|
|
264
|
+
class="select-none border-t base-border px-1.5 pb-1.5 pt-1"
|
|
265
|
+
>
|
|
266
|
+
<div
|
|
267
|
+
:id="listId"
|
|
268
|
+
class="max-h-[150px] overflow-y-auto"
|
|
269
|
+
role="listbox"
|
|
270
|
+
>
|
|
271
|
+
<div
|
|
272
|
+
v-for="(item, index) in filteredItems"
|
|
273
|
+
:id="`${id}-option-${index}`"
|
|
274
|
+
:key="keyOf(item)"
|
|
275
|
+
:aria-selected="index === highlightedIndex"
|
|
276
|
+
class="flex cursor-pointer items-center rounded-md px-2 font-medium tracking-tight outline-none"
|
|
277
|
+
:class="{
|
|
278
|
+
'min-h-8': size === 'sm',
|
|
279
|
+
'min-h-10': size === 'base',
|
|
280
|
+
'min-h-12': size === 'lg',
|
|
281
|
+
'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100': index === highlightedIndex,
|
|
282
|
+
'text-gray-600 dark:text-gray-400': index !== highlightedIndex,
|
|
283
|
+
}"
|
|
284
|
+
role="option"
|
|
285
|
+
@click="onSelect(item)"
|
|
286
|
+
@mousedown.prevent
|
|
287
|
+
@mouseenter="onHighlight(index)"
|
|
288
|
+
>
|
|
289
|
+
<span class="truncate">
|
|
290
|
+
{{ labelOf(item) }}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div
|
|
295
|
+
v-if="canCreate"
|
|
296
|
+
:id="`${id}-create`"
|
|
297
|
+
:aria-selected="highlightedIndex === createIndex"
|
|
298
|
+
class="flex cursor-pointer items-center rounded-md px-2 font-medium tracking-tight outline-none"
|
|
299
|
+
:class="{
|
|
300
|
+
'min-h-8': size === 'sm',
|
|
301
|
+
'min-h-10': size === 'base',
|
|
302
|
+
'min-h-12': size === 'lg',
|
|
303
|
+
'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100': highlightedIndex === createIndex,
|
|
304
|
+
'text-gray-600 dark:text-gray-400': highlightedIndex !== createIndex,
|
|
305
|
+
}"
|
|
306
|
+
role="option"
|
|
307
|
+
@click="onCreate"
|
|
308
|
+
@mousedown.prevent
|
|
309
|
+
@mouseenter="onHighlight(createIndex)"
|
|
310
|
+
>
|
|
311
|
+
<BaseIcon
|
|
312
|
+
class="mr-1.5 shrink-0"
|
|
313
|
+
color="gray"
|
|
314
|
+
:icon="getLayerIcon('plus')"
|
|
315
|
+
size="2xs"
|
|
316
|
+
/>
|
|
317
|
+
|
|
318
|
+
<span class="truncate">
|
|
319
|
+
{{ t('create', { label: searchRaw.trim() }) }}
|
|
320
|
+
</span>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<FieldMessage
|
|
327
|
+
:description="description"
|
|
328
|
+
:disabled="disabled"
|
|
329
|
+
:hide-error="hideError"
|
|
330
|
+
:size="size"
|
|
331
|
+
:validation="validation"
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
</template>
|
|
335
|
+
|
|
336
|
+
<i18n lang="json">
|
|
337
|
+
{
|
|
338
|
+
"de": {
|
|
339
|
+
"add": "Hinzufügen",
|
|
340
|
+
"create": "„{label}“ erstellen",
|
|
341
|
+
"remove": "{label} entfernen",
|
|
342
|
+
"search": "Suchen…",
|
|
343
|
+
"searchOrCreate": "Suchen oder erstellen…"
|
|
344
|
+
},
|
|
345
|
+
"en": {
|
|
346
|
+
"add": "Add",
|
|
347
|
+
"create": "Create \"{label}\"",
|
|
348
|
+
"remove": "Remove {label}",
|
|
349
|
+
"search": "Search…",
|
|
350
|
+
"searchOrCreate": "Search or create…"
|
|
351
|
+
},
|
|
352
|
+
"es": {
|
|
353
|
+
"add": "Añadir",
|
|
354
|
+
"create": "Crear «{label}»",
|
|
355
|
+
"remove": "Eliminar {label}",
|
|
356
|
+
"search": "Buscar…",
|
|
357
|
+
"searchOrCreate": "Buscar o crear…"
|
|
358
|
+
},
|
|
359
|
+
"fr": {
|
|
360
|
+
"add": "Ajouter",
|
|
361
|
+
"create": "Créer « {label} »",
|
|
362
|
+
"remove": "Retirer {label}",
|
|
363
|
+
"search": "Rechercher…",
|
|
364
|
+
"searchOrCreate": "Rechercher ou créer…"
|
|
365
|
+
},
|
|
366
|
+
"id": {
|
|
367
|
+
"add": "Tambah",
|
|
368
|
+
"create": "Buat \"{label}\"",
|
|
369
|
+
"remove": "Hapus {label}",
|
|
370
|
+
"search": "Cari…",
|
|
371
|
+
"searchOrCreate": "Cari atau buat…"
|
|
372
|
+
},
|
|
373
|
+
"it": {
|
|
374
|
+
"add": "Aggiungi",
|
|
375
|
+
"create": "Crea «{label}»",
|
|
376
|
+
"remove": "Rimuovi {label}",
|
|
377
|
+
"search": "Cerca…",
|
|
378
|
+
"searchOrCreate": "Cerca o crea…"
|
|
379
|
+
},
|
|
380
|
+
"ja": {
|
|
381
|
+
"add": "追加",
|
|
382
|
+
"create": "「{label}」を作成",
|
|
383
|
+
"remove": "{label} を削除",
|
|
384
|
+
"search": "検索…",
|
|
385
|
+
"searchOrCreate": "検索または作成…"
|
|
386
|
+
},
|
|
387
|
+
"ko": {
|
|
388
|
+
"add": "추가",
|
|
389
|
+
"create": "\"{label}\" 만들기",
|
|
390
|
+
"remove": "{label} 제거",
|
|
391
|
+
"search": "검색…",
|
|
392
|
+
"searchOrCreate": "검색 또는 생성…"
|
|
393
|
+
},
|
|
394
|
+
"nl": {
|
|
395
|
+
"add": "Toevoegen",
|
|
396
|
+
"create": "\"{label}\" maken",
|
|
397
|
+
"remove": "{label} verwijderen",
|
|
398
|
+
"search": "Zoeken…",
|
|
399
|
+
"searchOrCreate": "Zoeken of maken…"
|
|
400
|
+
},
|
|
401
|
+
"pl": {
|
|
402
|
+
"add": "Dodaj",
|
|
403
|
+
"create": "Utwórz „{label}”",
|
|
404
|
+
"remove": "Usuń {label}",
|
|
405
|
+
"search": "Szukaj…",
|
|
406
|
+
"searchOrCreate": "Szukaj lub utwórz…"
|
|
407
|
+
},
|
|
408
|
+
"pt": {
|
|
409
|
+
"add": "Adicionar",
|
|
410
|
+
"create": "Criar «{label}»",
|
|
411
|
+
"remove": "Remover {label}",
|
|
412
|
+
"search": "Pesquisar…",
|
|
413
|
+
"searchOrCreate": "Pesquisar ou criar…"
|
|
414
|
+
},
|
|
415
|
+
"pt-BR": {
|
|
416
|
+
"add": "Adicionar",
|
|
417
|
+
"create": "Criar \"{label}\"",
|
|
418
|
+
"remove": "Remover {label}",
|
|
419
|
+
"search": "Pesquisar…",
|
|
420
|
+
"searchOrCreate": "Pesquisar ou criar…"
|
|
421
|
+
},
|
|
422
|
+
"vi": {
|
|
423
|
+
"add": "Thêm",
|
|
424
|
+
"create": "Tạo \"{label}\"",
|
|
425
|
+
"remove": "Xóa {label}",
|
|
426
|
+
"search": "Tìm…",
|
|
427
|
+
"searchOrCreate": "Tìm hoặc tạo…"
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
</i18n>
|
package/app/types/bases.d.ts
CHANGED
|
@@ -338,8 +338,6 @@ export interface BaseTag {
|
|
|
338
338
|
|
|
339
339
|
export type BaseTagColor = 'gray' | 'green' | 'indigo' | 'orange'
|
|
340
340
|
|
|
341
|
-
export type BaseTagVariant = 'filled' | 'ghost'
|
|
342
|
-
|
|
343
341
|
export interface BaseTagItem extends BaseTag {
|
|
344
342
|
id?: number | string
|
|
345
343
|
order?: number
|
|
@@ -353,6 +351,8 @@ export interface BaseTags {
|
|
|
353
351
|
|
|
354
352
|
export type BaseTagSize = 'base' | 'sm' | 'xs'
|
|
355
353
|
|
|
354
|
+
export type BaseTagVariant = 'filled' | 'ghost'
|
|
355
|
+
|
|
356
356
|
export interface BaseText {
|
|
357
357
|
alignment?: BaseAlignment
|
|
358
358
|
background?: BaseBackground
|
package/app/types/fields.d.ts
CHANGED
|
@@ -37,6 +37,25 @@ export interface FieldCheckbox {
|
|
|
37
37
|
validation?: VuelidateValidation
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export interface FieldCombobox<Item = Record<string, unknown>> {
|
|
41
|
+
allowCreate?: boolean
|
|
42
|
+
border?: FieldInputBorder
|
|
43
|
+
description?: BaseTextText
|
|
44
|
+
disabled?: boolean
|
|
45
|
+
hideError?: boolean
|
|
46
|
+
itemKey?: ((item: Item) => string) | string
|
|
47
|
+
itemLabel?: ((item: Item) => string) | string
|
|
48
|
+
items?: Item[]
|
|
49
|
+
label?: BaseTextText
|
|
50
|
+
labelIcon?: string
|
|
51
|
+
modelValue: Item[]
|
|
52
|
+
placeholder?: string
|
|
53
|
+
required?: boolean
|
|
54
|
+
size?: FieldSize
|
|
55
|
+
uppercase?: boolean
|
|
56
|
+
validation?: VuelidateValidation
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
export interface FieldDays {
|
|
41
60
|
disabled?: boolean
|
|
42
61
|
modelValue: number[]
|
package/app/types/global.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ declare global {
|
|
|
91
91
|
type FieldAvatar = import('./fields').FieldAvatar
|
|
92
92
|
type FieldBackground = import('./fields').FieldBackground
|
|
93
93
|
type FieldCheckbox = import('./fields').FieldCheckbox
|
|
94
|
+
type FieldCombobox<Item = Record<string, unknown>> = import('./fields').FieldCombobox<Item>
|
|
94
95
|
type FieldDays = import('./fields').FieldDays
|
|
95
96
|
type FieldEmojis = import('./fields').FieldEmojis
|
|
96
97
|
type FieldInput = import('./fields').FieldInput
|
package/package.json
CHANGED
package/uno.config.ts
CHANGED
|
@@ -39,6 +39,7 @@ export const shortcuts: Record<string, string> = {
|
|
|
39
39
|
'base-highlight-indigo': 'border border-indigo-600/35 bg-indigo-600/[0.08] shadow-indigo-600/15 dark:border-indigo-400/35 dark:bg-indigo-400/[0.08] dark:shadow-indigo-400/15',
|
|
40
40
|
'base-surface': 'border base-border base-bg shadow-sm',
|
|
41
41
|
'field-disabled': 'opacity-50 cursor-not-allowed',
|
|
42
|
+
'field-uppercase': 'uppercase [&_*]:uppercase [&_input::placeholder]:uppercase',
|
|
42
43
|
'scroll-y-stable': 'overflow-y-auto [scrollbar-gutter:stable]',
|
|
43
44
|
}
|
|
44
45
|
|