plugin-ui-for-kzt 0.0.28 → 0.0.30

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.
Files changed (28) hide show
  1. package/dist/components/BaseCheckbox/BaseCheckbox.vue.d.ts +2 -2
  2. package/dist/components/BaseDropdown/BaseDropdown.vue.d.ts +1 -1
  3. package/dist/components/BaseInput/BaseInput.vue.d.ts +3 -3
  4. package/dist/components/BaseInputCalendar/BaseInputCalendar.vue.d.ts +4 -4
  5. package/dist/components/BaseInputCurrency/BaseInputCurrency.vue.d.ts +3 -3
  6. package/dist/components/BaseInputEmail/BaseInputEmail.vue.d.ts +3 -3
  7. package/dist/components/BaseInputPhone/BaseInputPhone.vue.d.ts +3 -3
  8. package/dist/components/BaseRadio/BaseRadio.vue.d.ts +2 -2
  9. package/dist/components/BaseSelect/BaseSelect.vue.d.ts +16 -3
  10. package/dist/components/BaseTable/BaseTable.vue.d.ts +14 -1
  11. package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +3 -3
  12. package/dist/components/BaseToggle/BaseToggle.vue.d.ts +2 -2
  13. package/dist/components/BaseTooltip/BaseTooltip.vue.d.ts +2 -2
  14. package/dist/index.js +1 -1
  15. package/dist/sprite.svg +1 -1
  16. package/example/App.vue +271 -7
  17. package/package.json +1 -1
  18. package/src/assets/icons/search.svg +4 -0
  19. package/src/components/BaseButton/BaseButton.vue +20 -20
  20. package/src/components/BaseSelect/BaseSelect.vue +164 -10
  21. package/src/components/BaseSelect/README.md +110 -1
  22. package/src/components/BaseTable/BaseTable.vue +278 -26
  23. package/src/components/BaseTable/README.md +96 -1
  24. package/src/components/BaseTooltip/BaseTooltip.vue +9 -5
  25. package/src/components/BaseUpload/BaseUpload.vue +97 -25
  26. package/src/types/input.d.ts +1 -0
  27. package/src/types/table.d.ts +6 -0
  28. package/src/vue-virtual-scroller.d.ts +4 -0
@@ -8,7 +8,9 @@
8
8
  :disabled="actualDisabled"
9
9
  >
10
10
  <template #top>
11
+ <!-- Старый header для дефолтного поведения -->
11
12
  <div
13
+ v-if="!searchable"
12
14
  :data-error="Boolean(error)"
13
15
  :data-disabled="disabled"
14
16
  class="base-select__header"
@@ -21,12 +23,9 @@
21
23
  <slot name="headerIcon" />
22
24
  </div>
23
25
 
24
- <div
25
- v-if="actualOption"
26
- class="base-select__header_value"
27
- >
28
- {{ actualOption.name }}
29
- </div>
26
+ <div v-if="actualOption" class="base-select__header_value">
27
+ {{ actualOption.name }}
28
+ </div>
30
29
 
31
30
  <div
32
31
  v-else
@@ -48,15 +47,45 @@
48
47
  </div>
49
48
  </div>
50
49
  </div>
50
+
51
+ <BaseInput
52
+ v-else
53
+ :id="id"
54
+ :model-value="displayValue || ''"
55
+ :placeholder="placeholder"
56
+ :disabled="disabled"
57
+ :readonly="readonly"
58
+ :size="size"
59
+ :error="Boolean(error)"
60
+ class="base-select__input"
61
+ @input="handleInputChange"
62
+ >
63
+ <template #left-icon>
64
+ <base-icon
65
+ name="search"
66
+ :size="size"
67
+ />
68
+ </template>
69
+
70
+ <template #right-icon>
71
+ <base-icon
72
+ v-if="!readonly"
73
+ :name="actualOption ? 'close' : 'arrow-down'"
74
+ :size="size"
75
+ class="base-select__toggle-icon"
76
+ @click="handleIconClick"
77
+ />
78
+ </template>
79
+ </BaseInput>
51
80
  </template>
52
81
 
53
82
  <template #dropdown>
54
83
  <div
55
- v-if="(options ?? []).length"
84
+ v-if="(filteredOptions ?? []).length"
56
85
  class="base-select__dropdown"
57
- >
86
+ >
58
87
  <dynamic-scroller
59
- :items="options as ICoreSelectBaseProps[]"
88
+ :items="filteredOptions as ICoreSelectBaseProps[]"
60
89
  :min-item-size="36"
