@alikhalilll/a-tel-input 1.0.1

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/index.cjs +5846 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +791 -0
  6. package/dist/index.d.ts +791 -0
  7. package/dist/index.js +5804 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/nuxt/index.cjs +30 -0
  10. package/dist/nuxt/index.cjs.map +1 -0
  11. package/dist/nuxt/index.d.cts +15 -0
  12. package/dist/nuxt/index.d.ts +15 -0
  13. package/dist/nuxt/index.js +30 -0
  14. package/dist/nuxt/index.js.map +1 -0
  15. package/dist/resolver/index.cjs +25 -0
  16. package/dist/resolver/index.cjs.map +1 -0
  17. package/dist/resolver/index.d.cts +14 -0
  18. package/dist/resolver/index.d.ts +14 -0
  19. package/dist/resolver/index.js +25 -0
  20. package/dist/resolver/index.js.map +1 -0
  21. package/dist/styles.css +520 -0
  22. package/package.json +123 -0
  23. package/src/components/ACountryFlag.vue +78 -0
  24. package/src/components/ACountrySelect.vue +674 -0
  25. package/src/components/ATelInput.vue +742 -0
  26. package/src/composables/useCountryDetection.ts +247 -0
  27. package/src/composables/useCountryMatching.ts +213 -0
  28. package/src/composables/usePhoneValidation.ts +573 -0
  29. package/src/composables/useTelInputValidation.ts +136 -0
  30. package/src/composables/useTypingPhase.ts +88 -0
  31. package/src/icons/AlertCircleIcon.vue +17 -0
  32. package/src/icons/CheckCircleIcon.vue +16 -0
  33. package/src/icons/CheckIcon.vue +15 -0
  34. package/src/icons/ChevronDownIcon.vue +15 -0
  35. package/src/icons/SearchIcon.vue +16 -0
  36. package/src/icons/SpinnerIcon.vue +28 -0
  37. package/src/icons/index.ts +6 -0
  38. package/src/index.ts +36 -0
  39. package/src/nuxt/index.ts +37 -0
  40. package/src/resolver/index.ts +29 -0
  41. package/src/types.ts +389 -0
  42. package/src/utils/digits.ts +42 -0
  43. package/src/utils/flag-url.ts +10 -0
  44. package/web-types.json +526 -0
