@alikhalilll/ui 1.0.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/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/index.d.ts +1009 -0
- package/dist/index.mjs +1519 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +74 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 alikhalilll
|
|
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,286 @@
|
|
|
1
|
+
# @alikhalilll/ui
|
|
2
|
+
|
|
3
|
+
Headless, [shadcn-vue](https://www.shadcn-vue.com/) style component library for Vue 3 (and Nuxt). Built on [reka-ui](https://reka-ui.com) and [vaul-vue](https://github.com/unovue/vaul-vue), fully typed, scalable, with sensible defaults you can override on every level.
|
|
4
|
+
|
|
5
|
+
The first component shipped is **`ATellInput`** — a phone-number input that:
|
|
6
|
+
|
|
7
|
+
- Detects the user's country automatically (IP geolocation → timezone → `navigator.language` → fallback).
|
|
8
|
+
- Validates and formats input via [`libphonenumber-js`](https://www.npmjs.com/package/libphonenumber-js).
|
|
9
|
+
- Renders a **popover on desktop, vaul-vue drawer on mobile** for the country picker.
|
|
10
|
+
- Exposes every sub-primitive so you can compose your own variant if the default doesn't fit.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @alikhalilll/ui
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Peer dependency: `vue ^3.5.0`. The library bundles `reka-ui`, `vaul-vue`, `libphonenumber-js`, `lucide-vue-next`, `@vueuse/core`, `class-variance-authority`, `clsx`, and `tailwind-merge`.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
The components are styled with Tailwind utility classes (`bg-popover`, `text-destructive`, etc.) that resolve to CSS variables shipped in `@alikhalilll/ui/styles.css`. You need to:
|
|
23
|
+
|
|
24
|
+
1. **Import the variables** somewhere global:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// In a Nuxt config or your main entry
|
|
28
|
+
import '@alikhalilll/ui/styles.css';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. **Expose the variables to Tailwind**. For Tailwind v4 add an `@theme inline` block to your global stylesheet:
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
@import 'tailwindcss';
|
|
35
|
+
@import '@alikhalilll/ui/styles.css';
|
|
36
|
+
|
|
37
|
+
@theme inline {
|
|
38
|
+
--color-background: hsl(var(--ak-ui-background));
|
|
39
|
+
--color-foreground: hsl(var(--ak-ui-foreground));
|
|
40
|
+
--color-popover: hsl(var(--ak-ui-popover));
|
|
41
|
+
--color-popover-foreground: hsl(var(--ak-ui-popover-foreground));
|
|
42
|
+
--color-primary: hsl(var(--ak-ui-primary));
|
|
43
|
+
--color-primary-foreground: hsl(var(--ak-ui-primary-foreground));
|
|
44
|
+
--color-muted: hsl(var(--ak-ui-muted));
|
|
45
|
+
--color-muted-foreground: hsl(var(--ak-ui-muted-foreground));
|
|
46
|
+
--color-accent: hsl(var(--ak-ui-accent));
|
|
47
|
+
--color-accent-foreground: hsl(var(--ak-ui-accent-foreground));
|
|
48
|
+
--color-destructive: hsl(var(--ak-ui-destructive));
|
|
49
|
+
--color-destructive-foreground: hsl(var(--ak-ui-destructive-foreground));
|
|
50
|
+
--color-border: hsl(var(--ak-ui-border));
|
|
51
|
+
--color-input: hsl(var(--ak-ui-input));
|
|
52
|
+
--color-ring: hsl(var(--ak-ui-ring));
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For Tailwind v3, add the same colors to `theme.extend.colors` in `tailwind.config.ts`.
|
|
57
|
+
|
|
58
|
+
3. **Toggle dark mode** by adding `.dark` to a parent element (typically `<html>`).
|
|
59
|
+
|
|
60
|
+
The lib ships **both** `:root, .light { … }` and `.dark { … }` blocks. You can lock the theme:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Locked dark
|
|
64
|
+
export default defineNuxtConfig({
|
|
65
|
+
app: { head: { htmlAttrs: { class: 'dark' } } },
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or run a tri-state Light / Dark / System switcher that follows `prefers-color-scheme`. The docs site under [`apps/docs/composables/useColorMode.ts`](https://github.com/alikhalilll/ali-nuxt-toolkit/blob/master/apps/docs/composables/useColorMode.ts) ships a complete working pattern (persisted preference, OS-change listener, pre-paint inline script to avoid flash of wrong theme) that you can copy as-is.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```vue
|
|
74
|
+
<script setup lang="ts">
|
|
75
|
+
import { ref } from 'vue';
|
|
76
|
+
import { ATellInput } from '@alikhalilll/ui';
|
|
77
|
+
|
|
78
|
+
const phone = ref('');
|
|
79
|
+
const country = ref('');
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<ATellInput
|
|
84
|
+
v-model:phone="phone"
|
|
85
|
+
v-model:country="country"
|
|
86
|
+
default-country="SA"
|
|
87
|
+
show-validation
|
|
88
|
+
/>
|
|
89
|
+
</template>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Props
|
|
93
|
+
|
|
94
|
+
| Prop | Type | Default | Description |
|
|
95
|
+
| ------------------- | ------------------------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
96
|
+
| `v-model:phone` | `string` | `''` | Digits-only national number (no leading `+` or dial code). |
|
|
97
|
+
| `v-model:country` | `string` | `''` | ISO 3166-1 alpha-2 code, e.g. `"EG"`. |
|
|
98
|
+
| `placeholder` | `string` | `'Phone number'` | Falls back to the country's example number when empty. |
|
|
99
|
+
| `disabled` | `boolean` | `false` | |
|
|
100
|
+
| `loading` | `boolean` | `false` | Disables interaction. |
|
|
101
|
+
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Controls input height (32 / 36 / 40 px). |
|
|
102
|
+
| `allowedDialCodes` | `string[]` | _all_ | Whitelist of dial-digit codes (no `+`). Countries outside the list are shown but disabled. |
|
|
103
|
+
| `showValidation` | `boolean` | `false` | Renders an error message below the input when invalid. |
|
|
104
|
+
| `detectCountry` | `'auto' \| 'locale' \| 'none'` | `'auto'` | Country auto-detect strategy. |
|
|
105
|
+
| `defaultCountry` | `string` | `'US'` | Fallback ISO2 when detection fails or is disabled. |
|
|
106
|
+
| `ipEndpoint` | `string` | `'https://ipapi.co/json/'` | Override the geolocation endpoint. Must return JSON with `country_code` or `country`. |
|
|
107
|
+
| `searchPlaceholder` | `string` | `'Search by country or code…'` | Country picker search input placeholder. |
|
|
108
|
+
| `emptyText` | `string` | `'No countries found.'` | Shown when the search yields no results. |
|
|
109
|
+
| `loadingText` | `string` | `'Loading…'` | Shown while the country list is loading. |
|
|
110
|
+
| `errorMessages` | `Partial<Record<PhoneValidationReason, string>>` | English defaults | Override the validation error labels. |
|
|
111
|
+
| `class` | `string \| any[] \| Record<string, boolean>` | — | Merged into the outer wrapper via `tailwind-merge`. |
|
|
112
|
+
|
|
113
|
+
### Exposed (via template ref)
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const ref = ref<InstanceType<typeof ATellInput>>();
|
|
117
|
+
ref.value.validation; // PhoneValidationResult — computed, reactive
|
|
118
|
+
ref.value.required; // PhoneRequiredInfo | null — example, length range, format hint
|
|
119
|
+
ref.value.selectedDialCode; // '+20' | null
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Compose your own component
|
|
123
|
+
|
|
124
|
+
Every primitive, composable, and CVA helper is re-exported from the package root, so you can build a custom variant without forking the library.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import {
|
|
128
|
+
// Composables
|
|
129
|
+
usePhoneValidation,
|
|
130
|
+
useCountryDetection,
|
|
131
|
+
detectCountry,
|
|
132
|
+
|
|
133
|
+
// Primitives
|
|
134
|
+
AInput,
|
|
135
|
+
APopover,
|
|
136
|
+
APopoverTrigger,
|
|
137
|
+
APopoverContent,
|
|
138
|
+
ADrawer,
|
|
139
|
+
ADrawerTrigger,
|
|
140
|
+
ADrawerContent,
|
|
141
|
+
ADrawerOverlay,
|
|
142
|
+
AResponsivePopover,
|
|
143
|
+
AResponsivePopoverTrigger,
|
|
144
|
+
AResponsivePopoverContent,
|
|
145
|
+
|
|
146
|
+
// ATellInput pieces
|
|
147
|
+
ATellInput,
|
|
148
|
+
ACountrySelect,
|
|
149
|
+
ACountryFlag,
|
|
150
|
+
aTellInputVariants,
|
|
151
|
+
DEFAULT_ERROR_MESSAGES,
|
|
152
|
+
|
|
153
|
+
// Utilities
|
|
154
|
+
cn,
|
|
155
|
+
} from '@alikhalilll/ui';
|
|
156
|
+
|
|
157
|
+
// Build your own tel input from the same composables
|
|
158
|
+
const { validate, getCountries, searchCountries } = usePhoneValidation();
|
|
159
|
+
const detected = await detectCountry({ strategy: 'auto' });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Or pull the country select on its own (any list-of-countries UI):
|
|
163
|
+
|
|
164
|
+
```vue
|
|
165
|
+
<ACountrySelect v-model:selected="iso2" />
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or build any responsive popover (popover-on-desktop, drawer-on-mobile):
|
|
169
|
+
|
|
170
|
+
```vue
|
|
171
|
+
<AResponsivePopover v-model:open="open">
|
|
172
|
+
<AResponsivePopoverTrigger as-child>
|
|
173
|
+
<button>Open</button>
|
|
174
|
+
</AResponsivePopoverTrigger>
|
|
175
|
+
<AResponsivePopoverContent>
|
|
176
|
+
<p>Content</p>
|
|
177
|
+
</AResponsivePopoverContent>
|
|
178
|
+
</AResponsivePopover>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Full customization
|
|
182
|
+
|
|
183
|
+
`ATellInput` exposes a deep customization surface across three vectors. Every override is opt-in — defaults are sensible, so a vanilla `<ATellInput />` works out of the box.
|
|
184
|
+
|
|
185
|
+
**Slots (replace any rendered region):**
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
prefix · suffix · trigger · chevron · flag · search · search-icon ·
|
|
189
|
+
loading · empty · group-header · item · item-check · valid-icon ·
|
|
190
|
+
error-icon · hint · error
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Data-override props:**
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
flagUrl?: (iso2, width) => string // swap flagcdn.com for any source
|
|
197
|
+
countries?: CountryOption[] // bypass REST Countries; ship your own list
|
|
198
|
+
searcher?: (query, country) => boolean // custom search (fuzzy/starts-with/locale-aware)
|
|
199
|
+
detector?: (opts) => Promise<string | null> // custom country detection (e.g. server-driven)
|
|
200
|
+
errorMessages?: Partial<Record<PhoneValidationReason, string>> // i18n
|
|
201
|
+
kbdOpen?: string | null // override the '⌘K' hint
|
|
202
|
+
kbdClose?: string | null // override the 'Esc' hint
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Class-override props (every region):** `class`, `fieldClass`, `inputClass`, `contentClass`, `popoverClass`, `drawerClass`, `hintClass`, `errorClass`. All merged via `tailwind-merge`, so you only specify the bits you want to change.
|
|
206
|
+
|
|
207
|
+
**Example — fully bespoke trigger, custom country list, custom searcher, custom detector:**
|
|
208
|
+
|
|
209
|
+
```vue
|
|
210
|
+
<script setup lang="ts">
|
|
211
|
+
import { ATellInput, defaultFlagUrl, type CountryOption } from '@alikhalilll/ui';
|
|
212
|
+
|
|
213
|
+
const countries: CountryOption[] = [
|
|
214
|
+
/* … your curated list … */
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const flagUrl = (iso: string) => `/flags/${iso.toLowerCase()}.svg`;
|
|
218
|
+
const searcher = (q: string, c: CountryOption) =>
|
|
219
|
+
c.raw_data.name.toLowerCase().startsWith(q.toLowerCase());
|
|
220
|
+
|
|
221
|
+
async function detector() {
|
|
222
|
+
// Pretend your backend tells you the user's country from the request
|
|
223
|
+
const { country } = await $fetch('/api/locale');
|
|
224
|
+
return country;
|
|
225
|
+
}
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
<template>
|
|
229
|
+
<ATellInput
|
|
230
|
+
v-model:phone="phone"
|
|
231
|
+
v-model:country="country"
|
|
232
|
+
:countries="countries"
|
|
233
|
+
:flag-url="flagUrl"
|
|
234
|
+
:searcher="searcher"
|
|
235
|
+
:detector="detector"
|
|
236
|
+
>
|
|
237
|
+
<template #trigger="{ selectedCountry, open }">
|
|
238
|
+
<button :data-open="open">
|
|
239
|
+
{{ selectedCountry?.raw_data.iso2 ?? '??' }}
|
|
240
|
+
</button>
|
|
241
|
+
</template>
|
|
242
|
+
</ATellInput>
|
|
243
|
+
</template>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`AInput` also exposes `#prefix` and `#suffix` slots — when either is filled the component switches to a wrapped layout so the bordered field carries the focus ring while the slot content sits inside the border. See the [docs site](https://alikhalilll.github.io/ali-nuxt-toolkit/ui/input) for the full prop tables and a live demo gallery.
|
|
247
|
+
|
|
248
|
+
## Exported types
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import type {
|
|
252
|
+
ATellInputProps,
|
|
253
|
+
ATellInputSize,
|
|
254
|
+
ATellInputVariants,
|
|
255
|
+
CountryOption,
|
|
256
|
+
PhoneValidationResult,
|
|
257
|
+
PhoneValidationReason,
|
|
258
|
+
PhoneRequiredInfo,
|
|
259
|
+
DetectionStrategy,
|
|
260
|
+
DetectCountryOptions,
|
|
261
|
+
UseCountryDetectionReturn,
|
|
262
|
+
UsePhoneValidationReturn,
|
|
263
|
+
FlagUrlBuilder,
|
|
264
|
+
Size,
|
|
265
|
+
} from '@alikhalilll/ui';
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The `defaultFlagUrl(iso2, width)` builder is also exported — handy for composing a custom builder on top of the default:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { defaultFlagUrl } from '@alikhalilll/ui';
|
|
272
|
+
const hiRes = (iso: string) => defaultFlagUrl(iso, 80);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Country detection chain
|
|
276
|
+
|
|
277
|
+
1. **IP geolocation** — fetch `ipEndpoint` (default `https://ipapi.co/json/`), aborted after `timeoutMs` (default 2 s). Result cached in `sessionStorage` so re-mounts skip the network call.
|
|
278
|
+
2. **Timezone** — `Intl.DateTimeFormat().resolvedOptions().timeZone` against a built-in IANA-zone-to-ISO2 map (~70 zones, covers most populated cities).
|
|
279
|
+
3. **`navigator.language`** — extracts the region from tags like `en-US`, `ar-EG`, `pt-BR`.
|
|
280
|
+
4. **`defaultCountry`** — final fallback (`'US'` if you don't set one).
|
|
281
|
+
|
|
282
|
+
Pass `detect-country="locale"` to skip the IP step (no network), or `detect-country="none"` to use `defaultCountry` immediately.
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT © alikhalilll
|