plugin-ui-for-kzt 0.0.22 → 0.0.23

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 (35) hide show
  1. package/dist/components/BaseBadge/BaseBadge.vue.d.ts +1 -1
  2. package/dist/components/BaseButton/BaseButton.vue.d.ts +3 -3
  3. package/dist/components/BaseCheckbox/BaseCheckbox.vue.d.ts +4 -4
  4. package/dist/components/BaseDropdown/BaseDropdown.vue.d.ts +3 -3
  5. package/dist/components/BaseField/BaseField.vue.d.ts +2 -2
  6. package/dist/components/BaseInput/BaseInput.vue.d.ts +6 -6
  7. package/dist/components/BaseInputCalendar/BaseInputCalendar.vue.d.ts +5 -5
  8. package/dist/components/BaseInputCurrency/BaseInputCurrency.vue.d.ts +6 -6
  9. package/dist/components/BaseInputEmail/BaseInputEmail.vue.d.ts +5 -5
  10. package/dist/components/BaseInputPhone/BaseInputPhone.vue.d.ts +5 -5
  11. package/dist/components/BaseOpenedListItem/BaseOpenedListItem.vue.d.ts +3 -3
  12. package/dist/components/BasePagination/BasePagination.vue.d.ts +1 -1
  13. package/dist/components/BaseRadio/BaseRadio.vue.d.ts +4 -4
  14. package/dist/components/BaseSegmentedButtons/BaseSegmentedButtons.vue.d.ts +3 -3
  15. package/dist/components/BaseSelect/BaseSelect.vue.d.ts +4 -4
  16. package/dist/components/BaseTabs/BaseTabs.vue.d.ts +25 -0
  17. package/dist/components/BaseTag/BaseTag.vue.d.ts +1 -1
  18. package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +5 -5
  19. package/dist/components/BaseToggle/BaseToggle.vue.d.ts +4 -4
  20. package/dist/components/BaseUpload/BaseUpload.vue.d.ts +11 -0
  21. package/dist/components/BaseUpload/CropModal.vue.d.ts +9 -0
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.LICENSE.txt +15 -0
  25. package/example/App.vue +37 -31
  26. package/example/TestImage.vue +6 -0
  27. package/package.json +2 -1
  28. package/src/components/BaseCheckbox/BaseCheckbox.vue +76 -46
  29. package/src/components/BaseRadio/BaseRadio.vue +266 -233
  30. package/src/components/BaseTabs/BaseTabs.vue +193 -0
  31. package/src/components/BaseUpload/BaseUpload.vue +35 -1
  32. package/src/components/BaseUpload/CropModal.vue +210 -0
  33. package/src/index.ts +5 -2
  34. package/src/types/tab.d.ts +17 -0
  35. package/src/types/uploadedFile.d.ts +7 -0
