@finema/core 2.19.1 → 2.21.0

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": "2.19.1",
3
+ "version": "2.21.0",
4
4
  "configKey": "core",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.1",
package/dist/module.mjs CHANGED
@@ -3,7 +3,7 @@ import defu from 'defu';
3
3
  import * as theme from '../dist/runtime/theme/index.js';
4
4
 
5
5
  const name = "@finema/core";
6
- const version = "2.19.1";
6
+ const version = "2.21.0";
7
7
 
8
8
  const nuxtAppOptions = {
9
9
  head: {
@@ -1,99 +1,102 @@
1
1
  <template>
2
- <div
3
- v-show="isShowDevTools"
4
- ref="devToolsRef"
5
- class="fixed z-50 overflow-hidden rounded-lg border border-neutral-300 bg-white opacity-80 shadow-2xl"
6
- :style="devToolsDynamicStyles"
7
- >
8
- <!-- Draggable Title Bar -->
9
- <div class="flex items-center justify-between px-2 py-1 select-none">
10
- <p
11
- class="flex-grow cursor-move text-sm font-semibold"
12
- @mousedown.prevent="handleDragStart"
13
- >
14
- Debug Tools
15
- </p>
16
- <div class="flex items-center">
17
- <Button
18
- icon="i-heroicons-arrow-path"
19
- size="xs"
20
- color="neutral"
21
- variant="ghost"
22
- class="mr-1"
23
- title="Reset Position & Size"
24
- @click.stop="resetDevToolsState"
25
- />
26
- <Button
27
- icon="i-heroicons-x-mark"
28
- size="xs"
29
- color="neutral"
30
- variant="ghost"
31
- title="Close DevTools"
32
- @click.stop="closeDevTools"
33
- />
34
- </div>
35
- </div>
36
- <hr class="text-neutral-300" />
37
- <!-- Content Area Target for Logs -->
38
- <div
39
- id="dev-logs"
40
- class="flex flex-1 flex-col space-y-1 overflow-auto p-2"
41
- :style="{ height: `calc(${devToolsHeight} - 40px)` }"
42
- />
43
-
44
- <!-- Resize Handles -->
45
- <div
46
- v-if="!isDragging"
47
- class="resize-handles"
48
- >
49
- <div
50
- class="resize-handle top-left"
51
- @mousedown.prevent="handleResizeStart('top-left', $event)"
52
- />
53
- <div
54
- class="resize-handle top-center"
55
- @mousedown.prevent="handleResizeStart('top', $event)"
56
- />
57
- <div
58
- class="resize-handle top-right"
59
- @mousedown.prevent="handleResizeStart('top-right', $event)"
60
- />
61
- <div
62
- class="resize-handle middle-left"
63
- @mousedown.prevent="handleResizeStart('left', $event)"
64
- />
65
- <div
66
- class="resize-handle middle-right"
67
- @mousedown.prevent="handleResizeStart('right', $event)"
68
- />
69
- <div
70
- class="resize-handle bottom-left"
71
- @mousedown.prevent="handleResizeStart('bottom-left', $event)"
72
- />
73
- <div
74
- class="resize-handle bottom-center"
75
- @mousedown.prevent="handleResizeStart('bottom', $event)"
76
- />
77
- <div
78
- class="resize-handle bottom-right"
79
- @mousedown.prevent="handleResizeStart('bottom-right', $event)"
80
- />
81
- </div>
82
- </div>
83
-
84
- <!-- Toggle button for this DevToolsWindow -->
85
- <div
86
- class="fixed right-1 bottom-1 z-[99999]"
87
- >
88
- <Button
89
- :icon="isShowDevTools ? 'heroicons:x-mark' : 'heroicons:information-circle'"
90
- color="info"
91
- square
92
- size="sm"
93
- :ui="{ base: 'rounded-full' }"
94
- @click="toggleDevTools"
95
- />
96
- </div>
2
+ <div
3
+ v-show="isShowDevTools"
4
+ ref="devToolsRef"
5
+ class="
6
+ fixed z-50 overflow-hidden rounded-lg border border-neutral-300 bg-white
7
+ opacity-80 shadow-2xl
8
+ "
9
+ :style="devToolsDynamicStyles"
10
+ >
11
+ <!-- Draggable Title Bar -->
12
+ <div class="flex items-center justify-between px-2 py-1 select-none">
13
+ <p
14
+ class="flex-grow cursor-move text-sm font-semibold"
15
+ @mousedown.prevent="handleDragStart"
16
+ >
17
+ Debug Tools
18
+ </p>
19
+ <div class="flex items-center">
20
+ <Button
21
+ icon="i-heroicons-arrow-path"
22
+ size="xs"
23
+ color="neutral"
24
+ variant="ghost"
25
+ class="mr-1"
26
+ title="Reset Position & Size"
27
+ @click.stop="resetDevToolsState"
28
+ />
29
+ <Button
30
+ icon="i-heroicons-x-mark"
31
+ size="xs"
32
+ color="neutral"
33
+ variant="ghost"
34
+ title="Close DevTools"
35
+ @click.stop="closeDevTools"
36
+ />
37
+ </div>
38
+ </div>
39
+ <hr class="text-neutral-300" />
40
+ <!-- Content Area Target for Logs -->
41
+ <div
42
+ id="dev-logs"
43
+ class="flex flex-1 flex-col space-y-1 overflow-auto p-2"
44
+ :style="{ height: `calc(${devToolsHeight} - 40px)` }"
45
+ />
46
+
47
+ <!-- Resize Handles -->
48
+ <div
49
+ v-if="!isDragging"
50
+ class="resize-handles"
51
+ >
52
+ <div
53
+ class="resize-handle top-left"
54
+ @mousedown.prevent="handleResizeStart('top-left', $event)"
55
+ />
56
+ <div
57
+ class="resize-handle top-center"
58
+ @mousedown.prevent="handleResizeStart('top', $event)"
59
+ />
60
+ <div
61
+ class="resize-handle top-right"
62
+ @mousedown.prevent="handleResizeStart('top-right', $event)"
63
+ />
64
+ <div
65
+ class="resize-handle middle-left"
66
+ @mousedown.prevent="handleResizeStart('left', $event)"
67
+ />
68
+ <div
69
+ class="resize-handle middle-right"
70
+ @mousedown.prevent="handleResizeStart('right', $event)"
71
+ />
72
+ <div
73
+ class="resize-handle bottom-left"
74
+ @mousedown.prevent="handleResizeStart('bottom-left', $event)"
75
+ />
76
+ <div
77
+ class="resize-handle bottom-center"
78
+ @mousedown.prevent="handleResizeStart('bottom', $event)"
79
+ />
80
+ <div
81
+ class="resize-handle bottom-right"
82
+ @mousedown.prevent="handleResizeStart('bottom-right', $event)"
83
+ />
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Toggle button for this DevToolsWindow -->
88
+ <div
89
+ class="fixed right-1 bottom-1 z-[99999]"
90
+ >
91
+ <Button
92
+ :icon="isShowDevTools ? 'heroicons:x-mark' : 'heroicons:information-circle'"
93
+ color="info"
94
+ square
95
+ size="sm"
96
+ :ui="{ base: 'rounded-full' }"
97
+ @click="toggleDevTools"
98
+ />
99
+ </div>
97
100
  </template>
98
101
 
99
102
  <script setup>
@@ -53,8 +53,8 @@
53
53
  {{ pageBetween }} รายการ จากทั้งหมด {{ totalCountWithComma }} รายการ
54
54
  </p>
55
55
  <Pagination
56
- v-if="options.pageOptions.totalPage > 1 && !isShowSimplePagination"
57
- :to="options.isPreventRouteChange ? void 0 : to"
56
+ v-if="options.pageOptions.totalPage > 1"
57
+ :to="options.isRouteChange ? to : void 0"
58
58
  :default-page="options.pageOptions?.currentPage || 1"
59
59
  :items-per-page="options.pageOptions.limit"
60
60
  :total="options.pageOptions.totalCount"
@@ -67,7 +67,7 @@
67
67
  <script setup>
68
68
  import { computed, ref, watch } from "vue";
69
69
  import { useElementVisibility } from "@vueuse/core";
70
- import { StringHelper } from "#core/utils/StringHelper";
70
+ import { NumberHelper } from "#core/utils/NumberHelper";
71
71
  import { _debounce, useRouter, useWatchChange, useWatchTrue } from "#imports";
72
72
  import { useCoreConfig } from "#core/composables/useConfig";
73
73
  import Empty from "#core/components/Empty.vue";
@@ -108,7 +108,7 @@ const to = (page2) => {
108
108
  };
109
109
  };
110
110
  useWatchChange(() => props.options?.pageOptions?.request?.params, () => {
111
- if (props.options?.isPreventRouteChange) return;
111
+ if (props.options?.isRouteChange) return;
112
112
  router.replace({
113
113
  query: props.options?.pageOptions?.request?.params || {}
114
114
  });
@@ -123,7 +123,7 @@ const pageBetween = computed(() => {
123
123
  return `${start} - ${end}`;
124
124
  });
125
125
  const totalCountWithComma = computed(() => {
126
- return !props.options.pageOptions.totalCount ? "0" : StringHelper.withComma(props.options.pageOptions.totalCount);
126
+ return !props.options.pageOptions.totalCount ? "0" : NumberHelper.withComma(props.options.pageOptions.totalCount);
127
127
  });
128
128
  const totalInnerRawData = computed(() => {
129
129
  return innerRawData.value?.length || 0;
@@ -6,7 +6,7 @@ export interface IFlexDeckOptions<T = object> {
6
6
  pageOptions: IPageOptions;
7
7
  isEnabledSearch?: boolean;
8
8
  searchPlaceholder?: string;
9
- isPreventRouteChange: boolean;
9
+ isRouteChange: boolean;
10
10
  isHidePagination?: boolean;
11
11
  isEnableInfiniteScroll?: boolean;
12
12
  }
@@ -1,60 +1,90 @@
1
1
  <template>
2
2
  <FieldWrapper v-bind="wrapperProps">
3
- <Input
4
- v-if="type === 'password'"
5
- v-maska="activeMaskOptions"
6
- :model-value="value"
7
- :disabled="wrapperProps.disabled"
8
- :leading-icon="leadingIcon"
9
- :trailing-icon="trailingIcon"
10
- :loading="loading"
11
- :loading-icon="loadingIcon"
12
- :name="name"
13
- :placeholder="wrapperProps.placeholder"
14
- :type="isShowPassword ? 'text' : 'password'"
15
- :autofocus="!!autoFocus"
16
- :icon="icon"
17
- :readonly="readonly"
18
- :ui="defu(ui, { icon: { trailing: { pointer: '' } } })"
19
- @update:model-value="onChange"
20
- >
21
- <template #trailing>
22
- <Button
23
- color="neutral"
24
- variant="link"
25
- :icon="isShowPassword ? 'i-heroicons-eye-slash' : 'i-heroicons-eye'"
26
- :padded="false"
27
- @click="isShowPassword = !isShowPassword"
28
- />
29
- </template>
30
- </Input>
31
- <Input
32
- v-else
33
- v-maska="activeMaskOptions"
34
- :model-value="value"
35
- :disabled="wrapperProps.disabled"
36
- :leading-icon="leadingIcon"
37
- :trailing-icon="trailingIcon"
38
- :loading="loading"
39
- :loading-icon="loadingIcon"
40
- :name="name"
41
- :placeholder="wrapperProps.placeholder"
42
- :type="type"
43
- :autofocus="!!autoFocus"
44
- :icon="icon"
45
- :readonly="readonly"
46
- :ui="ui"
47
- @update:model-value="onChange"
48
- />
3
+ <div class="relative">
4
+ <Input
5
+ v-if="type === 'password'"
6
+ ref="inputRef"
7
+ v-maska="activeMaskOptions"
8
+ :model-value="value"
9
+ :disabled="wrapperProps.disabled"
10
+ :leading-icon="leadingIcon"
11
+ :trailing-icon="trailingIcon"
12
+ :loading="loading"
13
+ :loading-icon="loadingIcon"
14
+ :name="name"
15
+ :placeholder="wrapperProps.placeholder"
16
+ :type="isShowPassword ? 'text' : 'password'"
17
+ :autofocus="!!autoFocus"
18
+ :icon="icon"
19
+ :readonly="readonly"
20
+ :ui="defu(ui, { icon: { trailing: { pointer: '' } } })"
21
+ @update:model-value="onChange"
22
+ @focus="onFocus"
23
+ @blur="onBlur"
24
+ @keydown="onKeydown"
25
+ >
26
+ <template #trailing>
27
+ <Button
28
+ color="neutral"
29
+ variant="link"
30
+ :icon="isShowPassword ? 'i-heroicons-eye-slash' : 'i-heroicons-eye'"
31
+ :padded="false"
32
+ @click="isShowPassword = !isShowPassword"
33
+ />
34
+ </template>
35
+ </Input>
36
+ <Input
37
+ v-else
38
+ ref="inputRef"
39
+ v-maska="activeMaskOptions"
40
+ :model-value="value"
41
+ :disabled="wrapperProps.disabled"
42
+ :leading-icon="leadingIcon"
43
+ :trailing-icon="trailingIcon"
44
+ :loading="loading"
45
+ :loading-icon="loadingIcon"
46
+ :name="name"
47
+ :placeholder="wrapperProps.placeholder"
48
+ :type="type"
49
+ :autofocus="!!autoFocus"
50
+ :icon="icon"
51
+ :readonly="readonly"
52
+ :ui="ui"
53
+ @update:model-value="onChange"
54
+ @focus="onFocus"
55
+ @blur="onBlur"
56
+ @keydown="onKeydown"
57
+ />
58
+ <div
59
+ v-if="showSuggestions && filteredSuggestions.length > 0"
60
+ ref="suggestionsContainerRef"
61
+ :class="theme.suggestionsContainer()"
62
+ >
63
+ <div
64
+ v-for="(suggestion, index) in filteredSuggestions"
65
+ :key="suggestion"
66
+ :ref="(el) => setSuggestionItemRef(el, index)"
67
+ :class="[
68
+ theme.suggestionItem(),
69
+ { [theme.suggestionItemActive()]: index === selectedSuggestionIndex }
70
+ ]"
71
+ @mousedown.prevent="selectSuggestion(suggestion, index)"
72
+ @mouseenter="selectedSuggestionIndex = index"
73
+ >
74
+ {{ suggestion }}
75
+ </div>
76
+ </div>
77
+ </div>
49
78
  </FieldWrapper>
50
79
  </template>
51
80
 
52
81
  <script setup>
53
82
  import { vMaska } from "maska/vue";
54
83
  import { defu } from "defu";
55
- import { ref, computed } from "#imports";
84
+ import { ref, computed, nextTick, useUiConfig } from "#imports";
56
85
  import { useFieldHOC } from "#core/composables/useForm";
57
86
  import FieldWrapper from "#core/components/Form/FieldWrapper.vue";
87
+ import { inputTheme } from "#core/theme/input";
58
88
  const emits = defineEmits(["change"]);
59
89
  const props = defineProps({
60
90
  type: { type: String, required: false, default: "text" },
@@ -69,6 +99,7 @@ const props = defineProps({
69
99
  maskEager: { type: Boolean, required: false },
70
100
  maskTokensReplace: { type: Boolean, required: false },
71
101
  maskReversed: { type: Boolean, required: false },
102
+ suggestions: { type: Array, required: false },
72
103
  form: { type: Object, required: false },
73
104
  name: { type: String, required: true },
74
105
  errorMessage: { type: String, required: false },
@@ -84,52 +115,158 @@ const props = defineProps({
84
115
  help: { type: String, required: false },
85
116
  ui: { type: null, required: false }
86
117
  });
118
+ const theme = computed(() => useUiConfig(inputTheme, "input")());
87
119
  const {
88
120
  value,
89
121
  wrapperProps,
90
122
  handleChange
91
123
  } = useFieldHOC(props);
92
124
  const isShowPassword = ref(false);
125
+ const inputRef = ref();
126
+ const showSuggestions = ref(false);
127
+ const selectedSuggestionIndex = ref(-1);
128
+ const suggestionsContainerRef = ref();
129
+ const suggestionItemRefs = ref([]);
130
+ const setSuggestionItemRef = (el, index) => {
131
+ if (suggestionItemRefs.value) {
132
+ suggestionItemRefs.value[index] = el;
133
+ }
134
+ };
93
135
  const onChange = (value2) => {
136
+ if (props.suggestions && props.suggestions.length > 0) {
137
+ showSuggestions.value = true;
138
+ selectedSuggestionIndex.value = -1;
139
+ }
94
140
  handleChange(value2);
95
141
  emits("change", value2);
96
142
  };
97
- const activeMaskOptions = computed(() => {
98
- if (props.maskOptions && Object.keys(props.maskOptions).length > 0) {
99
- if (typeof props.maskOptions.mask === "string" && Object.keys(props.maskOptions).length === 1) {
100
- return props.maskOptions.mask;
101
- }
102
- return props.maskOptions;
143
+ const filteredSuggestions = computed(() => {
144
+ if (!props.suggestions || !value.value) {
145
+ return props.suggestions || [];
103
146
  }
104
- const options = {};
105
- let hasIndividualProps = false;
106
- if (props.mask !== void 0) {
107
- options.mask = props.mask;
108
- hasIndividualProps = true;
147
+ const inputValue = value.value.toLowerCase();
148
+ return props.suggestions.filter(
149
+ (suggestion) => suggestion.toLowerCase().includes(inputValue)
150
+ );
151
+ });
152
+ const onFocus = () => {
153
+ if (props.suggestions && props.suggestions.length > 0) {
154
+ showSuggestions.value = true;
155
+ selectedSuggestionIndex.value = -1;
109
156
  }
110
- if (props.maskTokens !== void 0) {
111
- options.tokens = props.maskTokens;
112
- hasIndividualProps = true;
157
+ };
158
+ const onBlur = (event) => {
159
+ setTimeout(() => {
160
+ showSuggestions.value = false;
161
+ selectedSuggestionIndex.value = -1;
162
+ }, 150);
163
+ };
164
+ const onKeydown = (event) => {
165
+ if (!showSuggestions.value || filteredSuggestions.value.length === 0) {
166
+ return;
167
+ }
168
+ switch (event.key) {
169
+ case "ArrowDown":
170
+ event.preventDefault();
171
+ selectedSuggestionIndex.value = selectedSuggestionIndex.value < filteredSuggestions.value.length - 1 ? selectedSuggestionIndex.value + 1 : 0;
172
+ scrollToSelectedSuggestion();
173
+ break;
174
+ case "ArrowUp":
175
+ event.preventDefault();
176
+ selectedSuggestionIndex.value = selectedSuggestionIndex.value > 0 ? selectedSuggestionIndex.value - 1 : filteredSuggestions.value.length - 1;
177
+ scrollToSelectedSuggestion();
178
+ break;
179
+ case "Enter":
180
+ event.preventDefault();
181
+ if (selectedSuggestionIndex.value >= 0) {
182
+ selectSuggestion(filteredSuggestions.value[selectedSuggestionIndex.value], selectedSuggestionIndex.value);
183
+ }
184
+ break;
185
+ case "Escape":
186
+ showSuggestions.value = false;
187
+ selectedSuggestionIndex.value = -1;
188
+ break;
113
189
  }
114
- if (props.maskEager !== void 0) {
115
- options.eager = props.maskEager;
116
- hasIndividualProps = true;
190
+ };
191
+ const selectSuggestion = (suggestion, index) => {
192
+ if (index !== void 0) {
193
+ scrollToSuggestionByIndex(index);
117
194
  }
118
- if (props.maskReversed !== void 0) {
119
- options.reversed = props.maskReversed;
120
- hasIndividualProps = true;
195
+ handleChange(suggestion);
196
+ emits("change", suggestion);
197
+ showSuggestions.value = false;
198
+ selectedSuggestionIndex.value = -1;
199
+ nextTick(() => {
200
+ if (inputRef.value) {
201
+ inputRef.value.$el.querySelector("input")?.focus();
202
+ }
203
+ });
204
+ };
205
+ const scrollToSelectedSuggestion = () => {
206
+ nextTick(() => {
207
+ if (selectedSuggestionIndex.value >= 0) {
208
+ scrollToSuggestionByIndex(selectedSuggestionIndex.value);
209
+ }
210
+ });
211
+ };
212
+ const scrollToSuggestionByIndex = (index) => {
213
+ if (!suggestionsContainerRef.value || !suggestionItemRefs.value[index]) {
214
+ return;
121
215
  }
122
- if (props.maskTokensReplace !== void 0) {
123
- options.tokensReplace = props.maskTokensReplace;
124
- hasIndividualProps = true;
216
+ const container = suggestionsContainerRef.value;
217
+ const item = suggestionItemRefs.value[index];
218
+ if (item) {
219
+ const containerRect = container.getBoundingClientRect();
220
+ const itemRect = item.getBoundingClientRect();
221
+ const isAboveView = itemRect.top < containerRect.top;
222
+ const isBelowView = itemRect.bottom > containerRect.bottom;
223
+ if (isAboveView || isBelowView) {
224
+ item.scrollIntoView({
225
+ behavior: "smooth",
226
+ block: "nearest",
227
+ inline: "nearest"
228
+ });
229
+ }
125
230
  }
126
- if (hasIndividualProps) {
127
- const keys = Object.keys(options);
128
- if (keys.length === 1 && keys[0] === "mask" && typeof options.mask === "string") {
129
- return options.mask;
231
+ };
232
+ const activeMaskOptions = computed(
233
+ () => {
234
+ if (props.maskOptions && Object.keys(props.maskOptions).length > 0) {
235
+ if (typeof props.maskOptions.mask === "string" && Object.keys(props.maskOptions).length === 1) {
236
+ return props.maskOptions.mask;
237
+ }
238
+ return props.maskOptions;
239
+ }
240
+ const options = {};
241
+ let hasIndividualProps = false;
242
+ if (props.mask !== void 0) {
243
+ options.mask = props.mask;
244
+ hasIndividualProps = true;
245
+ }
246
+ if (props.maskTokens !== void 0) {
247
+ options.tokens = props.maskTokens;
248
+ hasIndividualProps = true;
130
249
  }
131
- return options;
250
+ if (props.maskEager !== void 0) {
251
+ options.eager = props.maskEager;
252
+ hasIndividualProps = true;
253
+ }
254
+ if (props.maskReversed !== void 0) {
255
+ options.reversed = props.maskReversed;
256
+ hasIndividualProps = true;
257
+ }
258
+ if (props.maskTokensReplace !== void 0) {
259
+ options.tokensReplace = props.maskTokensReplace;
260
+ hasIndividualProps = true;
261
+ }
262
+ if (hasIndividualProps) {
263
+ const keys = Object.keys(options);
264
+ if (keys.length === 1 && keys[0] === "mask" && typeof options.mask === "string") {
265
+ return options.mask;
266
+ }
267
+ return options;
268
+ }
269
+ return void 0;
132
270
  }
133
- return void 0;
134
- });
271
+ );
135
272
  </script>
@@ -13,6 +13,7 @@ export interface ITextFieldProps extends IFieldProps {
13
13
  maskEager?: boolean;
14
14
  maskTokensReplace?: boolean;
15
15
  maskReversed?: boolean;
16
+ suggestions?: string[];
16
17
  }
17
18
  export type ITextField = IFormFieldBase<INPUT_TYPES.TEXT | INPUT_TYPES.PASSWORD | INPUT_TYPES.EMAIL, ITextFieldProps, {
18
19
  change?: (value: string) => void;
@@ -6,7 +6,7 @@
6
6
  />
7
7
  <div :class="theme.labelWrapper()">
8
8
  <p
9
- class="cursor-pointer font-bold text-primary"
9
+ class="text-primary cursor-pointer font-bold"
10
10
  @click="$emit('openFile')"
11
11
  >
12
12
  {{ selectFileLabel }}
@@ -11,7 +11,7 @@
11
11
  <h1 class="truncate font-bold">
12
12
  {{ selectedFile.name }}
13
13
  </h1>
14
- <p class="truncate font-light text-error">
14
+ <p class="text-error truncate font-light">
15
15
  {{ uploadFailedLabel }}
16
16
  </p>
17
17
  <Button
@@ -20,7 +20,7 @@
20
20
  <p class="text-error-400">
21
21
  <Icon
22
22
  name="i-heroicons:exclamation-circle-solid"
23
- class="size-8 text-error-400"
23
+ class="text-error-400 size-8"
24
24
  />
25
25
  </p>
26
26
  </div>
@@ -18,7 +18,7 @@
18
18
  <div class="flex h-60 items-center justify-center">
19
19
  <Icon
20
20
  name="i-svg-spinners:180-ring-with-bg"
21
- class="size-8 text-primary"
21
+ class="text-primary size-8"
22
22
  />
23
23
  </div>
24
24
  </template>
@@ -76,7 +76,7 @@ import { computed } from "vue";
76
76
  import { COLUMN_TYPES } from "#core/components/Table/types";
77
77
  import ColumnNumber from "#core/components/Table/ColumnNumber.vue";
78
78
  import ColumnImage from "#core/components/Table/ColumnImage.vue";
79
- import { StringHelper } from "#core/utils/StringHelper";
79
+ import { NumberHelper } from "#core/utils/NumberHelper";
80
80
  import { ref, watch } from "#imports";
81
81
  import ColumnDateTime from "#core/components/Table/ColumnDateTime.vue";
82
82
  import Empty from "#core/components/Empty.vue";
@@ -148,7 +148,7 @@ const transformValue = (column, row) => {
148
148
  return column.transform ? column.transform(value, row, column) : value;
149
149
  };
150
150
  const totalCountWithComma = computed(() => {
151
- return !props.pageOptions?.totalCount ? "0" : StringHelper.withComma(props.pageOptions.totalCount);
151
+ return !props.pageOptions?.totalCount ? "0" : NumberHelper.withComma(props.pageOptions.totalCount);
152
152
  });
153
153
  watch(page, () => {
154
154
  emits("pageChange", page.value);
@@ -1,14 +1,14 @@
1
1
  <template>
2
- {{ getValue }}
2
+ {{ getValue }}
3
3
  </template>
4
4
 
5
5
  <script setup>
6
6
  import { computed } from "vue";
7
- import { StringHelper } from "../../utils/StringHelper";
7
+ import { NumberHelper } from "#core/utils/NumberHelper";
8
8
  const props = defineProps({
9
9
  value: { type: null, required: true },
10
10
  row: { type: null, required: true },
11
11
  column: { type: null, required: true }
12
12
  });
13
- const getValue = computed(() => StringHelper.withComma(props.value));
13
+ const getValue = computed(() => NumberHelper.withComma(props.value));
14
14
  </script>
@@ -32,7 +32,9 @@
32
32
  name="error"
33
33
  >
34
34
  <div
35
- class="flex h-[200px] items-center justify-center text-2xl text-error-400"
35
+ class="
36
+ text-error-400 flex h-[200px] items-center justify-center text-2xl
37
+ "
36
38
  >
37
39
  {{ StringHelper.getError(options.status.errorData) }}
38
40
  </div>
@@ -88,7 +90,7 @@
88
90
  :default-page="options.pageOptions?.currentPage || 1"
89
91
  :items-per-page="options.pageOptions.limit"
90
92
  :total="options.pageOptions.totalCount"
91
- :to="options.isPreventRouteChange ? void 0 : to"
93
+ :to="options.isRouteChange ? to : void 0"
92
94
  @update:page="onPageChange"
93
95
  />
94
96
  </div>
@@ -99,7 +101,7 @@
99
101
  import { computed } from "vue";
100
102
  import { COLUMN_TYPES } from "#core/components/Table/types";
101
103
  import { _debounce, ref, useUiConfig, watch, useWatchChange, useRouter } from "#imports";
102
- import { StringHelper } from "#core/utils/StringHelper";
104
+ import { NumberHelper } from "#core/utils/NumberHelper";
103
105
  import ColumnDate from "#core/components/Table/ColumnDate.vue";
104
106
  import ColumnDateTime from "#core/components/Table/ColumnDateTime.vue";
105
107
  import ColumnImage from "#core/components/Table/ColumnImage.vue";
@@ -168,7 +170,7 @@ const totalCountWithComma = computed(() => {
168
170
  if (!total || total <= 0) {
169
171
  return "0";
170
172
  }
171
- return StringHelper.withComma(total);
173
+ return NumberHelper.withComma(total);
172
174
  });
173
175
  const transformValue = (column, row) => {
174
176
  return column.cell ? column.cell({
@@ -28,7 +28,7 @@ export interface ITableOptions<T extends Record<string, any> = Record<string, an
28
28
  isHideToolbar?: boolean;
29
29
  isEnabledSearch?: boolean;
30
30
  searchPlaceholder?: string;
31
- isPreventRouteChange: boolean;
31
+ isRouteChange: boolean;
32
32
  }
33
33
  export interface ISimpleTableOptions<T extends Record<string, any> = Record<string, any>> extends IBaseTableOptions<T> {
34
34
  limit?: number;
@@ -7,7 +7,7 @@ export const createFlexDeckOptions = (repo, options) => {
7
7
  pageOptions: get(repo.fetch.options),
8
8
  status: get(repo.fetch.status),
9
9
  primary: get(repo.fetch.options).primary || config.default_primary_key,
10
- isPreventRouteChange: false,
10
+ isRouteChange: false,
11
11
  ...options
12
12
  };
13
13
  };
@@ -23,7 +23,7 @@ export const createTableOptions = (repo, columns, options, transformItems) => {
23
23
  pageOptions: get(repo.fetch.options),
24
24
  columns,
25
25
  status: get(repo.fetch.status),
26
- isPreventRouteChange: false,
26
+ isRouteChange: false,
27
27
  ...options
28
28
  };
29
29
  };
@@ -1,4 +1,4 @@
1
- import { computed, ref, StringHelper } from "#imports";
1
+ import { computed, ref } from "#imports";
2
2
  export const checkMaxSize = (file, acceptFileSize = 0) => {
3
3
  if (acceptFileSize) {
4
4
  return file.size / 1e3 <= acceptFileSize;
@@ -90,22 +90,14 @@ export const useFileProgress = () => {
90
90
  percent.value = 100;
91
91
  return;
92
92
  }
93
- percent.value = Number.parseFloat(
94
- StringHelper.withFixed(
95
- (Math.floor(progressEvent.loaded * 100 / progressEvent.total) || 0) * 0.8
96
- )
97
- );
93
+ percent.value = (Math.floor(progressEvent.loaded * 100 / progressEvent.total) || 0) * 0.8;
98
94
  };
99
95
  const onDownloadProgress = (progressEvent) => {
100
96
  if (!progressEvent.total || progressEvent.total === 0) {
101
97
  percent.value = 100;
102
98
  return;
103
99
  }
104
- percent.value = Number.parseFloat(
105
- StringHelper.withFixed(
106
- Math.floor(progressEvent.loaded * 100 / progressEvent.total) * 0.2 + 80
107
- )
108
- );
100
+ percent.value = Math.floor(progressEvent.loaded * 100 / progressEvent.total) * 0.2 + 80;
109
101
  };
110
102
  return {
111
103
  percent,
@@ -1,6 +1,9 @@
1
1
  export declare const inputTheme: {
2
2
  slots: {
3
3
  root: string;
4
+ suggestionsContainer: string;
5
+ suggestionItem: string;
6
+ suggestionItemActive: string;
4
7
  };
5
8
  defaultVariants: {
6
9
  size: string;
@@ -1,6 +1,9 @@
1
1
  export const inputTheme = {
2
2
  slots: {
3
- root: "w-full"
3
+ root: "w-full",
4
+ suggestionsContainer: "absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow max-h-60 overflow-y-auto",
5
+ suggestionItem: "px-3 py-3 text-sm cursor-pointer hover:bg-(--ui-color-primary-100) truncate",
6
+ suggestionItemActive: "bg-(--ui-color-primary-100)"
4
7
  },
5
8
  defaultVariants: {
6
9
  size: "lg"
@@ -0,0 +1,6 @@
1
+ export declare class NumberHelper {
2
+ static withComma: (value?: number | string) => string;
3
+ static withFixed: (value?: number | string) => string;
4
+ static toCurrency: (value?: number | string, currency?: string) => string;
5
+ static toNumber: (value: any) => number;
6
+ }
@@ -0,0 +1,25 @@
1
+ export class NumberHelper {
2
+ static withComma = (value = 0) => {
3
+ return (+(value || 0)).toLocaleString();
4
+ };
5
+ static withFixed = (value = 0) => {
6
+ return (+(value || 0)).toLocaleString(void 0, {
7
+ minimumFractionDigits: 0,
8
+ maximumFractionDigits: 2
9
+ });
10
+ };
11
+ static toCurrency = (value = 0, currency = "") => {
12
+ const options = {
13
+ minimumFractionDigits: 2,
14
+ maximumFractionDigits: 2
15
+ };
16
+ if (currency) {
17
+ options.style = "currency";
18
+ options.currency = currency;
19
+ }
20
+ return (+(value || 0)).toLocaleString(void 0, options);
21
+ };
22
+ static toNumber = (value) => {
23
+ return Number(value) || 0;
24
+ };
25
+ }
@@ -1,7 +1,5 @@
1
1
  export declare class StringHelper {
2
2
  static genString: (length?: number) => string;
3
- static withComma: (value?: number | string) => string;
4
- static withFixed: (value?: number | string) => string;
5
3
  static split: (str: string | null | undefined, separator: string | RegExp) => string[];
6
4
  static joinURL: (...parts: string[]) => string;
7
5
  static truncate: (str: any, num?: number) => any;
@@ -9,15 +9,6 @@ export class StringHelper {
9
9
  }
10
10
  return result;
11
11
  };
12
- static withComma = (value = 0) => {
13
- return (+(value || 0)).toLocaleString();
14
- };
15
- static withFixed = (value = 0) => {
16
- return (+(value || 0)).toLocaleString(void 0, {
17
- minimumFractionDigits: 0,
18
- maximumFractionDigits: 2
19
- });
20
- };
21
12
  static split = (str, separator) => {
22
13
  return `${str || ""}`.split(separator).filter((item) => item).map((item) => item.trim());
23
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "2.19.1",
3
+ "version": "2.21.0",
4
4
  "repository": "https://gitlab.finema.co/finema/ui-kit",
5
5
  "license": "MIT",
6
6
  "author": "Finema Dev Core Team",
@@ -72,7 +72,6 @@
72
72
  },
73
73
  "devDependencies": {
74
74
  "@eslint/js": "^9.26.0",
75
- "@hyoban/eslint-plugin-tailwindcss": "^4.0.0-alpha.12",
76
75
  "@nuxt/devtools": "^2.4.1",
77
76
  "@nuxt/eslint-config": "^1.3.1",
78
77
  "@nuxt/module-builder": "^1.0.1",
@@ -81,11 +80,8 @@
81
80
  "@types/node": "latest",
82
81
  "changelogen": "^0.5.7",
83
82
  "eslint": "^9.26.0",
84
- "eslint-config-prettier": "^9.1.0",
85
- "eslint-import-resolver-typescript": "^3.7.0",
86
- "eslint-plugin-prettier": "^5.2.1",
87
- "eslint-plugin-tailwindcss": "^3.17.5",
88
83
  "eslint-plugin-unused-imports": "^4.1.4",
84
+ "eslint-plugin-better-tailwindcss": "^3.3.0",
89
85
  "husky": "^9.1.7",
90
86
  "lint-staged": "^16.0.0",
91
87
  "nuxt": "3.17.3",