61
90
  key-field="id"
62
91
  class="base-select__list"
@@ -86,6 +115,9 @@
86
115
  </template>
87
116
  </dynamic-scroller>
88
117
  </div>
118
+ <div class="base-select__empty">
119
+ <slot name="empty" />
120
+ </div>
89
121
  </template>
90
122
  </base-dropdown>
91
123
  </div>
@@ -99,6 +131,7 @@ import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
99
131
  import type { ICoreSelectProps, TSelectValue, ICoreSelectBaseProps, ICoreSelectOption, ISelectSlotProps } from '../../types/input';
100
132
  import BaseDropdown from '../BaseDropdown/BaseDropdown.vue';
101
133
  import BaseIcon from '../BaseIcon/BaseIcon.vue';
134
+ import BaseInput from '../BaseInput/BaseInput.vue';
102
135
  import BaseOpenedListItem from '../BaseOpenedListItem/BaseOpenedListItem.vue';
103
136
  import { useKitSize } from '../../composables/kit/size';
104
137
  import { useKitState } from '../../composables/kit/state';
@@ -109,18 +142,31 @@ const props = withDefaults(defineProps<ICoreSelectProps & {
109
142
  }>(), {
110
143
  options: () => [],
111
144
  size: 'medium',
145
+ searchable: false,
112
146
  });
113
147
 
114
148
  const emit = defineEmits<{
115
149
  (e: 'update:modelValue', value: TSelectValue): void
116
150
  (e: 'change', value: TSelectValue): void
151
+ (e: 'error'): void
152
+ (e: 'search', query: string): void
153
+ (e: 'open'): void
154
+ (e: 'clear'): void
117
155
  }>();
118
156
 
119
157
  const actualValue = ref<TSelectValue>(props.modelValue ?? '');
158
+ const searchQuery = ref<string>('');
120
159
  const actualOption = computed(() =>
121
160
  props.options?.find((item: ICoreSelectOption) => item?.id === actualValue.value) || null
122
161
  );
123
162
 
163
+ const displayValue = computed(() => {
164
+ if (actualOption.value) {
165
+ return actualOption.value.name;
166
+ }
167
+ return searchQuery.value;
168
+ });
169
+
124
170
  watch(() => props.modelValue, (val) => {
125
171
  actualValue.value = val ?? '';
126
172
  }, { immediate: true });
@@ -129,10 +175,72 @@ function handleInput(value: TSelectValue) {
129
175
  actualValue.value = value;
130
176
  emit('update:modelValue', value);
131
177
  emit('change', value);
178
+ dropdownVisible.value = false;
179
+ searchQuery.value = '';
180
+ }
181
+
182
+ function handleInputChange(event: Event) {
183
+ const target = event.target as HTMLInputElement;
184
+ const inputValue = target.value;
185
+ searchQuery.value = inputValue;
186
+
187
+ emit('search', inputValue);
188
+
189
+ if (inputValue) {
190
+ const exactMatch = props.options?.find((option: ICoreSelectOption) =>
191
+ option.name.toLowerCase() === inputValue.toLowerCase()
192
+ );
193
+
194
+ if (exactMatch) {
195
+ handleInput(exactMatch.id);
196
+ return;
197
+ }
198
+
199
+ if (!dropdownVisible.value) {
200
+ dropdownVisible.value = true;
201
+ }
202
+ }
203
+ }
204
+
205
+ function handleIconClick(event: Event) {
206
+ event.stopPropagation();
207
+ if (actualOption.value) {
208
+ handleInput('');
209
+ emit('clear');
210
+ } else {
211
+ dropdownVisible.value = !dropdownVisible.value;
212
+ }
132
213
  }
133
214
 
134
215
  const actualDisabled = computed(() => props.disabled || !props.options?.length);
135
216
  const dropdownVisible = ref(false);
217
+
218
+ const filteredOptions = computed(() => {
219
+ if (!props.searchable || !searchQuery.value.trim()) {
220
+ return props.options || [];
221
+ }
222
+
223
+ const query = searchQuery.value.toLowerCase().trim();
224
+ return (props.options || []).filter((option: ICoreSelectOption) =>
225
+ option.name.toLowerCase().includes(query)
226
+ );
227
+ });
228
+
229
+ watch(() => props.error, (error) => {
230
+ console.log('ERROR', error);
231
+ });
232
+
233
+ watch(dropdownVisible, (isVisible) => {
234
+ if (isVisible) {
235
+ emit('open');
236
+ }
237
+ if (!isVisible) {
238
+ if (searchQuery.value && !actualOption.value) {
239
+ emit('error');
240
+ }
241
+ searchQuery.value = '';
242
+ }
243
+ });
136
244
  const { sizeClassList } = useKitSize(props);