@@ -0,0 +1,193 @@
1
+ <template>
2
+ <div class="base-tabs" :class="classList">
3
+ <div class="base-tabs__header">
4
+ <base-button
5
+ v-for="tab in tabs"
6
+ :key="tab.id"
7
+ size="custom"
8
+ color="custom"
9
+ class="base-tabs__tab"
10
+ :class="{ 'base-tabs__tab--active': activeTab === tab.id }"
11
+ @click="selectTab(tab.id)"
12
+ :disabled="tab.disabled"
13
+ >
14
+ <div v-if="tab.icon" class="base-tab__icon">
15
+ <component :is="tab.icon" />
16
+ </div>
17
+ <span>{{ tab.title }}</span>
18
+ </base-button>
19
+ </div>
20
+ <div class="base-tabs__content">
21
+ <slot />
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { ref, computed, watch } from 'vue';
28
+ import type { TTabProps } from '../../types/tab.d';
29
+ import BaseButton from '../BaseButton/BaseButton.vue';
30
+
31
+ const props = withDefaults(defineProps<TTabProps>(), {
32
+ size: 'medium',
33
+ modelValue: 0,
34
+ });
35
+
36
+ const emit = defineEmits(['update:modelValue']);
37
+
38
+ const activeTab = ref(props.modelValue);
39
+
40
+ watch(() => props.modelValue, (newValue) => {
41
+ activeTab.value = newValue;
42
+ });
43
+
44
+ const selectTab = (id: number) => {
45
+ if (!props.tabs[id]?.disabled) {
46
+ activeTab.value = id;
47
+ emit('update:modelValue', id);
48
+ }
49
+ };
50
+
51
+ const classList = computed(() => [`base-tabs--${props.size}`]);
52
+ </script>
53
+
54
+ <style lang="scss" scoped>
55
+ @import '../../styles/variables';
56
+ @import '../../styles/root';
57
+
58
+ .base-tabs {
59
+ width: 100%;
60
+
61
+ &__header {
62
+ display: flex;
63
+ align-items: stretch;
64
+ position: relative;
65
+ z-index: 1;
66
+
67
+ &::before {
68
+ content: '';
69
+ width: 100%;
70
+ border-bottom: 1px solid var(--primary-black-200);
71
+ position: absolute;
72
+ bottom: 0;
73
+ }
74
+ }
75
+
76
+ &__tab {
77
+ display: flex;
78
+ align-items: center;
79
+ padding: var(--spacing-m) var(--spacing-l);
80
+ background: none;
81
+ border: none;
82
+ cursor: pointer;
83
+ color: var(--primary-black-700);
84
+ transition: color 0.2s ease, border-color 0.2s ease;
85
+ white-space: nowrap;
86
+ position: relative;
87
+
88
+ @include hover {
89
+ & {
90
+ color: var(--primary-black);
91
+ }
92
+ }
93
+
94
+ @include pressed {
95
+ color: var(--primary-black);
96
+ }
97
+
98
+ @include is-disabled-state {
99
+ color: var(--primary-black-400);
100
+ }
101
+
102
+ &--active {
103
+ color: var(--primary-blue);
104
+ border-bottom: 2px solid var(--primary-blue);
105
+ z-index: 2;
106
+
107
+ @include hover {
108
+ & {
109
+ color: var(--primary-blue-700);
110
+ border-color: transparent;
111
+ }
112
+ }
113
+
114
+ @include pressed {
115
+ color: var(--primary-blue-deep);
116
+ border-color: transparent;
117
+ }
118
+ }
119
+
120
+ .base-tab__icon {
121
+ display: flex;
122
+ align-items: center;
123
+ }
124
+ }
125
+
126
+ // Размеры
127
+ &--extra-small {
128
+ .base-tabs__header {
129
+ gap: var(--spacing-l);
130
+ }
131
+
132
+ .base-tabs__tab {
133
+ height: 38px;
134
+ padding: var(--spacing-s) var(--spacing-m);
135
+ font: var(--typography-text-s-medium);
136
+ gap: var(--spacing-s);
137
+
138
+ &--active {
139
+ font: var(--typography-text-s-semibold);
140
+ }
141
+
142
+ .base-tab__icon {
143
+ width: 20px;
144
+ height: 20px;
145
+ }
146
+ }
147
+ }
148
+
149
+ &--small {
150
+ .base-tabs__header {
151
+ gap: var(--spacing-xl);
152
+ }
153
+
154
+ .base-tabs__tab {
155
+ height: 48px;
156
+ padding: var(--spacing-m) var(--spacing-l);
157
+ font: var(--typography-text-m-medium);
158
+ gap: var(--spacing-m);
159
+
160
+ &--active {
161
+ font: var(--typography-text-m-semibold);
162
+ }
163
+
164
+ .base-tab__icon {
165
+ width: 24px;
166
+ height: 24px;
167
+ }
168
+ }
169
+ }
170
+
171
+ &--medium {
172
+ .base-tabs__header {
173
+ gap: var(--spacing-xl);
174
+ }
175
+
176
+ .base-tabs__tab {
177
+ height: 56px;
178
+ padding: var(--spacing-l) var(--spacing-xl);
179
+ font: var(--typography-text-l-medium);
180
+ gap: var(--spacing-m);
181
+
182
+ &--active {
183
+ font: var(--typography-text-l-semibold);
184
+ }
185
+
186
+ .base-tab__icon {
187
+ width: 32px;
188
+ height: 32px;
189
+ }
190
+ }
191
+ }
192
+ }
193
+ </style>
@@ -111,10 +111,12 @@ import BaseIcon from '../BaseIcon/BaseIcon.vue';
111
111
  import { useModal } from '../../composables/useModal';
