@finema/core 1.4.89 → 1.4.91

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/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "1.4.89",
3
+ "version": "1.4.91",
4
4
  "configKey": "core",
5
5
  "compatibility": {
6
6
  "nuxt": "^3.7.4"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineNuxtModule, createResolver, installModule, addPlugin, addComponentsDir, addImportsDir } from '@nuxt/kit';
2
2
 
3
3
  const name = "@finema/core";
4
- const version = "1.4.89";
4
+ const version = "1.4.91";
5
5
 
6
6
  const colors = {
7
7
  black: "#20243E",
@@ -341,7 +341,8 @@ const module = defineNuxtModule({
341
341
  files: [
342
342
  ...tailwindConfig.content.files || [],
343
343
  resolve(runtimeDir, "components/**/*.{vue,mjs,ts}"),
344
- resolve(runtimeDir, "ui.config/**/*.{mjs,js,ts}")
344
+ resolve(runtimeDir, "ui.config/**/*.{mjs,js,ts}"),
345
+ resolve(runtimeDir, "presets/**/*.{mjs,js,ts}")
345
346
  ]
346
347
  };
347
348
  tailwindConfig.theme.extend.colors = {
@@ -11,12 +11,6 @@
11
11
  },
12
12
  ]"
13
13
  >
14
- <Icon
15
- v-if="selectedFile"
16
- :name="ui.default.closeIcon"
17
- :class="[ui.button.delete]"
18
- @click="handleDeleteFile"
19
- />
20
14
  <input
21
15
  ref="fileInputRef"
22
16
  type="file"
@@ -27,18 +21,6 @@
27
21
  @change="handleChange"
28
22
  />
29
23
  <div :class="[ui.wrapper]">
30
- <div v-if="selectedFile" :class="[ui.preview.wrapper]">
31
- <div v-if="isImage(selectedFile)" :class="[ui.preview.image.wrapper]">
32
- <img :src="generateURL(selectedFile)" :class="[ui.preview.image.img]" alt="file" />
33
- </div>
34
- <Icon v-else :name="ui.default.filePreviewIcon" :class="[ui.preview.icon]" />
35
- <div :class="[ui.preview.filename]">
36
- <p class="truncate">{{ selectedFile.name }}</p>
37
- </div>
38
- <Badge size="xs" variant="outline" class="mt-1">
39
- {{ isSelectedFileUseMb ? `${selectedFileSizeMb} Mb` : `${selectedFileSizeKb} Kb` }}
40
- </Badge>
41
- </div>
42
24
  <div v-if="!selectedFile" :class="[ui.placeholderWrapper]">
43
25
  <Icon :name="ui.default.uploadIcon" :class="[ui.labelIcon]" />
44
26
  <div :class="[ui.labelWrapper]">
@@ -49,6 +31,67 @@
49
31
  </div>
50
32
  <p v-if="placeholder" :class="[ui.placeholder]">{{ placeholder }}</p>
51
33
  </div>
34
+
35
+ <!-- Selected State -->
36
+ <div v-if="selectedFile" :class="[ui.onPreview.wrapper]">
37
+ <div :class="[ui.onPreview.previewImgWrapper]">
38
+ <div v-if="isImage(selectedFile)" class="size-full overflow-hidden">
39
+ <img
40
+ :src="generateURL(selectedFile)"
41
+ :class="[ui.onPreview.previewImgClass]"
42
+ alt="image-preview"
43
+ />
44
+ </div>
45
+ <div v-else>
46
+ <Icon
47
+ :name="ui.onPreview.previewFileIcon || ui.default.filePreviewIcon"
48
+ :class="[ui.onPreview.previewFileClass]"
49
+ />
50
+ </div>
51
+ </div>
52
+ <div :class="[ui.onPreview.textWrapper]">
53
+ <div class="truncate">
54
+ <h1 class="truncate font-bold">{{ selectedFile?.name }}</h1>
55
+ <p class="truncate font-light text-gray-400">
56
+ {{
57
+ fileAllocate.isSelectedFileUseMb
58
+ ? `${fileAllocate.selectedFileSizeMb} MB`
59
+ : `${fileAllocate.selectedFileSizeKb} KB`
60
+ }}
61
+ </p>
62
+ </div>
63
+ <div :class="[ui.action.wrapper]">
64
+ <PImage
65
+ v-if="isImage(selectedFile)"
66
+ alt="image-popup-preview"
67
+ preview
68
+ :pt="{ button: 'absolute opacity-0 inset-0' }"
69
+ >
70
+ <template #image>
71
+ <Icon :name="ui.action.previewIcon" :class="[ui.action.iconClass]" />
72
+ </template>
73
+ <template #preview="slotProps">
74
+ <img
75
+ :src="generateURL(selectedFile)"
76
+ alt="preview"
77
+ :style="slotProps.style"
78
+ @click="slotProps.previewCallback"
79
+ />
80
+ </template>
81
+ </PImage>
82
+ <Icon
83
+ :name="ui.action.downloadIcon"
84
+ :class="[ui.action.iconClass]"
85
+ @click="handleDownloadFile"
86
+ />
87
+ <Icon
88
+ :name="ui.action.deleteIcon"
89
+ :class="[ui.action.iconClass]"
90
+ @click="handleDeleteFile"
91
+ />
92
+ </div>
93
+ </div>
94
+ </div>
52
95
  </div>
53
96
  </div>
54
97
  </FieldWrapper>
@@ -57,7 +100,14 @@
57
100
  <script lang="ts" setup>
58
101
  import { useDropZone } from '@vueuse/core'
59
102
  import { type IUploadDropzoneProps } from './types'
60
- import { isImage, generateURL, checkMaxSize, checkFileType } from '#core/helpers/componentHelper'
103
+ import {
104
+ isImage,
105
+ generateURL,
106
+ checkMaxSize,
107
+ checkFileType,
108
+ getFileAllocate,
109
+ downloadFileFromURL,
110
+ } from '#core/helpers/componentHelper'
61
111
  import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
62
112
  import { useFieldHOC } from '#core/composables/useForm'
63
113
  import { computed, ref, toRef, useUI, useUiConfig } from '#imports'
@@ -80,13 +130,10 @@ const acceptFile = computed(() =>
80
130
  typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
81
131
  )
82
132
 
83
- const acceptFileSizeKb = computed(() => props.maxSize)
84
- const acceptFileSizeMb = computed(() => ((acceptFileSizeKb.value ?? 0) / 1024).toFixed(2))
85
- const isAcceptFileUseMb = computed(() => acceptFileSizeKb.value && acceptFileSizeKb.value > 1024)
133
+ const fileAllocate = computed(() =>
134
+ getFileAllocate(props.maxSize ?? 0, selectedFile.value?.size ?? 0)
135
+ )
86
136
 
87
- const selectedFileSizeKb = computed(() => ((value?.value.size || 0) / 1000).toFixed(2))
88
- const selectedFileSizeMb = computed(() => ((value?.value.size || 0) / 1000 / 1000).toFixed(2))
89
- const isSelectedFileUseMb = computed(() => (value?.value.size || 0) / 1000 > 1024)
90
137
  const selectedFile = computed(() => value?.value)
91
138
 
92
139
  const onDrop = (files: File[] | null) => {
@@ -128,6 +175,10 @@ const handleDeleteFile = () => {
128
175
  emit('delete')
129
176
  }
130
177
 
178
+ const handleDownloadFile = () => {
179
+ downloadFileFromURL(generateURL(selectedFile.value), selectedFile.value?.name)
180
+ }
181
+
131
182
  const handleCheckFileCondition = (file: File | undefined): boolean => {
132
183
  if (!file) return false
133
184
 
@@ -139,13 +190,17 @@ const handleCheckFileCondition = (file: File | undefined): boolean => {
139
190
  return false
140
191
  }
141
192
 
142
- const maxSize = checkMaxSize(file, acceptFileSizeKb.value ?? 0)
193
+ const maxSize = checkMaxSize(file, fileAllocate.value.acceptFileSizeKb ?? 0)
143
194
 
144
195
  if (!maxSize) {
145
- if (isAcceptFileUseMb.value) {
146
- setErrors(i18next.t('custom:invalid_file_size_mb', { size: acceptFileSizeMb.value }))
196
+ if (fileAllocate.value.isAcceptFileUseMb) {
197
+ setErrors(
198
+ i18next.t('custom:invalid_file_size_mb', { size: fileAllocate.value.acceptFileSizeMb })
199
+ )
147
200
  } else {
148
- setErrors(i18next.t('custom:invalid_file_size_kb', { size: acceptFileSizeKb.value }))
201
+ setErrors(
202
+ i18next.t('custom:invalid_file_size_kb', { size: fileAllocate.value.acceptFileSizeKb })
203
+ )
149
204
  }
150
205
 
151
206
  return false
@@ -8,15 +8,10 @@
8
8
  [ui.disabled]: isDisabled,
9
9
  [ui.background.default]: !isOverDropZone && !isDisabled,
10
10
  [ui.background.dragover]: isOverDropZone && !isDisabled,
11
+ [ui.failed]: upload.status.value.isError,
11
12
  },
12
13
  ]"
13
14
  >
14
- <Icon
15
- v-if="selectedFile"
16
- :name="ui.default.closeIcon"
17
- :class="[ui.button.delete]"
18
- @click="handleDeleteFile"
19
- />
20
15
  <input
21
16
  ref="fileInputRef"
22
17
  type="file"
@@ -27,51 +22,141 @@
27
22
  @change="handleChange"
28
23
  />
29
24
  <div :class="[ui.wrapper]">
30
- <div v-if="selectedFile" :class="[ui.preview.wrapper]">
31
- <div
32
- v-if="isImage(selectedFile) && upload.status.value.isLoaded"
33
- :class="[ui.preview.image.wrapper]"
34
- >
35
- <img
36
- :src="upload.data.value[responseKey || 'url']"
37
- :class="[ui.preview.image.img]"
38
- alt="file"
39
- />
25
+ <div v-if="!selectedFile" :class="[ui.placeholderWrapper]">
26
+ <Icon :name="ui.default.uploadIcon" :class="[ui.labelIcon]" />
27
+ <div :class="[ui.labelWrapper]">
28
+ <p class="text-primary cursor-pointer" @click="handleOpenFile">
29
+ {{ selectFileLabel ?? 'คลิกเพื่อเลือกไฟล์' }}
30
+ </p>
31
+ <p>{{ selectFileSubLabel ?? 'หรือ ลากและวางที่นี่' }}</p>
40
32
  </div>
41
- <Icon v-else :name="ui.default.filePreviewIcon" :class="[ui.preview.icon]" />
42
- <div :class="[ui.preview.filename]">
43
- <p class="truncate">{{ selectedFile.name }}</p>
33
+ <p v-if="placeholder" :class="[ui.placeholder]">{{ placeholder }}</p>
34
+ </div>
35
+
36
+ <!-- Loading State -->
37
+ <div v-if="selectedFile && upload.status.value.isLoading" :class="[ui.onLoading.wrapper]">
38
+ <div :class="[ui.onLoading.placeholderWrapper]">
39
+ <Icon
40
+ :name="
41
+ isImage(selectedFile)
42
+ ? ui.onLoading.placeholderImgIcon || ui.default.placeholderImgIcon
43
+ : ui.onLoading.placeholderFileIcon || ui.default.filePreviewIcon
44
+ "
45
+ :class="[ui.onLoading.placeholderIconClass]"
46
+ />
44
47
  </div>
45
- <div class="flex items-center space-x-1">
46
- <Badge size="xs" variant="outline" class="mt-1">
47
- {{ isSelectedFileUseMb ? `${selectedFileSizeMb} MB` : `${selectedFileSizeKb} KB` }}
48
- </Badge>
48
+ <div :class="[ui.onLoading.textWrapper]">
49
+ <div class="truncate">
50
+ <h1 class="truncate font-bold">{{ selectedFile?.name }}</h1>
51
+ <p class="truncate font-light text-gray-400">
52
+ {{
53
+ fileAllocate.isSelectedFileUseMb
54
+ ? `${fileAllocate.selectedFileSizeMb} MB`
55
+ : `${fileAllocate.selectedFileSizeKb} KB`
56
+ }}
57
+ - {{ percent }}% {{ uploadingLabel ?? 'กำลังอัพโหลด...' }}
58
+ </p>
59
+ </div>
49
60
  <div>
50
61
  <Icon
51
- v-if="upload.status.value.isSuccess"
52
- name="heroicons:check-circle-20-solid"
53
- class="text-success"
62
+ :name="ui.onLoading.loadingIcon || ui.default.loadingIcon"
63
+ :class="[ui.onLoading.loadingIconClass]"
54
64
  />
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <!-- Success State -->
70
+ <div v-if="selectedFile && upload.status.value.isSuccess" :class="[ui.onPreview.wrapper]">
71
+ <div :class="[ui.onPreview.previewImgWrapper]">
72
+ <div v-if="isImage(selectedFile)" class="size-full overflow-hidden">
73
+ <img
74
+ :src="upload.data.value[responseKey || 'url']"
75
+ :class="[ui.onPreview.previewImgClass]"
76
+ alt="image-preview"
77
+ />
78
+ </div>
79
+ <div v-else>
55
80
  <Icon
56
- v-if="upload.status.value.isError"
57
- name="heroicons:x-circle-20-solid"
58
- class="text-danger"
81
+ :name="ui.onPreview.previewFileIcon || ui.default.filePreviewIcon"
82
+ :class="[ui.onPreview.previewFileClass]"
59
83
  />
60
84
  </div>
61
85
  </div>
86
+ <div :class="[ui.onPreview.textWrapper]">
87
+ <div class="truncate">
88
+ <h1 class="truncate font-bold">{{ selectedFile?.name }}</h1>
89
+ <p class="truncate text-sm font-light text-gray-400">
90
+ {{
91
+ fileAllocate.isSelectedFileUseMb
92
+ ? `${fileAllocate.selectedFileSizeMb} MB`
93
+ : `${fileAllocate.selectedFileSizeKb} KB`
94
+ }}
95
+ </p>
96
+ </div>
97
+ <div :class="[ui.action.wrapper]">
98
+ <Icon
99
+ v-if="isImage(selectedFile)"
100
+ :name="ui.action.previewIcon"
101
+ :class="[ui.action.iconClass]"
102
+ title="ดูตัวอย่าง"
103
+ @click="() => (isPreviewOpen = true)"
104
+ />
105
+ <Icon
106
+ :name="ui.action.downloadIcon"
107
+ :class="[ui.action.iconClass]"
108
+ title="ดาวน์โหลดไฟล์"
109
+ @click="handleDownloadFile"
110
+ />
111
+ <Icon
112
+ :name="ui.action.deleteIcon"
113
+ :class="[ui.action.iconClass]"
114
+ title="ลบไฟล์"
115
+ @click="handleDeleteFile"
116
+ />
117
+ <Modal v-model="isPreviewOpen" :title="selectedFile?.name">
118
+ <img :src="upload.data.value[responseKey || 'url']" alt="image-preview" />
119
+ </Modal>
120
+ </div>
121
+ </div>
62
122
  </div>
63
- <div v-if="upload.status.value.isLoading" :class="[ui.uploadStatus.wrapper]">
64
- <UProgress :class="[ui.uploadStatus.progressBar]" :value="100" />
65
- </div>
66
- <div v-if="!selectedFile" :class="[ui.placeholderWrapper]">
67
- <Icon :name="ui.default.uploadIcon" :class="[ui.labelIcon]" />
68
- <div :class="[ui.labelWrapper]">
69
- <p class="text-primary cursor-pointer" @click="handleOpenFile">
70
- {{ selectFileLabel ?? 'คลิกเพื่อเลือกไฟล์' }}
71
- </p>
72
- <p>{{ selectFileSubLabel ?? 'หรือ ลากและวางที่นี่' }}</p>
123
+
124
+ <!-- Failed State -->
125
+ <div v-if="selectedFile && upload.status.value.isError" :class="[ui.onFailed.wrapper]">
126
+ <div :class="[ui.onFailed.failedImgWrapper]">
127
+ <Icon
128
+ :name="
129
+ isImage(selectedFile)
130
+ ? ui.onFailed.failedImgIcon || ui.default.placeholderImgIcon
131
+ : ui.onFailed.failedFileIcon || ui.default.filePreviewIcon
132
+ "
133
+ :class="[ui.onFailed.failedIconClass]"
134
+ />
135
+ </div>
136
+ <div :class="[ui.onFailed.textWrapper]">
137
+ <div class="truncate">
138
+ <h1 class="truncate font-bold">{{ selectedFile?.name }}</h1>
139
+ <p class="text-danger truncate font-light">
140
+ {{ uploadFailedLabel || 'อัพโหลดล้มเหลว, กรุณาลองอีกครั้ง' }}
141
+ </p>
142
+ <Button
143
+ variant="ghost"
144
+ :label="retryLabel || 'ลองอีกครั้ง'"
145
+ :leading-icon="ui.action.retryIcon"
146
+ :class="[ui.action.retryBtnClass]"
147
+ size="sm"
148
+ @click="handleRetryUpload"
149
+ />
150
+ </div>
151
+ <div :class="[ui.action.wrapper]">
152
+ <Icon
153
+ title="ลบไฟล์"
154
+ :name="ui.action.deleteIcon"
155
+ :class="[ui.action.deleteIconClass]"
156
+ @click="handleDeleteFile"
157
+ />
158
+ </div>
73
159
  </div>
74
- <p v-if="placeholder" :class="[ui.placeholder]">{{ placeholder }}</p>
75
160
  </div>
76
161
  </div>
77
162
  </div>
@@ -81,7 +166,13 @@
81
166
  <script lang="ts" setup>
82
167
  import { useDropZone } from '@vueuse/core'
83
168
  import { type IUploadDropzoneAutoProps } from './types'
84
- import { isImage, checkMaxSize, checkFileType } from '#core/helpers/componentHelper'
169
+ import {
170
+ checkMaxSize,
171
+ checkFileType,
172
+ isImage,
173
+ getFileAllocate,
174
+ downloadFileFromURL,
175
+ } from '#core/helpers/componentHelper'
85
176
  import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
86
177
  import { useFieldHOC } from '#core/composables/useForm'
87
178
  import {
@@ -118,23 +209,17 @@ const { ui } = useUI('uploadFileDropzone', toRef(props, 'ui'), config)
118
209
  const fileInputRef = ref<HTMLInputElement>()
119
210
  const dropzoneRef = ref<HTMLDivElement>()
120
211
  const selectedFile = ref<File | undefined>()
121
- const percent = ref<number>(0)
212
+ const percent = ref<number | string>(0)
213
+ const isPreviewOpen = ref<boolean>(false)
122
214
 
123
215
  const acceptFile = computed(() =>
124
216
  typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
125
217
  )
126
218
 
127
- const acceptFileSizeKb = computed(() => props.maxSize)
128
- const acceptFileSizeMb = computed(() => ((acceptFileSizeKb.value ?? 0) / 1024).toFixed(2))
129
- const isAcceptFileUseMb = computed(() => acceptFileSizeKb.value && acceptFileSizeKb.value > 1024)
130
-
131
- const selectedFileSizeKb = computed(() => ((selectedFile.value?.size || 0) / 1000).toFixed(2))
132
- const selectedFileSizeMb = computed(() =>
133
- ((selectedFile.value?.size || 0) / 1000 / 1000).toFixed(2)
219
+ const fileAllocate = computed(() =>
220
+ getFileAllocate(props.maxSize ?? 0, selectedFile.value?.size ?? 0)
134
221
  )
135
222
 
136
- const isSelectedFileUseMb = computed(() => (selectedFile.value?.size || 0) / 1000 > 1024)
137
-
138
223
  const onDrop = (files: File[] | null) => {
139
224
  if (props.isDisabled || files?.length === 0 || !files) return
140
225
 
@@ -195,13 +280,17 @@ const handleCheckFileCondition = (file: File | undefined): boolean => {
195
280
  return false
196
281
  }
197
282
 
198
- const maxSize = checkMaxSize(file, acceptFileSizeKb.value ?? 0)
283
+ const maxSize = checkMaxSize(file, fileAllocate.value.acceptFileSizeKb ?? 0)
199
284
 
200
285
  if (!maxSize) {
201
- if (isAcceptFileUseMb.value) {
202
- setErrors(i18next.t('custom:invalid_file_size_mb', { size: acceptFileSizeMb.value }))
286
+ if (fileAllocate.value.isAcceptFileUseMb) {
287
+ setErrors(
288
+ i18next.t('custom:invalid_file_size_mb', { size: fileAllocate.value.acceptFileSizeMb })
289
+ )
203
290
  } else {
204
- setErrors(i18next.t('custom:invalid_file_size_kb', { size: acceptFileSizeKb.value }))
291
+ setErrors(
292
+ i18next.t('custom:invalid_file_size_kb', { size: fileAllocate.value.acceptFileSizeKb })
293
+ )
205
294
  }
206
295
 
207
296
  return false
@@ -212,8 +301,23 @@ const handleCheckFileCondition = (file: File | undefined): boolean => {
212
301
  return true
213
302
  }
214
303
 
304
+ const handleDownloadFile = () => {
305
+ downloadFileFromURL(_get(upload.data.value, props.responseKey || 'url'), selectedFile.value?.name)
306
+ }
307
+
308
+ const handleRetryUpload = () => {
309
+ if (selectedFile.value) {
310
+ const formData = new FormData()
311
+
312
+ formData.append(props.bodyKey || 'file', selectedFile.value)
313
+ upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
314
+ }
315
+ }
316
+
215
317
  const onUploadProgress = (progressEvent: ProgressEvent) => {
216
- percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
318
+ percent.value = StringHelper.withFixed(
319
+ (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
320
+ )
217
321
  }
218
322
 
219
323
  const onDownloadProgress = (progressEvent: ProgressEvent) => {
@@ -223,7 +327,9 @@ const onDownloadProgress = (progressEvent: ProgressEvent) => {
223
327
  return
224
328
  }
225
329
 
226
- percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
330
+ percent.value = StringHelper.withFixed(
331
+ (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
332
+ )
227
333
  }
228
334
 
229
335
  useWatchTrue(
@@ -7,6 +7,9 @@ export interface IUploadDropzoneAutoProps extends IFieldProps {
7
7
  uploadPathURL?: string;
8
8
  selectFileLabel?: string;
9
9
  selectFileSubLabel?: string;
10
+ uploadingLabel?: string;
11
+ uploadFailedLabel?: string;
12
+ retryLabel?: string;
10
13
  accept?: string[] | string;
11
14
  bodyKey?: string;
12
15
  responseKey?: string;
@@ -19,7 +19,7 @@
19
19
  <Icon
20
20
  v-if="!isHideCloseBtn"
21
21
  name="i-heroicons-x-mark"
22
- class="h-6 w-6 cursor-pointer opacity-50"
22
+ class="size-6 cursor-pointer opacity-50"
23
23
  @click="close"
24
24
  />
25
25
  </div>
@@ -31,7 +31,7 @@
31
31
  <Icon
32
32
  v-if="!isHideCloseBtn"
33
33
  name="i-heroicons-x-mark"
34
- class="h-6 w-6 cursor-pointer opacity-50"
34
+ class="size-6 cursor-pointer opacity-50"
35
35
  @click="close"
36
36
  />
37
37
  </div>
@@ -2,3 +2,12 @@ export declare const checkMaxSize: (file: File, acceptFileSize: number) => boole
2
2
  export declare const checkFileType: (file: File, acceptFileType: string | string[]) => boolean;
3
3
  export declare const generateURL: (file: File) => string;
4
4
  export declare const isImage: (file: File) => boolean;
5
+ export declare const getFileAllocate: (maxSize: number, selectedFileSize: Blob['size']) => {
6
+ acceptFileSizeKb: number;
7
+ acceptFileSizeMb: string;
8
+ isAcceptFileUseMb: boolean;
9
+ selectedFileSizeKb: string;
10
+ selectedFileSizeMb: string;
11
+ isSelectedFileUseMb: boolean;
12
+ };
13
+ export declare const downloadFileFromURL: (url: string, filename?: string) => Promise<void>;
@@ -37,3 +37,33 @@ export const generateURL = (file) => {
37
37
  export const isImage = (file) => {
38
38
  return file.type.startsWith("image/");
39
39
  };
40
+ export const getFileAllocate = (maxSize, selectedFileSize) => {
41
+ return {
42
+ acceptFileSizeKb: maxSize,
43
+ acceptFileSizeMb: (maxSize / 1024).toFixed(2),
44
+ isAcceptFileUseMb: maxSize > 1024,
45
+ selectedFileSizeKb: (selectedFileSize / 1e3).toFixed(2),
46
+ selectedFileSizeMb: (selectedFileSize / 1e3 / 1e3).toFixed(2),
47
+ isSelectedFileUseMb: selectedFileSize / 1e3 > 1024
48
+ };
49
+ };
50
+ export const downloadFileFromURL = async (url, filename) => {
51
+ return new Promise((resolve, reject) => {
52
+ const xhr = new XMLHttpRequest();
53
+ xhr.responseType = "blob";
54
+ xhr.onload = () => {
55
+ const a = document.createElement("a");
56
+ const url2 = window.URL.createObjectURL(xhr.response);
57
+ a.href = url2;
58
+ a.download = filename || url2.split("/").pop() || (/* @__PURE__ */ new Date()).toISOString();
59
+ a.click();
60
+ window.URL.revokeObjectURL(url2);
61
+ resolve();
62
+ };
63
+ xhr.onerror = () => {
64
+ reject();
65
+ };
66
+ xhr.open("GET", url);
67
+ xhr.send();
68
+ });
69
+ };
@@ -2,33 +2,56 @@ export declare const uploadFileDropzone: {
2
2
  base: string;
3
3
  wrapper: string;
4
4
  disabled: string;
5
+ failed: string;
5
6
  placeholderWrapper: string;
6
7
  placeholder: string;
7
8
  labelWrapper: string;
8
- preview: {
9
+ onLoading: {
9
10
  wrapper: string;
10
- icon: string;
11
- image: {
12
- wrapper: string;
13
- img: string;
14
- };
15
- filename: string;
11
+ placeholderWrapper: string;
12
+ placeholderImgIcon: string;
13
+ placeholderFileIcon: string;
14
+ placeholderIconClass: string;
15
+ textWrapper: string;
16
+ loadingIcon: string;
17
+ loadingIconClass: string;
16
18
  };
17
- uploadStatus: {
19
+ onPreview: {
18
20
  wrapper: string;
19
- progressBar: string;
21
+ previewImgWrapper: string;
22
+ previewImgClass: string;
23
+ previewFileIcon: string;
24
+ previewFileClass: string;
25
+ textWrapper: string;
26
+ };
27
+ onFailed: {
28
+ wrapper: string;
29
+ failedImgWrapper: string;
30
+ failedImgIcon: string;
31
+ failedFileIcon: string;
32
+ failedIconClass: string;
33
+ textWrapper: string;
34
+ };
35
+ action: {
36
+ wrapper: string;
37
+ iconClass: string;
38
+ deleteIconClass: string;
39
+ retryBtnClass: string;
40
+ previewIcon: string;
41
+ downloadIcon: string;
42
+ deleteIcon: string;
43
+ retryIcon: string;
20
44
  };
21
45
  background: {
22
46
  default: string;
23
47
  dragover: string;
24
48
  };
25
- button: {
26
- delete: string;
27
- };
28
49
  labelIcon: string;
29
50
  default: {
30
- closeIcon: string;
31
51
  filePreviewIcon: string;
32
52
  uploadIcon: string;
53
+ placeholderImgIcon: string;
54
+ failedImgIcon: string;
55
+ loadingIcon: string;
33
56
  };
34
57
  };
@@ -1,34 +1,57 @@
1
1
  export const uploadFileDropzone = {
2
- base: "relative w-full p-8 transition rounded-lg flex items-center justify-center border-2 ring-0 min-h-[250px]",
2
+ base: "relative w-full p-4 transition rounded-lg flex items-center justify-center border-[1px] ring-0",
3
3
  wrapper: "flex flex-col items-center w-full",
4
- disabled: "bg-gray-disabled cursor-not-allowed",
5
- placeholderWrapper: "flex flex-col items-center justify-center",
4
+ disabled: "bg-gray-100 border-none grayscale cursor-not-allowed",
5
+ failed: "border-danger",
6
+ placeholderWrapper: "py-4 flex flex-col items-center justify-center",
6
7
  placeholder: "text-gray-400 text-center font-light text-sm truncate",
7
8
  labelWrapper: "flex items-center space-x-2 text-gray-400 text-center",
8
- preview: {
9
- wrapper: "flex flex-col items-center w-full",
10
- icon: "mb-2 h-10 w-10 text-gray-400",
11
- image: {
12
- wrapper: "flex justify-center mb-2 rounded overflow-hidden max-w-[300px]",
13
- img: "w-full object-contain"
14
- },
15
- filename: "flex items-center justify-center w-full font-semibold truncate"
9
+ onLoading: {
10
+ wrapper: "w-full flex space-x-4",
11
+ placeholderWrapper: "flex size-12 items-center justify-center rounded-lg bg-gray-100",
12
+ placeholderImgIcon: "",
13
+ placeholderFileIcon: "",
14
+ placeholderIconClass: "size-7 text-gray-400",
15
+ textWrapper: "flex w-full items-center justify-between",
16
+ loadingIcon: "",
17
+ loadingIconClass: "text-primary size-10"
16
18
  },
17
- uploadStatus: {
18
- wrapper: "w-full md:w-1/2 mt-2",
19
- progressBar: ""
19
+ onPreview: {
20
+ wrapper: "w-full flex space-x-4",
21
+ previewImgWrapper: "flex size-12 items-center justify-center rounded-lg overflow-hidden bg-gray-100",
22
+ previewImgClass: "w-full h-full object-cover",
23
+ previewFileIcon: "",
24
+ previewFileClass: "size-7 text-gray-400",
25
+ textWrapper: "flex w-full items-center justify-between"
26
+ },
27
+ onFailed: {
28
+ wrapper: "w-full flex space-x-4",
29
+ failedImgWrapper: "flex size-12 items-center justify-center rounded-lg bg-gray-100",
30
+ failedImgIcon: "",
31
+ failedFileIcon: "",
32
+ failedIconClass: "size-7 text-gray-400",
33
+ textWrapper: "flex w-full items-start justify-between"
34
+ },
35
+ action: {
36
+ wrapper: "flex items-center space-x-4",
37
+ iconClass: "size-6 text-gray-400 cursor-pointer",
38
+ deleteIconClass: "size-6 text-danger cursor-pointer",
39
+ retryBtnClass: "px-0",
40
+ previewIcon: "i-ic:outline-remove-red-eye",
41
+ downloadIcon: "i-ic:outline-file-download",
42
+ deleteIcon: "ph:trash",
43
+ retryIcon: "ph:arrow-counter-clockwise"
20
44
  },
21
45
  background: {
22
46
  default: "bg-white border-gray-border",
23
47
  dragover: "bg-gray-100 border-primary"
24
48
  },
25
- button: {
26
- delete: "cursor-pointer w-5 h-5 ml-2 text-gray-400 hover:text-gray-300 absolute top-6 right-6 "
27
- },
28
49
  labelIcon: "w-6 h-6 text-gray-400 text-center mb-4",
29
50
  default: {
30
- closeIcon: "i-heroicons:trash-solid",
31
51
  filePreviewIcon: "i-heroicons:document-text-solid",
32
- uploadIcon: "i-ph:cloud-arrow-up"
52
+ uploadIcon: "i-ph:cloud-arrow-up",
53
+ placeholderImgIcon: "i-material-symbols:imagesmode-outline",
54
+ failedImgIcon: "i-material-symbols:imagesmode-outline",
55
+ loadingIcon: "i-svg-spinners:180-ring-with-bg"
33
56
  }
34
57
  };
@@ -1,6 +1,7 @@
1
1
  export declare class StringHelper {
2
2
  static genString: (length?: number) => string;
3
3
  static withComma: (value?: number | string) => string;
4
+ static withFixed: (value?: number | string) => string;
4
5
  static split: (str: string | null | undefined, separator: string | RegExp) => string[];
5
6
  static joinURL: (...parts: string[]) => string;
6
7
  static truncate: (str: any, num?: number) => any;
@@ -12,6 +12,12 @@ export class StringHelper {
12
12
  static withComma = (value = 0) => {
13
13
  return (+(value || 0)).toLocaleString();
14
14
  };
15
+ static withFixed = (value = 0) => {
16
+ return (+(value || 0)).toLocaleString(void 0, {
17
+ minimumFractionDigits: 0,
18
+ maximumFractionDigits: 2
19
+ });
20
+ };
15
21
  static split = (str, separator) => {
16
22
  return `${str || ""}`.split(separator).filter((item) => item).map((item) => item.trim());
17
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "1.4.89",
3
+ "version": "1.4.91",
4
4
  "repository": "https://gitlab.finema.co/finema/ui-kit",
5
5
  "license": "MIT",
6
6
  "author": "Finema Dev Core Team",
@@ -34,17 +34,17 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@nuxt/kit": "^3.10.3",
37
- "@nuxt/ui": "^2.14.1",
37
+ "@nuxt/ui": "^2.14.2",
38
38
  "@pinia/nuxt": "^0.5.1",
39
39
  "@vee-validate/nuxt": "^4.12.5",
40
40
  "@vee-validate/zod": "^4.12.5",
41
- "@vuepic/vue-datepicker": "^8.1.1",
41
+ "@vuepic/vue-datepicker": "^8.2.0",
42
42
  "axios": "^1.6.7",
43
43
  "date-fns": "^3.3.1",
44
44
  "i18next": "^23.10.0",
45
45
  "maska": "^2.1.11",
46
46
  "nuxt-lodash": "^2.5.3",
47
- "nuxt-security": "^1.2.0",
47
+ "nuxt-security": "^1.2.1",
48
48
  "pinia": "^2.1.7",
49
49
  "qrcode.vue": "^3.4.1",
50
50
  "url-join": "^5.0.0",
@@ -64,17 +64,17 @@
64
64
  "changelogen": "^0.5.5",
65
65
  "eslint": "^8.56.0",
66
66
  "happy-dom": "^13.0.0",
67
- "husky": "^8.0.3",
67
+ "husky": "^9.0.11",
68
68
  "lint-staged": "^15.2.0",
69
69
  "nuxt": "^3.10.3",
70
- "playwright-core": "^1.40.1",
70
+ "playwright-core": "^1.42.1",
71
71
  "prettier": "^3.1.1",
72
72
  "release-it": "^17.0.1",
73
73
  "sass": "^1.69.5",
74
74
  "stylelint": "^16.1.0",
75
75
  "stylelint-config-prettier-scss": "^1.0.0",
76
76
  "stylelint-config-standard-scss": "^13.0.0",
77
- "vitest": "^1.2.2",
77
+ "vitest": "^1.3.1",
78
78
  "vue": "^3.4.21"
79
79
  },
80
80
  "lint-staged": {