@@ -0,0 +1,88 @@
1
+ import { ref, readonly, type ComputedRef, type Ref } from 'vue';
2
+ import { useDebounceFn } from '@vueuse/core';
3
+
4
+ /**
5
+ * Typing-phase state machine for the tel input.
6
+ *
7
+ * Owns the three reactive flags that drive the "is the user still typing?" UX:
8
+ *
9
+ * - `isDetecting` — true while the debounce window is in flight (user is mid-burst or
10
+ * has just paused). Drives the loading spinner in the picker slot.
11
+ * - `hasFinishedTyping` — false from the moment a key lands until the debounce settles.
12
+ * Gates validation visibility, so error/success states only appear once the user pauses.
13
+ * - `detectionAttempted` — flips true the first time the debounce fires on non-empty
14
+ * input. Used by the consumer to keep the country picker visible after a failed
15
+ * detection (so the user can pick manually instead of being stranded).
16
+ *
17
+ * Design notes:
18
+ *
19
+ * - The composable is pure state — it does not know about country detection, phone
20
+ * numbers, or libphonenumber. The consumer wires the `onSettle` callback to whatever
21
+ * "what to do when typing pauses" logic is appropriate (typically: try to detect a
22
+ * country from the current digits, mark a detection attempt, and apply the match).
23
+ *
24
+ * - `markDetectionAttempt()` is exposed separately so the caller controls *when* the
25
+ * "keep the picker visible" flag flips — not every settle triggers a real attempt
26
+ * (e.g. when input is empty or the user already picked a country manually).
27
+ *
28
+ * - Refs are exposed `readonly` so external code can't bypass the state machine; all
29
+ * transitions go through the exposed actions.
30
+ */
31
+ export interface UseTypingPhaseOptions {
32
+ /** Debounce window in ms. Reactive so consumers can change `detectDebounceMs` at runtime. */
33
+ debounceMs: ComputedRef<number>;
34
+ /** Fired when the debounce timer settles. Runs regardless of input state — use this
35
+ * to clear loading UI, then perform any pause-triggered work (e.g. detection). */
36
+ onSettle?: () => void;
37
+ }
38
+
39
+ export interface UseTypingPhaseReturn {
40
+ isDetecting: Readonly<Ref<boolean>>;
41
+ hasFinishedTyping: Readonly<Ref<boolean>>;
42
+ detectionAttempted: Readonly<Ref<boolean>>;
43
+ /** Call from the input handler on every keystroke that produces non-empty input. */
44
+ markTyping: () => void;
45
+ /** Flip `detectionAttempted` to true. Call from within the `onSettle` callback when
46
+ * a real detection attempt is about to run — so the picker stays visible after even
47
+ * a failed match. */
48
+ markDetectionAttempt: () => void;
49
+ /** Reset all three flags to defaults. Call when the input is cleared. */
50
+ reset: () => void;
51
+ }
52
+
53
+ export function useTypingPhase(opts: UseTypingPhaseOptions): UseTypingPhaseReturn {
54
+ const isDetecting = ref(false);
55
+ const hasFinishedTyping = ref(true);
56
+ const detectionAttempted = ref(false);
57
+
58
+ const settle = useDebounceFn(() => {
59
+ isDetecting.value = false;
60
+ hasFinishedTyping.value = true;
61
+ opts.onSettle?.();
62
+ }, opts.debounceMs);
63
+
64
+ function markTyping() {
65
+ isDetecting.value = true;
66
+ hasFinishedTyping.value = false;
67
+ settle();
68
+ }
69
+
70
+ function markDetectionAttempt() {
71
+ detectionAttempted.value = true;
72
+ }
73
+
74
+ function reset() {
75
+ isDetecting.value = false;
76
+ hasFinishedTyping.value = true;
77
+ detectionAttempted.value = false;
78
+ }
79
+
80
+ return {
81
+ isDetecting: readonly(isDetecting),
82
+ hasFinishedTyping: readonly(hasFinishedTyping),
83
+ detectionAttempted: readonly(detectionAttempted),
84
+ markTyping,
85
+ markDetectionAttempt,
86
+ reset,
87
+ };
88
+ }
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ stroke-linecap="round"
9
+ stroke-linejoin="round"
10
+ aria-hidden="true"
11
+ focusable="false"
12
+ >
13
+ <circle cx="12" cy="12" r="10" />
14
+ <line x1="12" y1="8" x2="12" y2="12" />
15
+ <line x1="12" y1="16" x2="12.01" y2="16" />
16
+ </svg>
17
+ </template>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ stroke-linecap="round"
9
+ stroke-linejoin="round"
10
+ aria-hidden="true"
11
+ focusable="false"
12
+ >
13
+ <circle cx="12" cy="12" r="10" />
14
+ <path d="m9 12 2 2 4-4" />
15
+ </svg>
16
+ </template>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ stroke-linecap="round"
9
+ stroke-linejoin="round"
10
+ aria-hidden="true"
11
+ focusable="false"
12
+ >
13
+ <path d="M20 6 9 17l-5-5" />
14
+ </svg>
15
+ </template>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ stroke-linecap="round"
9
+ stroke-linejoin="round"
10
+ aria-hidden="true"
11
+ focusable="false"
12
+ >
13
+ <path d="m6 9 6 6 6-6" />
14
+ </svg>
15
+ </template>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ stroke-linecap="round"
9
+ stroke-linejoin="round"
10
+ aria-hidden="true"
11
+ focusable="false"
12
+ >
13
+ <circle cx="11" cy="11" r="8" />
14
+ <path d="m21 21-4.3-4.3" />
15
+ </svg>
16
+ </template>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <svg
3
+ class="a-tel-input-icon-spinner"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ aria-hidden="true"
12
+ focusable="false"
13
+ >
14
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
15
+ </svg>
16
+ </template>
17
+
18
+ <style>
19
+ @keyframes a-tel-input-icon-spin {
20
+ to {
21
+ transform: rotate(360deg);
22
+ }
23
+ }
24
+ .a-tel-input-icon-spinner {
25
+ animation: a-tel-input-icon-spin 1s linear infinite;
26
+ transform-origin: center;
27
+ }
28
+ </style>
@@ -0,0 +1,6 @@
1
+ export { default as CheckIcon } from './CheckIcon.vue';
2
+ export { default as CheckCircleIcon } from './CheckCircleIcon.vue';
3
+ export { default as AlertCircleIcon } from './AlertCircleIcon.vue';
4
+ export { default as SpinnerIcon } from './SpinnerIcon.vue';
5
+ export { default as ChevronDownIcon } from './ChevronDownIcon.vue';
6
+ export { default as SearchIcon } from './SearchIcon.vue';
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ // Components
2
+ export { default as ATelInput } from './components/ATelInput.vue';
3
+ export { default as ACountrySelect } from './components/ACountrySelect.vue';
4
+ export { default as ACountryFlag } from './components/ACountryFlag.vue';
5
+
6
+ // Types, variants, defaults
7
+ export {
8
+ aTelInputVariants,
9
+ DEFAULT_ERROR_MESSAGES,
10
+ DEFAULT_MESSAGES,
11
+ resolveMessages,
12
+ type ATelInputProps,
13
+ type ATelInputSlots,
14
+ type ATelInputEmits,
15
+ type ATelInputSize,
16
+ type ATelInputVariants,
17
+ type ATelInputDir,
18
+ type ACountrySelectProps,
19
+ type ACountrySelectSlots,
20
+ type ACountrySelectEmits,
21
+ type ACountryFlagProps,
22
+ type ACountryFlagSlots,
23
+ type TelInputMessages,
24
+ type TelInputMessagesInput,
25
+ } from './types';
26
+ export { defaultFlagUrl, type FlagUrlBuilder } from './utils/flag-url';
27
+
28
+ // i18n — alternative-numeral normalization
29
+ export { normalizeDigits, LOCALE_DIGIT_RANGES } from './utils/digits';
30
+
31
+ // Composables — co-located with the components since they're tel-input specific.
32
+ export * from './composables/usePhoneValidation';
33
+ export * from './composables/useCountryDetection';
34
+ export * from './composables/useCountryMatching';
35
+ export * from './composables/useTypingPhase';
36
+ export * from './composables/useTelInputValidation';
@@ -0,0 +1,37 @@
1
+ import { defineNuxtModule, addComponent } from '@nuxt/kit';
2
+ import type { NuxtModule } from '@nuxt/schema';
3
+
4
+ /**
5
+ * `@alikhalilll/a-tel-input/nuxt` — registers the tel-input components for Nuxt
6
+ * auto-import. The country picker renders a-popover/a-drawer, so also import
7
+ * their stylesheets plus a-ui-base tokens and a-tel-input styles.
8
+ */
9
+
10
+ export interface ModuleOptions {
11
+ prefix?: string;
12
+ }
13
+
14
+ const COMPONENTS: Record<string, string> = {
15
+ ATelInput: '@alikhalilll/a-tel-input',
16
+ ACountrySelect: '@alikhalilll/a-tel-input',
17
+ ACountryFlag: '@alikhalilll/a-tel-input',
18
+ };
19
+
20
+ const module: NuxtModule<ModuleOptions> = defineNuxtModule<ModuleOptions>({
21
+ meta: {
22
+ name: '@alikhalilll/a-tel-input',
23
+ configKey: 'aTelInput',
24
+ compatibility: { nuxt: '>=3.0.0' },
25
+ },
26
+ defaults: { prefix: '' },
27
+ setup(opts) {
28
+ const prefix = opts.prefix ?? '';
29
+ for (const [exportName, from] of Object.entries(COMPONENTS)) {
30
+ const baseName = exportName.startsWith('A') ? exportName.slice(1) : exportName;
31
+ const registeredName = `${prefix}${prefix ? baseName : exportName}`;
32
+ addComponent({ name: registeredName, export: exportName, filePath: from });
33
+ }
34
+ },
35
+ });
36
+
37
+ export default module;
@@ -0,0 +1,29 @@
1
+ import type { ComponentResolver } from 'unplugin-vue-components';
2
+
3
+ /**
4
+ * `@alikhalilll/a-tel-input/resolver` — auto-import resolver for non-Nuxt
5
+ * Vite/Webpack consumers via `unplugin-vue-components`.
6
+ */
7
+
8
+ export interface ResolverOptions {
9
+ prefix?: string;
10
+ }
11
+
12
+ const COMPONENT_TO_ENTRY: Record<string, string> = {
13
+ ATelInput: '@alikhalilll/a-tel-input',
14
+ ACountrySelect: '@alikhalilll/a-tel-input',
15
+ ACountryFlag: '@alikhalilll/a-tel-input',
16
+ };
17
+
18
+ export default function ATelInputResolver(opts: ResolverOptions = {}): ComponentResolver {
19
+ const prefix = opts.prefix ?? '';
20
+ return {
21
+ type: 'component',
22
+ resolve(name) {
23
+ const bare = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
24
+ const from = COMPONENT_TO_ENTRY[bare];
25
+ if (!from) return;
26
+ return { name: bare, from };
27
+ },
28
+ };
29
+ }