112
112
  import type { UploadedFile, IpropsUpload } from '../../types/uploadedFile.d';
113
113
  import ImageModal from './ImageModal.vue';
114
+ import CropModal from './CropModal.vue';
114
115
 
115
116
  const props = withDefaults(defineProps<IpropsUpload>(), {
116
117
  multiple: true,
117
118
  maxFileSize: 25 * 1024 * 1024, // 25 MB в байтах
119
+ enableCrop: false,
118
120
  });
119
121
 
120
122
  const emit = defineEmits(['update:files']);
@@ -141,6 +143,11 @@ const triggerFileInput = () => {
141
143
  fileInput.value?.click();
142
144
  };
143
145
 
146
+ const isImageFile = (file: File) => {
147
+ const fileExtension = `.${file.name.split('.').pop()?.toLowerCase() || ''}`;
148
+ return mediaExtensions.includes(fileExtension);
149
+ };
150
+
144
151
  const handleFileUpload = (event: Event) => {
145
152
  const target = event.target as HTMLInputElement;
146
153
  const files = target.files;
@@ -161,7 +168,11 @@ const handleFileUpload = (event: Event) => {
161
168
  return;
162
169
  }
163
170
 
164
- uploadedFiles.value.push({ name: file.name, size: file.size, file });
171
+ if (props.enableCrop && isImageFile(file)) {
172
+ openCropModal(file);
173
+ } else {
174
+ uploadedFiles.value.push({ name: file.name, size: file.size, file });
175
+ }
165
176
  });
166
177
 
167
178
  isUploading.value = false;
@@ -169,6 +180,29 @@ const handleFileUpload = (event: Event) => {
169
180
  }
170
181
  };
171
182
 
