@antify/ui 4.1.37 → 4.2.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.
Files changed (28) hide show
  1. package/dist/components/index.d.ts +3 -1
  2. package/dist/components/index.js +14 -0
  3. package/dist/components/index.mjs +4 -0
  4. package/dist/components/inputs/AntCountryInput.vue +366 -0
  5. package/dist/components/inputs/AntPhoneNumberInput.vue +347 -0
  6. package/dist/components/inputs/Elements/AntSelectMenu.vue +2 -0
  7. package/dist/components/inputs/__stories/AntCountryInput.stories.d.ts +16 -0
  8. package/dist/components/inputs/__stories/AntCountryInput.stories.js +286 -0
  9. package/dist/components/inputs/__stories/AntCountryInput.stories.mjs +289 -0
  10. package/dist/components/inputs/__stories/AntPhoneNumberInput.stories.d.ts +7 -0
  11. package/dist/components/inputs/__stories/AntPhoneNumberInput.stories.js +303 -0
  12. package/dist/components/inputs/__stories/AntPhoneNumberInput.stories.mjs +305 -0
  13. package/dist/components/inputs/__types/AntCountryInput.types.d.ts +11 -0
  14. package/dist/components/inputs/__types/AntCountryInput.types.js +1 -0
  15. package/dist/components/inputs/__types/AntCountryInput.types.mjs +0 -0
  16. package/dist/components/inputs/__types/index.d.ts +1 -0
  17. package/dist/components/inputs/__types/index.js +11 -0
  18. package/dist/components/inputs/__types/index.mjs +1 -0
  19. package/dist/constants/countries.d.ts +22 -0
  20. package/dist/constants/countries.js +2009 -0
  21. package/dist/constants/countries.mjs +2185 -0
  22. package/dist/constants/index.d.ts +1 -0
  23. package/dist/constants/index.js +16 -0
  24. package/dist/constants/index.mjs +1 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +11 -0
  27. package/dist/index.mjs +1 -0
  28. package/package.json +1 -1
@@ -17,6 +17,8 @@ import AntPasswordInput from './inputs/AntPasswordInput.vue';
17
17
  import AntRadio from './inputs/AntRadio.vue';
18
18
  import AntRadioGroup from './inputs/AntRadioGroup.vue';
19
19
  import AntRangeSlider from './inputs/AntRangeSlider.vue';
20
+ import AntCountryInput from './inputs/AntCountryInput.vue';
21
+ import AntPhoneNumberInput from './inputs/AntPhoneNumberInput.vue';
20
22
  import AntSearch from './inputs/AntSearch.vue';
21
23
  import AntSelect from './inputs/AntSelect.vue';
22
24
  import AntSwitch from './inputs/AntSwitch.vue';
@@ -54,4 +56,4 @@ import AntTag from './AntTag.vue';
54
56
  import AntToast from './AntToast.vue';
55
57
  import AntToaster from './AntToaster.vue';
56
58
  import AntTooltip from './AntTooltip.vue';
57
- export { AntButton, AntDatePicker, AntDialog, AntField, AntFormGroup, AntFormGroupLabel, AntBaseInput, AntSelectMenu, AntInputDescription, AntInputLabel, AntInputLimiter, AntCheckbox, AntCheckboxGroup, AntDateInput, AntNumberInput, AntPasswordInput, AntRadio, AntRadioGroup, AntRangeSlider, AntSearch, AntSelect, AntSwitch, AntSwitcher, AntTagInput, AntTextarea, AntTextInput, AntUnitInput, AntImageInput, AntNavLeftLayout, AntNavbar, AntNavbarItem, AntTable, AntTabs, AntTransitionCollapseHeight, AntAccordion, AntAccordionItem, AntAlert, AntCard, AntContent, AntDropdown, AntIcon, AntKeycap, AntListGroup, AntListGroupItem, AntModal, AntPagination, AntItemsPerPage, AntPopover, AntSkeleton, AntSpinner, AntTag, AntToast, AntToaster, AntTooltip, AntColorInput, AntMultiSelect, };
59
+ export { AntButton, AntDatePicker, AntDialog, AntField, AntFormGroup, AntFormGroupLabel, AntBaseInput, AntSelectMenu, AntInputDescription, AntInputLabel, AntInputLimiter, AntCheckbox, AntCheckboxGroup, AntDateInput, AntNumberInput, AntPasswordInput, AntRadio, AntRadioGroup, AntRangeSlider, AntCountryInput, AntPhoneNumberInput, AntSearch, AntSelect, AntSwitch, AntSwitcher, AntTagInput, AntTextarea, AntTextInput, AntUnitInput, AntImageInput, AntNavLeftLayout, AntNavbar, AntNavbarItem, AntTable, AntTabs, AntTransitionCollapseHeight, AntAccordion, AntAccordionItem, AntAlert, AntCard, AntContent, AntDropdown, AntIcon, AntKeycap, AntListGroup, AntListGroupItem, AntModal, AntPagination, AntItemsPerPage, AntPopover, AntSkeleton, AntSpinner, AntTag, AntToast, AntToaster, AntTooltip, AntColorInput, AntMultiSelect, };
@@ -63,6 +63,12 @@ Object.defineProperty(exports, "AntContent", {
63
63
  return _AntContent.default;
64
64
  }
65
65
  });
