@desource/phone-mask-react 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @desource/phone-mask-react
2
+
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - React: Design React version of phone-mask library
8
+ - Core: Add geoip reusable service for country detection based on IP address
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies []:
13
+ - @desource/phone-mask@0.3.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeSource Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,455 @@
1
+ # @desource/phone-mask-react
2
+
3
+ > React phone input component and hook with smart masking and Google libphonenumber data
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@desource/phone-mask-react?color=blue&logo=react)](https://www.npmjs.com/package/@desource/phone-mask-react)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@desource/phone-mask-react?label=gzip%20size&color=purple)](https://bundlephobia.com/package/@desource/phone-mask-react)
7
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/DeSource-Labs/phone-mask/blob/main/LICENSE)
8
+
9
+ Beautiful, accessible, extreme small & tree-shackable React phone input with auto-formatting, country selector, and validation.
10
+
11
+ ## ✨ Features
12
+
13
+ - 🎨 **Beautiful UI** — Modern design with light/dark themes
14
+ - 🔍 **Smart Country Search** — Fuzzy matching with keyboard navigation
15
+ - 🎭 **Auto-formatting** — As-you-type formatting with smart cursor
16
+ - ✅ **Validation** — Built-in validation with visual feedback
17
+ - 📋 **Copy Button** — One-click copy to clipboard
18
+ - 🌐 **Auto-detection** — GeoIP and locale-based detection
19
+ - ♿ **Accessible** — ARIA labels, keyboard navigation
20
+ - 📱 **Mobile-friendly** — Optimized for touch devices
21
+ - 🎯 **TypeScript** — Full type safety
22
+ - 🪝 **Hook API** — For custom input implementations
23
+ - ⚡ **Optimized** — Tree-shaking and code splitting
24
+
25
+ ## 📦 Installation
26
+
27
+ ```bash
28
+ npm install @desource/phone-mask-react
29
+ # or
30
+ yarn add @desource/phone-mask-react
31
+ # or
32
+ pnpm add @desource/phone-mask-react
33
+ ```
34
+
35
+ ## 🚀 Quick Start
36
+
37
+ ### Importing
38
+
39
+ Component mode:
40
+ ```tsx
41
+ import { PhoneInput } from '@desource/phone-mask-react';
42
+ import '@desource/phone-mask-react/assets/lib.css'; // Import styles
43
+ ```
44
+
45
+ Hook mode:
46
+ ```tsx
47
+ import { usePhoneMask } from '@desource/phone-mask-react';
48
+ ```
49
+
50
+ ### Component Mode
51
+
52
+ ```tsx
53
+ import { useState } from 'react';
54
+ import { PhoneInput } from '@desource/phone-mask-react';
55
+ import '@desource/phone-mask-react/assets/lib.css'; // Import styles
56
+
57
+ function App() {
58
+ const [phone, setPhone] = useState('');
59
+ const [isValid, setIsValid] = useState(false);
60
+
61
+ return (
62
+ <>
63
+ <PhoneInput
64
+ value={phone}
65
+ onChange={setPhone}
66
+ onValidationChange={setIsValid}
67
+ country="US"
68
+ />
69
+
70
+ {isValid && <p>✓ Valid phone number</p>}
71
+ </>
72
+ );
73
+ }
74
+ ```
75
+
76
+ ### Hook Mode
77
+
78
+ For custom input implementations:
79
+
80
+ ```tsx
81
+ import { usePhoneMask } from '@desource/phone-mask-react';
82
+
83
+ function CustomPhoneInput() {
84
+ const { ref, digits, full, fullFormatted, isComplete, country, setCountry } = usePhoneMask({
85
+ country: 'US',
86
+ detect: true,
87
+ onChange: (phone) => {
88
+ console.log(phone.full, phone.digits);
89
+ }
90
+ });
91
+
92
+ return (
93
+ <div>
94
+ <input ref={ref} type="tel" placeholder="Phone number" />
95
+ <p>Formatted: {fullFormatted}</p>
96
+ <p>Valid: {isComplete ? 'Yes' : 'No'}</p>
97
+ <p>Country: {country.name}</p>
98
+ <button onClick={() => setCountry('GB')}>Use UK</button>
99
+ </div>
100
+ );
101
+ }
102
+ ```
103
+
104
+ ## 📖 Component API
105
+
106
+ ### Props
107
+
108
+ ```ts
109
+ interface PhoneInputProps {
110
+ // Controlled value (digits only, without country code)
111
+ value?: string;
112
+
113
+ // Preselected country (ISO 3166-1 alpha-2)
114
+ country?: CountryKey;
115
+
116
+ // Auto-detect country from IP/locale
117
+ detect?: boolean; // Default: true
118
+
119
+ // Locale for country names
120
+ locale?: string; // Default: browser language
121
+
122
+ // Size variant
123
+ size?: 'compact' | 'normal' | 'large'; // Default: 'normal'
124
+
125
+ // Visual theme ("auto" | "light" | "dark")
126
+ theme?: 'auto' | 'light' | 'dark'; // Default: 'auto'
127
+
128
+ // Disabled state
129
+ disabled?: boolean; // Default: false
130
+
131
+ // Readonly state
132
+ readonly?: boolean; // Default: false
133
+
134
+ // Show copy button
135
+ showCopy?: boolean; // Default: true
136
+
137
+ // Show clear button
138
+ showClear?: boolean; // Default: false
139
+
140
+ // Show validation state (borders & outline)
141
+ withValidity?: boolean; // Default: true
142
+
143
+ // Custom search placeholder
144
+ searchPlaceholder?: string; // Default: 'Search country or code...'
145
+
146
+ // Custom no results text
147
+ noResultsText?: string; // Default: 'No countries found'
148
+
149
+ // Custom clear button label
150
+ clearButtonLabel?: string; // Default: 'Clear phone number'
151
+
152
+ // Dropdown menu custom CSS class
153
+ dropdownClass?: string;
154
+
155
+ // Disable default styles
156
+ disableDefaultStyles?: boolean; // Default: false
157
+
158
+ // Callback when the digits value changes.
159
+ // Returns only the digits without country code (e.g. '234567890')
160
+ onChange?: (digits: string) => void;
161
+
162
+ // Callback when the phone number changes.
163
+ // Provides an object with:
164
+ // - full: Full phone number with country code (e.g. +1234567890)
165
+ // - fullFormatted: Full phone number formatted according to country rules (e.g. +1 234-567-890)
166
+ // - digits: Only the digits of the phone number without country code (e.g. 234567890)
167
+ onPhoneChange?: (value: PhoneNumber) => void;
168
+
169
+ // Callback when the selected country changes
170
+ onCountryChange?: (country: MaskFull) => void;
171
+
172
+ // Callback when the validation state changes
173
+ onValidationChange?: (isValid: boolean) => void;
174
+
175
+ // Callback when the input is focused
176
+ onFocus?: (event: FocusEvent<HTMLInputElement>) => void;
177
+
178
+ // Callback when the input is blurred
179
+ onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
180
+
181
+ // Callback when phone number is copied
182
+ onCopy?: (value: string) => void;
183
+
184
+ // Callback when input is cleared
185
+ onClear?: () => void;
186
+
187
+ // Render custom action buttons before default ones
188
+ renderActionsBefore?: () => ReactNode;
189
+
190
+ // Render custom flag icons in the country list and country selector
191
+ renderFlag?: (country: MaskFull) => ReactNode;
192
+
193
+ // Render custom copy button SVG
194
+ renderCopySvg?: (copied: boolean) => ReactNode;
195
+
196
+ // Render custom clear button SVG
197
+ renderClearSvg?: () => ReactNode;
198
+ }
199
+ ```
200
+
201
+ ### Ref Methods
202
+
203
+ ```tsx
204
+ const phoneInputRef = useRef<PhoneInputRef>(null);
205
+
206
+ phoneInputRef.current?.focus(); // Focuses the input
207
+ phoneInputRef.current?.blur(); // Blurs the input
208
+ phoneInputRef.current?.clear(); // Clears the input value
209
+ phoneInputRef.current?.selectCountry('GB'); // Programmatically selects a country by ISO code (e.g., 'US', 'DE', 'GB')
210
+ phoneInputRef.current?.getFullNumber(); // Returns the full phone number with country code (e.g., +1234567890)
211
+ phoneInputRef.current?.getFullFormattedNumber(); // Returns the full phone number formatted according to country rules (e.g., +1 234-567-890)
212
+ phoneInputRef.current?.getDigits(); // Returns only the digits of the phone number without country code (e.g., 234567890)
213
+ phoneInputRef.current?.isValid(); // Checks if the current phone number is valid
214
+ phoneInputRef.current?.isComplete(); // Checks if the current phone number is complete
215
+ ```
216
+
217
+ ## 🪝 Hook API
218
+
219
+ ### Options
220
+
221
+ ```ts
222
+ interface UsePhoneMaskOptions {
223
+ // Predefined country ISO code (e.g., 'US', 'DE', 'GB')
224
+ country?: string;
225
+
226
+ // Locale for country names (default: navigator.language)
227
+ locale?: string;
228
+
229
+ // Auto-detect country from IP/locale (default: false)
230
+ detect?: boolean;
231
+
232
+ // Value change callback
233
+ onChange?: (phone: PhoneNumber) => void;
234
+
235
+ // Country change callback
236
+ onCountryChange?: (country: MaskFull) => void;
237
+ }
238
+ ```
239
+
240
+ ### Return Value
241
+
242
+ ```ts
243
+ interface UsePhoneMaskReturn {
244
+ ref: RefObject<HTMLInputElement>;
245
+ digits: string;
246
+ full: string;
247
+ fullFormatted: string;
248
+ isComplete: boolean;
249
+ isEmpty: boolean;
250
+ shouldShowWarn: boolean;
251
+ country: MaskFull;
252
+ setCountry: (countryCode: string) => void;
253
+ clear: () => void;
254
+ }
255
+ ```
256
+
257
+ ## 🎨 Component Styling
258
+
259
+ ### CSS Custom Properties
260
+
261
+ Customize colors via CSS variables:
262
+
263
+ ```css
264
+ .phone-input,
265
+ .phone-dropdown {
266
+ /* Colors */
267
+ --pi-bg: #ffffff;
268
+ --pi-fg: #111827;
269
+ --pi-muted: #6b7280;
270
+ --pi-border: #e5e7eb;
271
+ --pi-border-hover: #d1d5db;
272
+ --pi-border-focus: #3b82f6;
273
+ --pi-focus-ring: 3px solid rgb(59 130 246 / 0.15);
274
+ --pi-disabled-bg: #f9fafb;
275
+ --pi-disabled-fg: #9ca3af;
276
+ /* Sizes */
277
+ --pi-font-size: 16px;
278
+ --pi-height: 44px;
279
+ /* Spacing */
280
+ --pi-padding: 12px;
281
+ /* Border radius */
282
+ --pi-radius: 8px;
283
+ /* Shadows */
284
+ --pi-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
285
+ --pi-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05);
286
+ /* Validation */
287
+ --pi-warning: #f59e0b;
288
+ --pi-warning-light: #fbbf24;
289
+ --pi-success: #10b981;
290
+ --pi-focus-ring-warning: 3px solid rgb(245 158 11 / 0.15);
291
+ --pi-focus-ring-success: 3px solid rgb(16 185 129 / 0.15);
292
+ }
293
+ ```
294
+
295
+ ### Dark Theme
296
+
297
+ ```tsx
298
+ <PhoneInput value={phone} theme="dark" />
299
+ ```
300
+
301
+ Or with CSS:
302
+
303
+ ```css
304
+ .phone-input[data-theme='dark'] {
305
+ --pi-bg: #1f2937;
306
+ --pi-fg: #f9fafb;
307
+ --pi-border: #374151;
308
+ }
309
+ ```
310
+
311
+ ## 📚 Examples
312
+
313
+ ### With Validation
314
+
315
+ ```tsx
316
+ import { useState } from 'react';
317
+ import { PhoneInput } from '@desource/phone-mask-react';
318
+
319
+ function Example() {
320
+ const [phone, setPhone] = useState('');
321
+ const [isValid, setIsValid] = useState(false);
322
+
323
+ return (
324
+ <div>
325
+ <PhoneInput value={phone} onChange={setPhone} onValidationChange={setIsValid} />
326
+
327
+ {isValid && <span>✓ Valid phone number</span>}
328
+ </div>
329
+ );
330
+ }
331
+ ```
332
+
333
+ ### Auto-detect Country
334
+
335
+ ```tsx
336
+ import { useState } from 'react';
337
+ import { PhoneInput } from '@desource/phone-mask-react';
338
+
339
+ function Example() {
340
+ const [phone, setPhone] = useState('');
341
+ const [detectedCountry, setDetectedCountry] = useState('');
342
+
343
+ return (
344
+ <>
345
+ <PhoneInput
346
+ value={phone}
347
+ detect
348
+ onChange={setPhone}
349
+ onCountryChange={(country) => setDetectedCountry(country.name)}
350
+ />
351
+
352
+ {detectedCountry && <p>Detected: {detectedCountry}</p>}
353
+ </>
354
+ );
355
+ }
356
+ ```
357
+
358
+ ### With Form Libraries
359
+
360
+ #### React Hook Form
361
+
362
+ ```tsx
363
+ import { useForm, Controller } from 'react-hook-form';
364
+ import { PhoneInput } from '@desource/phone-mask-react';
365
+
366
+ function Example() {
367
+ const { control } = useForm({
368
+ defaultValues: { phone: '' }
369
+ });
370
+
371
+ return (
372
+ <Controller
373
+ name="phone"
374
+ control={control}
375
+ render={({ field }) => (
376
+ <PhoneInput
377
+ value={field.value}
378
+ onChange={(digits) => field.onChange(digits)}
379
+ onBlur={field.onBlur}
380
+ />
381
+ )}
382
+ />
383
+ );
384
+ }
385
+ ```
386
+
387
+ ### Multiple Inputs
388
+
389
+ ```tsx
390
+ import { useState } from 'react';
391
+ import { PhoneInput } from '@desource/phone-mask-react';
392
+
393
+ function Example() {
394
+ const [form, setForm] = useState({ mobile: '', home: '', work: '' });
395
+
396
+ return (
397
+ <div className="form">
398
+ <label>
399
+ Mobile
400
+ <PhoneInput value={form.mobile} onChange={(digits) => setForm({ ...form, mobile: digits })} />
401
+ </label>
402
+
403
+ <label>
404
+ Home
405
+ <PhoneInput value={form.home} onChange={(digits) => setForm({ ...form, home: digits })} />
406
+ </label>
407
+
408
+ <label>
409
+ Work
410
+ <PhoneInput value={form.work} onChange={(digits) => setForm({ ...form, work: digits })} />
411
+ </label>
412
+ </div>
413
+ );
414
+ }
415
+ ```
416
+
417
+ ## 🎯 Browser Support
418
+
419
+ - Chrome/Edge 90+
420
+ - Firefox 88+
421
+ - Safari 14+
422
+ - iOS Safari 14+
423
+ - Chrome Mobile
424
+
425
+ ## 📦 What's Included
426
+
427
+ ```
428
+ @desource/phone-mask-react/
429
+ ├── dist/
430
+ │ ├── esm # ESM bundle + types
431
+ │ ├── phone-mask-react.cjs.js # CommonJS bundle
432
+ │ └── phone-mask-react.css # Component styles
433
+ ├── README.md # This file
434
+ └── package.json # Package manifest
435
+ ```
436
+
437
+ ## 🔗 Related
438
+
439
+ - [@desource/phone-mask](../phone-mask) — Core library
440
+ - [@desource/phone-mask-nuxt](../phone-mask-nuxt) — Nuxt module
441
+ - [@desource/phone-mask-vue](../phone-mask-vue) — Vue 3 bindings
442
+
443
+ ## 📄 License
444
+
445
+ [MIT](../../LICENSE) © 2026 DeSource Labs
446
+
447
+ ## 🤝 Contributing
448
+
449
+ See [Contributing Guide](../../CONTRIBUTING.md)
450
+
451
+ ---
452
+
453
+ <div align="center">
454
+ <sub>Made with ❤️ by <a href="https://github.com/DeSource-Labs">DeSource Labs</a></sub>
455
+ </div>
@@ -0,0 +1,11 @@
1
+ import { type Ref } from 'react';
2
+ import type { PhoneInputProps, PhoneInputRef } from '../types';
3
+ type PhoneInputComponent = PhoneInputProps & {
4
+ ref?: Ref<PhoneInputRef>;
5
+ };
6
+ export declare const PhoneInput: {
7
+ ({ ref, ...props }: PhoneInputComponent): import("react/jsx-runtime").JSX.Element;
8
+ displayName: string;
9
+ };
10
+ export {};
11
+ //# sourceMappingURL=PhoneInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhoneInput.d.ts","sourceRoot":"","sources":["../../../src/components/PhoneInput.tsx"],"names":[],"mappings":"AAAA,OAAc,EAQZ,KAAK,GAAG,EACT,MAAM,OAAO,CAAC;AAYf,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAe,MAAM,UAAU,CAAC;AAS5E,KAAK,mBAAmB,GAAG,eAAe,GAAG;IAAE,GAAG,CAAC,EAAE,GAAG,CAAC,aAAa,CAAC,CAAA;CAAE,CAAC;AAE1E,eAAO,MAAM,UAAU;wBAAuB,mBAAmB;;CAuwBhE,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare const Delimiters: string[];
2
+ export declare const NavigationKeys: string[];
3
+ export declare const InvalidPattern: RegExp;
4
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,UAAuB,CAAC;AAC/C,eAAO,MAAM,cAAc,UAA4E,CAAC;AACxG,eAAO,MAAM,cAAc,QAAgB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { UsePhoneMaskOptions, UsePhoneMaskReturn } from '../types';
2
+ /**
3
+ * React hook for phone number masking.
4
+ * Provides low-level phone masking functionality for custom input implementations.
5
+ */
6
+ export declare function usePhoneMask(options?: UsePhoneMaskOptions): UsePhoneMaskReturn;
7
+ //# sourceMappingURL=usePhoneMask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePhoneMask.d.ts","sourceRoot":"","sources":["../../../src/hooks/usePhoneMask.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAe,MAAM,UAAU,CAAC;AAyBrF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,kBAAkB,CAwTlF"}
@@ -0,0 +1,14 @@
1
+ import { countPlaceholders, formatDigitsWithMap, pickMaskVariant, removeCountryCodePrefix, toArray } from '@desource/phone-mask';
2
+ export { PhoneInput } from './components/PhoneInput';
3
+ export { usePhoneMask } from './hooks/usePhoneMask';
4
+ export type { PhoneInputProps, PhoneInputRef, PhoneNumber, UsePhoneMaskOptions, UsePhoneMaskReturn, Size as PhoneInputSize, Theme as PhoneInputTheme } from './types';
5
+ export type { CountryKey as PCountryKey, MaskBase as PMaskBase, MaskBaseMap as PMaskBaseMap, Mask as PMask, MaskMap as PMaskMap, MaskWithFlag as PMaskWithFlag, MaskWithFlagMap as PMaskWithFlagMap, MaskFull as PMaskFull, MaskFullMap as PMaskFullMap } from '@desource/phone-mask';
6
+ export declare const PMaskHelpers: {
7
+ getFlagEmoji: (cc: string) => string;
8
+ countPlaceholders: typeof countPlaceholders;
9
+ formatDigitsWithMap: typeof formatDigitsWithMap;
10
+ pickMaskVariant: typeof pickMaskVariant;
11
+ removeCountryCodePrefix: typeof removeCountryCodePrefix;
12
+ toArray: typeof toArray;
13
+ };
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,uBAAuB,EACvB,OAAO,EACR,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,YAAY,EACV,eAAe,EACf,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,kBAAkB,EAClB,IAAI,IAAI,cAAc,EACtB,KAAK,IAAI,eAAe,EACzB,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,UAAU,IAAI,WAAW,EACzB,QAAQ,IAAI,SAAS,EACrB,WAAW,IAAI,YAAY,EAC3B,IAAI,IAAI,KAAK,EACb,OAAO,IAAI,QAAQ,EACnB,YAAY,IAAI,aAAa,EAC7B,eAAe,IAAI,gBAAgB,EACnC,QAAQ,IAAI,SAAS,EACrB,WAAW,IAAI,YAAY,EAC5B,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,YAAY;;;;;;;CAOxB,CAAC"}