183
+ const openCropModal = (file: File) => {
184
+ const imageUrl = URL.createObjectURL(file);
185
+
186
+ modal.open('crop-modal', {
187
+ closable: false,
188
+ imageUrl,
189
+ fileName: file.name,
190
+ cancelText: props.cropTexts?.cancel,
191
+ confirmText: props.cropTexts?.confirm,
192
+ onConfirm: (croppedFile: File) => {
193
+ uploadedFiles.value.push({
194
+ name: croppedFile.name,
195
+ size: croppedFile.size,
196
+ file: croppedFile
197
+ });
198
+ URL.revokeObjectURL(imageUrl);
199
+ },
200
+ onCancel: () => {
201
+ URL.revokeObjectURL(imageUrl);
202
+ }
203
+ }, CropModal);
204
+ };
205
+
172
206
  const removeFile = (index: number, isMedia: boolean) => {
173
207
  const files = isMedia ? mediaFiles.value : otherFiles.value;
174
208
  const globalIndex = uploadedFiles.value.indexOf(files[index]);
@@ -0,0 +1,210 @@
1
+ <template>
2
+ <div class="crop-modal">
3
+ <div class="crop-modal__container">
4
+ <div class="crop-modal__body">
5
+ <div class="crop-modal__cropper-container">
6
+ <Cropper
7
+ ref="cropperRef"
8
+ class="crop-modal__cropper"
9
+ :src="modalProps.imageUrl"
10
+ :stencil-props="{
11
+ aspectRatio: NaN,
12
+ movable: true,
13
+ resizable: true,
14
+ }"
15
+ :resize-image="{
16
+ adjustStencil: false
17
+ }"
18
+ />
19
+ </div>
20
+ </div>
21
+
22
+ <div class="crop-modal__footer">
23
+ <base-button
24
+ color="secondary"
25
+ size="small"
26
+ @click="handleCancel"
27
+ class="crop-modal__button"
28
+ >
29
+ {{ modalProps.cancelText || 'Отменить' }}
30
+ </base-button>
31
+
32
+ <base-button
33
+ color="primary"
34
+ size="small"
35
+ @click="handleConfirm"
36
+ class="crop-modal__button"
37
+ >
38
+ {{ modalProps.confirmText || 'Применить' }}
39
+ </base-button>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import {ref} from 'vue';
47
+ import {Cropper} from 'vue-advanced-cropper';
48
+ import 'vue-advanced-cropper/dist/style.css';
49
+ import BaseButton from '../BaseButton/BaseButton.vue';
50
+ import {useModal} from '../../composables/useModal';
51
+
52
+ interface CropModalProps {
53
+ modalProps: {
54
+ imageUrl: string;
55
+ fileName: string;
56
+ onConfirm: (croppedFile: File) => void;
57
+ onCancel: () => void;
58
+ cancelText?: string;
59
+ confirmText?: string;
60
+ };
61
+ }
62
+
63
+ const props = defineProps<CropModalProps>();
64
+ const modal = useModal();
65
+
66
+ const cropperRef = ref();
67
+
68
+ const handleCancel = () => {
69
+ props.modalProps.onCancel();
70
+ modal.close('crop-modal');
71
+ };
72
+
73
+ const handleConfirm = () => {
74
+ if (!cropperRef.value) return;
75
+
76
+ const {canvas} = cropperRef.value.getResult();
77
+
78
+ if (canvas) {
79
+ canvas.toBlob((blob: Blob | null) => {
80
+ if (blob) {
81
+ const croppedFile = new File([blob], props.modalProps.fileName, {
82
+ type: 'image/jpeg',
83
+ lastModified: Date.now(),
84
+ });
85
+
86
+ props.modalProps.onConfirm(croppedFile);
87
+ modal.close('crop-modal');
88
+ }
89
+ }, 'image/jpeg', 0.9);
90
+ }
91
+ };
92
+ </script>
93
+
94
+ <style lang="scss" scoped>
95
+ @import '../../styles/variables';
96
+ @import '../../styles/root';
97
+
98
+ .crop-modal {
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ max-width: 588px;
103
+ max-height: 776px;
104
+
105
+ @media #{$mobile-max-576} {
106
+ max-width: 100%;
107
+ width: 100%;
108
+ }
109
+
110
+ &__container {
111
+ width: 100%;
112
+ background: rgba(0, 0, 0, 0.3);
113
+ }
114
+
115
+ &__body {
116
+ flex: 1;
117
+ overflow: hidden;
118
+ }
119
+
120
+ &__cropper-container {
121
+ height: 500px;
122
+ width: 100%;
123
+ overflow: hidden;
124
+ display: flex;
125
+ align-items: center;
126
+
127
+ @media #{$mobile-max-375} {
128
+ overflow: unset;
129
+ }
130
+ }
131
+
132
+ &__footer {
133
+ display: flex;
134
+ gap: var(--spacing-xl);
135
+ justify-content: space-between;
136
+ padding: var(--spacing-xl);
137
+ background-color: var(--primary-black-white);
138
+
139
+ @media #{$mobile-max-576} {
140
+ flex-direction: column;
141
+ justify-content: center;
142
+ gap: var(--spacing-l);
143
+ }
144
+ }
145
+
146
+ &__button {
147
+ min-width: 258px;
148
+
149
+ @media #{$mobile-landscape} {
150
+ min-width: unset;
151
+ width: 100%;
152
+ }
153
+ }
154
+ }
155
+
156
+ ::v-deep(.vue-advanced-cropper__background) {
157
+ background: rgba(0, 0, 0, 0.3);
158
+ width: 100%;
159
+ }
160
+
161
+ ::v-deep(.vue-advanced-cropper__foreground) {
162
+ border-color: var(--primary-blue);
163
+ background: rgba(0, 0, 0, 0.3);
164
+ }
165
+
166
+ ::v-deep(.vue-simple-handler) {
167
+ background: var(--primary-blue);
168
+ border-color: white;
169
+ }
170
+
171
+ ::v-deep(.vue-simple-handler:hover) {
172
+ background: var(--primary-blue-deep);
173
+ }
174
+
175
+ ::v-deep(.vue-simple-line) {
176
+ border-color: var(--primary-blue);
177
+ }
178
+ </style>
179
+
180
+ <style lang="scss">
181
+ @import '../../styles/variables';
182
+ @import '../../styles/root';
183
+
184
+ .base-modal:has(.crop-modal) {
185
+ @media screen and (max-width: 375px) {
186
+ max-height: unset !important;
187
+ height: 100vh !important;
188
+ }
189
+ }
190
+
191
+ .base-modal__content:has(.crop-modal) {
192
+ width: 100% !important;
193
+
194
+ @media screen and (max-width: 375px) {
195
+ overflow-y: unset !important;
196
+ }
197
+ }
198
+
199
+ .base-modal__wrapper:has(.crop-modal) {
200
+ padding: 0 !important;
201
+
202
+ @media #{$mobile-max-767} {
203
+ padding: 0 !important;
204
+ }
205
+
206
+ @media screen and (max-width: 375px) {
207
+ justify-content: start !important;
208
+ }
209
+ }
210
+ </style>
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ import BaseBadge from "./components/BaseBadge/BaseBadge.vue";
36
36
  import BaseTag from "./components/BaseTag/BaseTag.vue";