137
245
  const { stateClassList } = useKitState(props);
138
246
  const { styleClassList } = useKitStyle(props);
@@ -162,6 +270,7 @@ defineSlots<{
162
270
  iconItem(props: { item: ICoreSelectBaseProps }): any;
163
271
  header(props: { value: ICoreSelectProps['options'] }): any;
164
272
  headerIcon(): any;
273
+ empty(): any;
165
274
  }>();
166
275
  </script>
167
276
 
@@ -185,6 +294,10 @@ defineSlots<{
185
294
  border: 1px solid var(--primary-black-300);
186
295
  outline: 4px solid var(--effects-primary-focus);
187
296
  }
297
+
298
+ &__toggle-icon {
299
+ transform: rotate(180deg);
300
+ }
188
301
  }
189
302
 
190
303
  .dropdown__dropdown {
@@ -202,6 +315,10 @@ defineSlots<{
202
315
  #{$select}__header {
203
316
  border: 1px solid var(--error-red-light-01);
204
317
  }
318
+
319
+ #{$select}__toggle-icon {
320
+ color: var(--error-red);
321
+ }
205
322
  }
206
323
 
207
324
  &__wrapper {
@@ -225,6 +342,8 @@ defineSlots<{
225
342
 
226
343
  &__header_value {
227
344
  color: var(--primary-text-primary);
345
+
346
+ @include text-clamp(1);
228
347
  }
229
348
 
230
349
  &__placeholder {
@@ -247,6 +366,25 @@ defineSlots<{
247
366
  transform: translate3d(0, -50%, 0);
248
367
  }
249
368
 
369
+ &__input {
370
+ cursor: pointer;
371
+
372
+ @include text-clamp(1);
373
+
374
+ &.--is-readonly {
375
+ cursor: default;
376
+ }
377
+ }
378
+
379
+ &__toggle-icon {
380
+ cursor: pointer;
381
+ transition: transform var(--transition);
382
+
383
+ &:hover {
384
+ opacity: 0.7;
385
+ }
386
+ }
387
+
250
388
  &__dropdown {
251
389
  width: 100%;
252
390
  height: 100%;
@@ -266,7 +404,7 @@ defineSlots<{
266
404
  font: var(--typography-text-m-regular);
267
405
  }
268
406
 
269
- &__dropdown {
407
+ &__dropdown, &__input {
270
408
  border-radius: var(--corner-radius-s);
271
409
  }
272
410
 
@@ -289,6 +427,10 @@ defineSlots<{
289
427
  font: var(--typography-text-m-regular);
290
428
  }
291
429
 
430
+ &__input {
431
+ border-radius: var(--corner-radius-m);
432
+ }
433
+
292
434
  &__header {
293
435
  height: 48px;
294
436
  padding: var(--spacing-m) var(--spacing-2l);
@@ -307,6 +449,10 @@ defineSlots<{
307
449
  font: var(--typography-text-l-regular);
308
450
  }
309
451
 
452
+ &__input {
453
+ border-radius: var(--corner-radius-l);
454
+ }
455
+
310
456
  &__header {
311
457
  height: 56px;
312
458
  padding: var(--spacing-m) var(--spacing-l);
@@ -328,6 +474,10 @@ defineSlots<{
328
474
  &__arrow {
329
475
  color: var(--ui-colors-input-icon-disabled);
330
476
  }
477
+
478
+ &__toggle-icon {
479
+ color: var(--ui-colors-input-icon-disabled);
480
+ }
331
481
  }
332
482
  }
333
483
 
@@ -336,6 +486,10 @@ defineSlots<{
336
486
  &__header {
337
487
  pointer-events: none;
338
488
  }
489
+
490
+ &__input {
491
+ pointer-events: none;
492
+ }
339
493
  }
340
494
  }
341
495
  }
@@ -4,8 +4,9 @@
4
4
 
5
5
  ---
6
6
 
7
- ### ✅ Пример использования
7
+ ### ✅ Примеры использования
8
8
 
9
+ #### Базовый пример
9
10
  ```vue
10
11
  <base-select
11
12
  v-model="selected"
@@ -21,6 +22,99 @@
21
22
  />
22
23
  ```
23
24
 
25
+ #### Server-side поиск (рекомендуемый подход)
26
+ ```vue
27
+ <template>
28
+ <base-select
29
+ id="employee-select"
30
+ v-model="selectedEmployee"
31
+ :options="employeeOptions"
32
+ searchable
33
+ placeholder="Найти сотрудника..."
34
+ label="Выберите сотрудника"
35
+ :error="employeeError"
36
+ @search="handleEmployeeSearch"
37
+ @open="handleEmployeeOpen"
38
+ @clear="handleEmployeeClear"
39
+ @error="handleSearchError"
40
+ />
41
+ </template>
42
+
43
+ <script setup>
44
+ import { ref } from 'vue'
45
+ import axios from 'axios'
46
+
47
+ const selectedEmployee = ref('')
48
+ const employeeOptions = ref([])
49
+ const employeeError = ref('')
50
+ const searchTimeout = ref(null)
51
+
52
+ // Дебаунс поиска для оптимизации запросов
53
+ const debounceSearch = (callback, delay) => {
54
+ if (searchTimeout.value) {
55
+ clearTimeout(searchTimeout.value)
56
+ }
57
+ searchTimeout.value = setTimeout(callback, delay)
58
+ }
59
+
60
+ // Загрузка сотрудников с сервера
61
+ const fetchEmployees = async (query = '') => {
62
+ try {
63
+ const params = {
64
+ limit: 100,
65
+ skip: 0
66
+ }
67
+
68
+ if (query?.trim()) {
69
+ params.name = query.trim()
70
+ }
71
+
72
+ const response = await axios.get('/api/employees', { params })
73
+ const employees = response.data.content || []
74
+
75
+ employeeOptions.value = employees.map(item => ({
76
+ id: item.employeeId,
77
+ name: `${item.lastName} ${item.firstName} ${item.middleName}`.trim(),
78
+ }))
79
+
80
+ employeeError.value = ''
81
+ } catch (error) {
82
+ console.error('Failed to fetch employees:', error)
83
+ employeeOptions.value = []
84
+ employeeError.value = 'Ошибка загрузки данных'
85
+ }
86
+ }
87
+
88
+ // Обработчик поиска с дебаунсом
89
+ const handleEmployeeSearch = (query) => {
90
+ debounceSearch(() => {
91
+ if (query.length >= 2 || query.length === 0) {
92
+ fetchEmployees(query)
93
+ }
94
+ }, 700) // Дебаунс 700мс
95
+ }
96
+
97
+ // Загрузка при открытии если данных нет
98
+ const handleEmployeeOpen = () => {
99
+ if (employeeOptions.value.length === 0) {
100
+ fetchEmployees()
101
+ }
102
+ }
103
+
104
+ // Очистка выбора
105
+ const handleEmployeeClear = () => {
106
+ selectedEmployee.value = ''
107
+ employeeError.value = ''
108
+ fetchEmployees() // Загружаем исходный список
109
+ }
110
+
111
+ // Обработка ошибок поиска
112
+ const handleSearchError = () => {
113
+ employeeError.value = 'Элемент не найден'
114
+ }
115
+ </script>
116
+ ```
117
+
24
118
  ---
25
119
 
26
120
  ### ⚙️ Пропсы
@@ -55,6 +149,9 @@
55
149
  - `size?: 'small' | 'medium' | 'large'`
56
150
  Управляет размерами шрифта, паддингов и иконок.
57
151
 
152
+ - `searchable?: boolean`
153
+ **Включает режим поиска. При `true` заменяет обычный заголовок на поле ввода для поиска. Идеально подходит для server-side поиска больших наборов данных.
154
+
58
155
  - `multiple?: boolean`
59
156
  Поддержка множественного выбора (не реализовано в текущем компоненте, зарезервировано).
60
157
 
@@ -71,6 +168,18 @@
71
168
  - `change`
72
169
  Также эмитится при выборе. Полезно для побочных эффектов.
73
170
 
171
+ - `search`
172
+ Эмитится при вводе в поисковое поле (только при `searchable: true`). Возвращает строку поискового запроса. Используется для server-side поиска.
173
+
174
+ - `open`
175
+ Эмитится при открытии выпадающего меню. Полезно для первоначальной загрузки данных при server-side поиске.
176
+
177
+ - `clear`
178
+ Эмитится при очистке выбранного значения через иконку крестика (только при `searchable: true`).
179
+
180
+ - `error`
181
+ Эмитится когда пользователь вводит текст, но не выбирает ни одну опцию из списка. Полезно для валидации поиска.
182
+
74
183
  ---
75
184
 
76
185
  ### 🧩 Слоты