@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 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