66
+ Object.defineProperty(exports, "AntCountryInput", {
67
+ enumerable: true,
68
+ get: function () {
69
+ return _AntCountryInput.default;
70
+ }
71
+ });
66
72
  Object.defineProperty(exports, "AntDateInput", {
67
73
  enumerable: true,
68
74
  get: function () {
@@ -207,6 +213,12 @@ Object.defineProperty(exports, "AntPasswordInput", {
207
213
  return _AntPasswordInput.default;
208
214
  }
209
215
  });
216
+ Object.defineProperty(exports, "AntPhoneNumberInput", {
217
+ enumerable: true,
218
+ get: function () {
219
+ return _AntPhoneNumberInput.default;
220
+ }
221
+ });
210
222
  Object.defineProperty(exports, "AntPopover", {
211
223
  enumerable: true,
212
224
  get: function () {
@@ -358,6 +370,8 @@ var _AntPasswordInput = _interopRequireDefault(require("./inputs/AntPasswordInpu
358
370
  var _AntRadio = _interopRequireDefault(require("./inputs/AntRadio.vue"));
359
371
  var _AntRadioGroup = _interopRequireDefault(require("./inputs/AntRadioGroup.vue"));
360
372
  var _AntRangeSlider = _interopRequireDefault(require("./inputs/AntRangeSlider.vue"));
373
+ var _AntCountryInput = _interopRequireDefault(require("./inputs/AntCountryInput.vue"));
374
+ var _AntPhoneNumberInput = _interopRequireDefault(require("./inputs/AntPhoneNumberInput.vue"));
361
375
  var _AntSearch = _interopRequireDefault(require("./inputs/AntSearch.vue"));
362
376
  var _AntSelect = _interopRequireDefault(require("./inputs/AntSelect.vue"));
363
377
  var _AntSwitch = _interopRequireDefault(require("./inputs/AntSwitch.vue"));
@@ -17,6 +17,8 @@ import AntPasswordInput from "./inputs/AntPasswordInput.vue";
17
17
  import AntRadio from "./inputs/AntRadio.vue";
18
18
  import AntRadioGroup from "./inputs/AntRadioGroup.vue";
19
19
  import AntRangeSlider from "./inputs/AntRangeSlider.vue";
20
+ import AntCountryInput from "./inputs/AntCountryInput.vue";
21
+ import AntPhoneNumberInput from "./inputs/AntPhoneNumberInput.vue";
20
22
  import AntSearch from "./inputs/AntSearch.vue";
21
23
  import AntSelect from "./inputs/AntSelect.vue";
22
24
  import AntSwitch from "./inputs/AntSwitch.vue";
@@ -74,6 +76,8 @@ export {
74
76
  AntRadio,
75
77
  AntRadioGroup,
76
78
  AntRangeSlider,
79
+ AntCountryInput,
80
+ AntPhoneNumberInput,
77
81
  AntSearch,
78
82
  AntSelect,
79
83
  AntSwitch,
@@ -0,0 +1,366 @@
1
+ <script lang="ts" setup>
2
+ import {
3
+ computed, ref, nextTick,
4
+ } from 'vue';
5
+ import {
6
+ faChevronDown, faChevronUp,
7
+ } from '@fortawesome/free-solid-svg-icons';
8
+ import {
9
+ Size, InputState, Grouped,
10
+ } from '../../enums';
11
+ import {
12
+ IconSize,
13
+ } from '../__types';
14
+ import {
15
+ COUNTRIES, CountryValueKey, Locale,
16
+ } from '../../constants/countries';
17
+ import type {
18
+ Country,
19
+ } from '../../types';
20
+ import AntField from '../forms/AntField.vue';
21
+ import AntSelectMenu from './Elements/AntSelectMenu.vue';
22
+ import AntSearch from './AntSearch.vue';
23
+ import AntIcon from '../AntIcon.vue';
24
+ import AntSkeleton from '../AntSkeleton.vue';
25
+
26
+ const props = withDefaults(defineProps<{
27
+ modelValue: string | number | null;
28
+ countries?: Country[];
29
+ label?: string;
30
+ description?: string;
31
+ placeholder?: string;
32
+ size?: Size;
33
+ state?: InputState;
34
+ disabled?: boolean;
35
+ readonly?: boolean;
36
+ skeleton?: boolean;
37
+ maxHeight?: string;
38
+ searchPlaceholder?: string;
39
+ searchable?: boolean;
40
+ grouped?: Grouped;
41
+ showFlags?: boolean;
42
+ isGrouped?: boolean;
43
+ emptyStateMessage?: string;
44
+ /**
45
+ * Key from the Country object used as the value for modelValue.
46
+ * Useful when you need to bind the select to dialCode, numericCode or ISO value.
47
+ * @default 'value'
48
+ */
49
+ optionValueKey?: CountryValueKey;
50
+ showDialCodeInMenu?: boolean;
51
+ showIsoCode?: boolean;
52
+ locale?: Locale;
53
+ sortable?: boolean;
54
+ }>(), {
55
+ size: Size.md,
56
+ state: InputState.base,
57
+ maxHeight: '350px',
58
+ placeholder: 'Select country',
59
+ searchable: true,
60
+ grouped: Grouped.none,
61
+ showFlags: true,
62
+ isGrouped: false,
63
+ emptyStateMessage: 'No countries found',
64
+ optionValueKey: CountryValueKey.value,
65
+ showDialCodeInMenu: false,
66
+ showIsoCode: false,
67
+ countries: () => COUNTRIES,
68
+ locale: Locale.en,
69
+ sortable: true,
70
+ });
71
+
72
+ const emit = defineEmits([
73
+ 'update:modelValue',
74
+ 'select',
75
+ ]);
76
+
77
+ const isOpen = ref(false);
78
+ const searchQuery = ref<string | null>(null);
79
+ const inputRef = ref<HTMLElement | null>(null);
80
+ const focusedItem = ref<string | number | null>(null);
81
+ const selectMenuRef = ref<InstanceType<typeof AntSelectMenu> | null>(null);
82
+ const searchInputRef = ref<HTMLInputElement | null>(null);
83
+ const hasInputState = computed(() => props.skeleton || props.readonly || props.disabled);
84
+
85
+ const filteredOptions = computed(() => {
86
+ const query = searchQuery.value?.trim().toLowerCase();
87
+ const currentLocale = (props.locale || Locale.en).toLowerCase();
88
+
89
+ const filtered = !props.searchable || !query
90
+ ? [
91
+ ...props.countries,
92
+ ]
93
+ : props.countries.filter(country => {
94
+ const labelValue = country.label[currentLocale] || country.label[Locale.en] || '';
95
+ const labelText = labelValue.toLowerCase();
96
+ const isoCode = country.isoCode.toLowerCase();
97
+ const dialCode = country.dialCode;
98
+
99
+ return labelText.includes(query) ||
100
+ isoCode.includes(query) ||
101
+ dialCode.includes(query);
102
+ });
103
+
104
+ if (props.sortable && currentLocale !== Locale.en) {
105
+ filtered.sort((a, b) => {
106
+ const labelA = a.label[currentLocale] || a.label[Locale.en];
107
+ const labelB = b.label[currentLocale] || b.label[Locale.en];
108
+
109
+ return labelA.localeCompare(labelB, currentLocale);
110
+ });
111
+ }
112
+
113
+ return filtered.map(country => ({
114
+ ...country,
115
+ label: country.label[currentLocale] || country.label[Locale.en],
116
+ value: country[props.optionValueKey] as string | number,
117
+ }));
118
+ });
119
+ const rootComponent = computed(() => (props.isGrouped ? 'div' : AntField));
120
+ const inputClasses = computed(() => {
121
+ const variants: Record<InputState, string> = {
122
+ [InputState.base]: 'outline-base-300 bg-white focus:ring-primary-200',
123
+ [InputState.success]: 'outline-success-500 bg-success-100 focus:ring-success-200',
124
+ [InputState.info]: 'outline-info-500 bg-info-100 focus:ring-info-200',
125
+ [InputState.warning]: 'outline-warning-500 bg-warning-100 focus:ring-warning-200',
126
+ [InputState.danger]: 'outline-danger-500 bg-danger-100 focus:ring-danger-200',
127
+ };
128
+
129
+ return {
130
+ 'flex items-center justify-between transition-colors border-none outline relative w-full cursor-pointer': true,
131
+ 'outline-offset-[-1px] outline-1 focus:outline-offset-[-1px] focus:outline-1': true,
132
+ [variants[props.state]]: true,
133
+ 'opacity-50 cursor-not-allowed': props.disabled,
134
+ 'read-only:cursor-default': props.readonly,
135
+ 'p-1 text-xs': props.size === Size.xs2,
136
+ 'p-1.5 text-xs': props.size === Size.xs,
137
+ 'p-1.5 text-sm': props.size === Size.sm,
138
+ 'p-2 text-sm': props.size === Size.md,
139
+ 'p-2.5 text-sm': props.size === Size.lg,
140
+ 'focus:ring-2': !hasInputState.value && (props.size === Size.sm || props.size === Size.xs || props.size === Size.xs2),
141
+ 'focus:ring-4': !hasInputState.value && (props.size === Size.lg || props.size === Size.md),
142
+ 'rounded-md': props.grouped === Grouped.none,
143
+ 'rounded-tl-md rounded-bl-md rounded-tr-none rounded-br-none': props.grouped === Grouped.left,
144
+ 'rounded-none': props.grouped === Grouped.center,
145
+ 'rounded-tr-md rounded-br-md rounded-tl-none rounded-bl-none': props.grouped === Grouped.right,
146
+ };
147
+ });
148
+ const placeholderClasses = computed(() => {
149
+ const variants: Record<InputState, string> = {
150
+ [InputState.base]: 'text-base-500',
151
+ [InputState.success]: 'text-success-700',
152
+ [InputState.info]: 'text-info-700',
153
+ [InputState.warning]: 'text-warning-700',
154
+ [InputState.danger]: 'text-danger-700',
155
+ };
156
+
157
+ return {
158
+ 'select-none text-ellipsis overflow-hidden whitespace-nowrap w-full': true,
159
+ [variants[props.state]]: true,
160
+ };
161
+ });
162
+ const arrowClasses = computed(() => {
163
+ const variants: Record<InputState, string> = {
164
+ [InputState.base]: 'text-for-white-bg-font',
165
+ [InputState.success]: 'text-success-100-font',
166
+ [InputState.info]: 'text-info-100-font',
167
+ [InputState.warning]: 'text-warning-100-font',
168
+ [InputState.danger]: 'text-danger-100-font',
169
+ };
170
+
171
+ return variants[props.state];
172
+ });
173
+ const selectedCountry = computed(() => {
174
+ if (props.modelValue === null || props.modelValue === undefined) {
175
+ return null;
176
+ }
177
+
178
+ const country = props.countries.find((c) => String(c[props.optionValueKey]) === String(props.modelValue));
179
+
180
+ if (!country) {
181
+ return null;
182
+ }
183
+
184
+ const currentLocale = props.locale || Locale.en;
185
+
186
+ return {
187
+ ...country,
188
+ label: country.label[currentLocale] || country.label[Locale.en],
189
+ value: country[props.optionValueKey] as string | number,
190
+ };
191
+ });
192
+ const skeletonGrouped = computed(() => props.grouped || Grouped.none);
193
+ const iconSize = computed(() => (props.size === Size.lg || props.size === Size.md || props.size === Size.sm ? IconSize.sm : IconSize.xs));
194
+ const fieldProps = computed(() => {
195
+ if (props.isGrouped) {
196
+ return {};
197
+ }
198
+
199
+ return {
200
+ label: props.label,
201
+ description: props.description,
202
+ state: props.state,
203
+ size: props.size,
204
+ skeleton: props.skeleton,
205
+ };
206
+ });
207
+
208
+ function onSelect(val: string | number | null) {
209
+ const country = filteredOptions.value.find(c => String(c[props.optionValueKey]) === String(val));
210
+
211
+ emit('update:modelValue', val);
212
+ emit('select', country || null);
213
+
214
+ isOpen.value = false;
215
+ searchQuery.value = null;
216
+ inputRef.value?.focus();
217
+ }
218
+
219
+ async function toggleMenu(e: MouseEvent) {
220
+ if (props.disabled || props.readonly) {
221
+ return;
222
+ }
223
+
224
+ e.preventDefault();
225
+ e.stopPropagation();
226
+
227
+ isOpen.value = !isOpen.value;
228
+
229
+ if (isOpen.value) {
230
+ if (props.searchable) {
231
+ await nextTick();
232
+
233
+ searchInputRef.value?.focus();
234
+ }
235
+ } else {
236
+ searchQuery.value = null;
237
+ inputRef.value?.focus();
238
+ }
239
+ }
240
+
241
+ function closeMenu() {
242
+ isOpen.value = false;
243
+ searchQuery.value = null;
244
+ }
245
+
246
+ </script>
247
+
248
+ <template>
249
+ <component
250
+ :is="rootComponent"
251
+ v-bind="fieldProps"
252
+ >
253
+ <div
254
+ class="relative w-full"
255
+ >
256
+ <AntSelectMenu
257
+ ref="selectMenuRef"
258
+ v-model:open="isOpen"
259
+ v-model:focused="focusedItem"
260
+ :options="filteredOptions"
261
+ :model-value="modelValue"
262
+ :input-ref="inputRef"
263
+ :max-height="maxHeight"
264
+ :size="size"
265
+ @select-element="onSelect"
266
+ @click-outside="closeMenu"
267
+ >
268
+ <template #contentBefore>
269
+ <div
270
+ v-if="searchable"
271
+ class="p-2 border-b border-base-300 bg-white"
272
+ @keydown.esc.stop="closeMenu"
273
+ >
274
+ <AntSearch
275
+ v-model:input-ref="searchInputRef"
276
+ v-model="searchQuery"
277
+ :size="Size.lg"
278
+ :placeholder="searchPlaceholder"
279
+ class="w-full"
280
+ />
281
+ </div>
282
+ </template>
283
+
284
+ <AntSkeleton
285
+ :visible="skeleton"
286
+ rounded
287
+ :grouped="skeletonGrouped"
288
+ class="w-full"
289
+ >
290
+ <div
291
+ ref="inputRef"
292
+ :class="inputClasses"
293
+ :tabindex="disabled || readonly || skeleton ? -1 : 0"
294
+ @mousedown="toggleMenu"
295
+ >
296
+ <div class="flex items-center gap-2 overflow-hidden">
297
+ <template v-if="selectedCountry">
298
+ <span
299
+ v-if="showFlags"
300
+ class="text-lg leading-none"
301
+ >{{ selectedCountry.flag }}</span>
302
+
303
+ <span class="truncate font-medium">{{ selectedCountry.dialCode }}</span>
304
+
305
+ <span
306
+ v-if="!isGrouped"
307
+ class="truncate"
308
+ >{{ selectedCountry.label }}</span>
309
+ </template>
310
+
311
+ <div
312
+ v-else
313
+ :class="placeholderClasses"
314
+ >
315
+ {{ placeholder }}
316
+ </div>
317
+ </div>
318
+
319
+ <AntIcon
320
+ :icon="isOpen ? faChevronUp : faChevronDown"
321
+ class="ml-2 flex-shrink-0"
322
+ :class="arrowClasses"
323
+ :size="iconSize"
324
+ />
325
+ </div>
326
+ </AntSkeleton>
327
+
328
+ <template #contentLeft="option">
329
+ <span
330
+ v-if="showFlags"
331
+ class="text-lg"
332
+ >{{ (option as any).flag }}</span>
333
+ </template>
334
+
335
+ <template #contentRight="option">
336
+ <slot
337
+ name="right"
338
+ :option="(option as any)"
339
+ >
340
+ <div class="ml-auto flex items-center gap-2">
341
+ <span
342
+ v-if="showIsoCode"
343
+ class="text-md uppercase text-for-white-bg-font"
344
+ >
345
+ {{ (option as any).isoCode }}
346
+ </span>
347
+
348
+ <span
349
+ v-if="showDialCodeInMenu"
350
+ class="text-md text-for-white-bg-font"
351
+ >
352
+ {{ (option as any).dialCode }}
353
+ </span>
354
+ </div>
355
+ </slot>
356
+ </template>
357
+
358
+ <template #empty>
359
+ <div class="p-2 text-center text-sm text-for-white-bg-font">
360
+ {{ emptyStateMessage }}
361
+ </div>
362
+ </template>
363
+ </AntSelectMenu>
364
+ </div>
365
+ </component>
366
+ </template>