@saasmakers/ui 1.4.29 → 1.4.31

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.
@@ -15,9 +15,9 @@ export default {
15
15
  },
16
16
  hideNext: { control: 'boolean' },
17
17
  hidePrevious: { control: 'boolean' },
18
+ loading: { control: 'boolean' },
18
19
  margin: { control: 'number' },
19
20
  navigable: { control: 'boolean' },
20
- loading: { control: 'boolean' },
21
21
  size: {
22
22
  control: 'select',
23
23
  options: ['sm', 'base'] satisfies BaseDividerSize[],
@@ -1,4 +1,5 @@
1
1
  <script lang="ts" setup>
2
+ import type { FieldInput } from '#components'
2
3
  import { vOnClickOutside } from '@vueuse/components'
3
4
  import type { FieldSelect, FieldSelectOption } from '../../types/fields'
4
5
 
@@ -18,6 +19,8 @@ const props = withDefaults(defineProps<Omit<FieldSelect, 'modelValue'>>(), {
18
19
  padding: true,
19
20
  placeholder: '',
20
21
  required: false,
22
+ searchable: false,
23
+ searchPlaceholder: '',
21
24
  size: 'base',
22
25
  validation: undefined,
23
26
  })
@@ -30,11 +33,24 @@ const emit = defineEmits<{
30
33
 
31
34
  const { getIcon } = useLayerIcons()
32
35
  const { fadeIn } = useMotion()
36
+ const { normalizeText } = useLayerUtils()
37
+ const { t } = useI18n()
33
38
  const id = useId()
34
39
 
35
40
  const modelValue = defineModel<FieldSelect['modelValue']>({ default: '' })
36
41
 
37
42
  const opened = ref(false)
43
+ const searchRaw = ref('')
44
+
45
+ const searchInput = ref<InstanceType<typeof FieldInput>>()
46
+
47
+ const computedColumns = computed(() => {
48
+ if (props.columns.length > 0) {
49
+ return props.columns
50
+ }
51
+
52
+ return [{ options: computedOptions.value }]
53
+ })
38
54
 
39
55
  const computedOptions = computed(() => {
40
56
  const options: FieldSelectOption[] = []
@@ -56,12 +72,28 @@ const computedOptions = computed(() => {
56
72
  return options
57
73
  })
58
74
 
59
- const computedColumns = computed(() => {
60
- if (props.columns.length > 0) {
61
- return props.columns
75
+ const searchQueryCleaned = computed(() => {
76
+ return normalizeText(searchRaw.value.trim())
77
+ })
78
+
79
+ const filteredColumns = computed(() => {
80
+ if (!props.searchable || !searchQueryCleaned.value) {
81
+ return computedColumns.value
62
82
  }
63
83
 
64
- return [{ options: computedOptions.value }]
84
+ return computedColumns.value
85
+ .map(column => ({
86
+ ...column,
87
+ options: column.options.filter((option) => {
88
+ return normalizeText(option.text).includes(searchQueryCleaned.value)
89
+ || normalizeText(option.value).includes(searchQueryCleaned.value)
90
+ }),
91
+ }))
92
+ .filter(column => column.options.length > 0)
93
+ })
94
+
95
+ const hasFilteredResults = computed(() => {
96
+ return filteredColumns.value.some(column => column.options.length > 0)
65
97
  })
66
98
 
67
99
  const selectedOption = computed(() => {
@@ -70,6 +102,18 @@ const selectedOption = computed(() => {
70
102
  })
71
103
  })
72
104
 
105
+ const showNoResults = computed(() => {
106
+ return props.searchable && !!searchQueryCleaned.value && !hasFilteredResults.value
107
+ })
108
+
109
+ watch(opened, (isOpened) => {
110
+ if (isOpened && props.searchable) {
111
+ nextTick(() => {
112
+ searchInput.value?.focus()
113
+ })
114
+ }
115
+ })
116
+
73
117
  function onClose() {
74
118
  reset()
75
119
  }
@@ -128,8 +172,15 @@ function onOptionKeyDown(event: KeyboardEvent) {
128
172
  }
129
173
  }
130
174
 
175
+ function onSearchKeyup(event: KeyboardEvent) {
176
+ if (event.key === 'Escape') {
177
+ reset()
178
+ }
179
+ }
180
+
131
181
  function reset() {
132
182
  opened.value = false
183
+ searchRaw.value = ''
133
184
  }
134
185
 
135
186
  function selectOption(event: MouseEvent, value: string) {
@@ -248,7 +299,33 @@ function selectOption(event: MouseEvent, value: string) {
248
299
  }"
249
300
  >
250
301
  <div
251
- v-for="(column, columnIndex) in computedColumns"
302
+ v-if="searchable"
303
+ class="sticky top-0 z-10 border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"
304
+ @click.stop
305
+ >
306
+ <FieldInput
307
+ ref="searchInput"
308
+ v-model="searchRaw"
309
+ :autocomplete="false"
310
+ background="white"
311
+ border="none"
312
+ class="px-3"
313
+ :placeholder="searchPlaceholder || t('search')"
314
+ :size="size"
315
+ type="search"
316
+ @keyup="onSearchKeyup"
317
+ />
318
+ </div>
319
+
320
+ <div
321
+ v-if="showNoResults"
322
+ class="px-3 py-2 text-center text-gray-600 dark:text-gray-400"
323
+ >
324
+ {{ t('noResults') }}
325
+ </div>
326
+
327
+ <div
328
+ v-for="(column, columnIndex) in filteredColumns"
252
329
  :key="columnIndex"
253
330
  >
254
331
  <BaseIcon
@@ -298,3 +375,60 @@ function selectOption(event: MouseEvent, value: string) {
298
375
  />
299
376
  </div>
300
377
  </template>
378
+
379
+ <i18n lang="json">
380
+ {
381
+ "de": {
382
+ "noResults": "Keine Ergebnisse",
383
+ "search": "Suchen"
384
+ },
385
+ "en": {
386
+ "noResults": "No results",
387
+ "search": "Search"
388
+ },
389
+ "es": {
390
+ "noResults": "Sin resultados",
391
+ "search": "Buscar"
392
+ },
393
+ "fr": {
394
+ "noResults": "Aucun résultat",
395
+ "search": "Rechercher"
396
+ },
397
+ "it": {
398
+ "noResults": "Nessun risultato",
399
+ "search": "Cerca"
400
+ },
401
+ "ja": {
402
+ "noResults": "結果がありません",
403
+ "search": "検索"
404
+ },
405
+ "ko": {
406
+ "noResults": "결과 없음",
407
+ "search": "검색"
408
+ },
409
+ "nl": {
410
+ "noResults": "Geen resultaten",
411
+ "search": "Zoeken"
412
+ },
413
+ "pl": {
414
+ "noResults": "Brak wyników",
415
+ "search": "Szukaj"
416
+ },
417
+ "pt": {
418
+ "noResults": "Sem resultados",
419
+ "search": "Pesquisar"
420
+ },
421
+ "pt-BR": {
422
+ "noResults": "Nenhum resultado",
423
+ "search": "Pesquisar"
424
+ },
425
+ "id": {
426
+ "noResults": "Tidak ada hasil",
427
+ "search": "Cari"
428
+ },
429
+ "vi": {
430
+ "noResults": "Không có kết quả",
431
+ "search": "Tìm kiếm"
432
+ }
433
+ }
434
+ </i18n>
@@ -109,9 +109,9 @@ export interface BaseDivider {
109
109
  borderStyle?: BaseDividerBorderStyle
110
110
  hideNext?: boolean
111
111
  hidePrevious?: boolean
112
+ loading?: boolean
112
113
  margin?: number
113
114
  navigable?: boolean
114
- loading?: boolean
115
115
  size?: BaseDividerSize
116
116
  title?: string
117
117
  }
@@ -132,6 +132,8 @@ export interface FieldSelect {
132
132
  padding?: boolean
133
133
  placeholder?: string
134
134
  required?: boolean
135
+ searchable?: boolean
136
+ searchPlaceholder?: string
135
137
  size?: FieldSize
136
138
  validation?: VuelidateValidation
137
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasmakers/ui",
3
- "version": "1.4.29",
3
+ "version": "1.4.31",
4
4
  "private": false,
5
5
  "description": "Reusable Nuxt UI components for SaaS Makers projects",
6
6
  "license": "MIT",