37
37
  import BaseBadgeGroup from "./components/BaseBadge/BaseBadgeGroup.vue";
38
38
  import BaseField from "./components/BaseField/BaseField.vue";
39
+ import BaseTabs from "./components/BaseTabs/BaseTabs.vue";
39
40
 
40
41
  const components = {
41
42
  BaseModal,
@@ -69,7 +70,8 @@ const components = {
69
70
  BaseChips,
70
71
  BaseSwiper,
71
72
  BaseUpload,
72
- BaseField
73
+ BaseField,
74
+ BaseTabs
73
75
  };
74
76
 
75
77
  // Функция для загрузки sprite.svg
@@ -156,5 +158,6 @@ export {
156
158
  BaseChips,
157
159
  BaseSwiper,
158
160
  BaseUpload,
159
- BaseField
161
+ BaseField,
162
+ BaseTabs
160
163
  };
@@ -0,0 +1,17 @@
1
+ import type { ICoreSize } from './utils';
2
+
3
+ type IconComponent = Component | (new () => Component) | (() => Promise<{ default: Component }>);
4
+
5
+ interface ITab {
6
+ title: string;
7
+ id: number;
8
+ icon?: IconComponent;
9
+ disabled?: boolean
10
+ }
11
+
12
+ interface ITabs {
13
+ tabs: ITab[];
14
+ modelValue: number,
15
+ }
16
+
17
+ export type TTabProps = ICoreSize & ITabs
@@ -4,10 +4,17 @@ export interface UploadedFile {
4
4
  file: File;
5
5
  }
6
6
 
7
+ export interface CropTexts {
8
+ cancel?: string;
9
+ confirm?: string;
10
+ }
11
+
7
12
  export interface IpropsUpload {
8
13
  acceptedFormats: string[]; // Фильтр по типу файлов (например, "image/*" для изображений)
9
14
  multiple?: boolean; // Разрешить множественный выбор файлов
10
15
  maxFileSize?: number; // Максимальный размер файла в байтах
16
+ enableCrop?: boolean; // Включить функциональность кропа для изображений
17
+ cropTexts?: CropTexts; // Тексты для кнопок в модальном окне кропа
11
18
  onUpload?: (files: UploadedFile[]) => void; // Callback при загрузке файлов
12
19
  onError?: (error: string) => void; // Callback при ошибке загрузки
13
20
  }