@alikhalilll/ui 1.2.2 → 1.2.4

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 (47) hide show
  1. package/dist/entries/drawer/components/ADrawer.vue.d.ts +1 -5
  2. package/dist/entries/drawer/components/ADrawerContent.vue.d.ts +1 -5
  3. package/dist/entries/drawer/components/ADrawerTrigger.vue.d.ts +1 -5
  4. package/dist/entries/input/components/AInput.vue.d.ts +1 -5
  5. package/dist/entries/popover/components/APopover.vue.d.ts +1 -5
  6. package/dist/entries/popover/components/APopoverContent.vue.d.ts +1 -5
  7. package/dist/entries/popover/components/APopoverTrigger.vue.d.ts +1 -5
  8. package/dist/entries/responsive-popover/components/AResponsivePopover.vue.d.ts +1 -5
  9. package/dist/entries/responsive-popover/components/AResponsivePopoverContent.vue.d.ts +1 -5
  10. package/dist/entries/responsive-popover/components/AResponsivePopoverTrigger.vue.d.ts +1 -5
  11. package/dist/entries/tell-input/components/ACountryFlag.vue.d.ts +1 -5
  12. package/dist/entries/tell-input/components/ACountrySelect.vue.d.ts +1 -5
  13. package/dist/entries/tell-input/components/ATellInput.vue.d.ts +1 -5
  14. package/entries/drawer/components/ADrawer.vue +16 -0
  15. package/entries/drawer/components/ADrawerContent.vue +35 -0
  16. package/entries/drawer/components/ADrawerOverlay.vue +25 -0
  17. package/entries/drawer/components/ADrawerTrigger.vue +13 -0
  18. package/entries/drawer/index.ts +4 -0
  19. package/entries/input/components/AInput.vue +111 -0
  20. package/entries/input/index.ts +1 -0
  21. package/entries/popover/components/APopover.vue +19 -0
  22. package/entries/popover/components/APopoverContent.vue +65 -0
  23. package/entries/popover/components/APopoverOverlay.vue +69 -0
  24. package/entries/popover/components/APopoverTrigger.vue +13 -0
  25. package/entries/popover/composables/useEventScrollLock.ts +193 -0
  26. package/entries/popover/index.ts +8 -0
  27. package/entries/responsive-popover/components/AResponsivePopover.vue +67 -0
  28. package/entries/responsive-popover/components/AResponsivePopoverContent.vue +80 -0
  29. package/entries/responsive-popover/components/AResponsivePopoverTrigger.vue +23 -0
  30. package/entries/responsive-popover/composables/useResponsivePopoverContext.ts +20 -0
  31. package/entries/responsive-popover/index.ts +3 -0
  32. package/entries/tell-input/components/ACountryFlag.vue +68 -0
  33. package/entries/tell-input/components/ACountrySelect.vue +522 -0
  34. package/entries/tell-input/components/ATellInput.vue +616 -0
  35. package/entries/tell-input/composables/useCountryDetection.ts +247 -0
  36. package/entries/tell-input/composables/useCountryMatching.ts +213 -0
  37. package/entries/tell-input/composables/usePhoneValidation.ts +573 -0
  38. package/entries/tell-input/composables/useTellInputValidation.ts +136 -0
  39. package/entries/tell-input/composables/useTypingPhase.ts +88 -0
  40. package/entries/tell-input/index.ts +29 -0
  41. package/entries/tell-input/utils/digits.ts +42 -0
  42. package/entries/tell-input/utils/flag-url.ts +10 -0
  43. package/entries/tell-input/utils/types.ts +169 -0
  44. package/package.json +4 -1
  45. package/utils/cn.ts +6 -0
  46. package/utils/index.ts +10 -0
  47. package/utils/sizes.ts +48 -0
@@ -0,0 +1,193 @@
1
+ import { onBeforeUnmount, toValue, watch, type MaybeRefOrGetter, type Ref } from 'vue';
2
+
3
+ export interface UseEventScrollLockOptions {
4
+ /**
5
+ * Element(s) inside the popover whose internal scroll should be preserved.
6
+ * Events whose target is inside one of these are allowed to scroll the
7
+ * element (subject to boundary clamping); everything else is preventDefault'd.
8
+ */
9
+ allowedScrollContainer: MaybeRefOrGetter<HTMLElement | HTMLElement[] | null | undefined>;
10
+ /** Lock activates when this becomes true and tears down when false. */
11
+ active: Ref<boolean> | (() => boolean);
12
+ }
13
+
14
+ // Module-level so multiple popovers stacking share one listener set.
15
+ let refCount = 0;
16
+ const allowedContainers = new Set<HTMLElement>();
17
+
18
+ const SCROLL_KEYS = new Set([
19
+ 'ArrowUp',
20
+ 'ArrowDown',
21
+ 'ArrowLeft',
22
+ 'ArrowRight',
23
+ 'PageUp',
24
+ 'PageDown',
25
+ 'Home',
26
+ 'End',
27
+ 'Space',
28
+ ' ',
29
+ ]);
30
+
31
+ function insideAllowed(target: EventTarget | null): boolean {
32
+ if (!(target instanceof Node)) return false;
33
+ for (const el of allowedContainers) if (el.contains(target)) return true;
34
+ return false;
35
+ }
36
+
37
+ // Walk up from the event target; if any ancestor (up to and including the
38
+ // allowed container) is a scroller that can absorb the delta in this direction
39
+ // without overshooting its boundary, allow the event.
40
+ function canConsume(el: HTMLElement, dx: number, dy: number): boolean {
41
+ let node: HTMLElement | null = el;
42
+ while (node) {
43
+ const s = getComputedStyle(node);
44
+ const scrollsY =
45
+ (s.overflowY === 'auto' || s.overflowY === 'scroll') && node.scrollHeight > node.clientHeight;
46
+ if (scrollsY && dy !== 0) {
47
+ const atTop = node.scrollTop <= 0;
48
+ const atBottom = node.scrollTop + node.clientHeight >= node.scrollHeight - 1;
49
+ if (!(dy < 0 && atTop) && !(dy > 0 && atBottom)) return true;
50
+ }
51
+ const scrollsX =
52
+ (s.overflowX === 'auto' || s.overflowX === 'scroll') && node.scrollWidth > node.clientWidth;
53
+ if (scrollsX && dx !== 0) {
54
+ const atLeft = node.scrollLeft <= 0;
55
+ const atRight = node.scrollLeft + node.clientWidth >= node.scrollWidth - 1;
56
+ if (!(dx < 0 && atLeft) && !(dx > 0 && atRight)) return true;
57
+ }
58
+ if (allowedContainers.has(node)) break;
59
+ node = node.parentElement;
60
+ }
61
+ return false;
62
+ }
63
+
64
+ function onWheel(e: WheelEvent) {
65
+ const t = (e.composedPath()[0] ?? e.target) as HTMLElement | null;
66
+ if (!t || !insideAllowed(t)) {
67
+ e.preventDefault();
68
+ return;
69
+ }
70
+ if (!canConsume(t, e.deltaX, e.deltaY)) e.preventDefault();
71
+ }
72
+
73
+ let touchStartY = 0;
74
+ let touchStartX = 0;
75
+ function onTouchStart(e: TouchEvent) {
76
+ if (e.touches.length === 1) {
77
+ touchStartY = e.touches[0]!.clientY;
78
+ touchStartX = e.touches[0]!.clientX;
79
+ }
80
+ }
81
+ function onTouchMove(e: TouchEvent) {
82
+ const t = (e.composedPath()[0] ?? e.target) as HTMLElement | null;
83
+ if (!t || !insideAllowed(t)) {
84
+ e.preventDefault();
85
+ return;
86
+ }
87
+ if (e.touches.length !== 1) {
88
+ e.preventDefault();
89
+ return;
90
+ }
91
+ const dy = touchStartY - e.touches[0]!.clientY;
92
+ const dx = touchStartX - e.touches[0]!.clientX;
93
+ if (!canConsume(t, dx, dy)) e.preventDefault();
94
+ }
95
+
96
+ function onKeyDown(e: KeyboardEvent) {
97
+ if (!SCROLL_KEYS.has(e.key)) return;
98
+ // Popover owns its own keyboard model (search input, list navigation).
99
+ if (insideAllowed(e.target)) return;
100
+ e.preventDefault();
101
+ }
102
+
103
+ function activate() {
104
+ if (refCount === 0) {
105
+ document.addEventListener('wheel', onWheel, {
106
+ passive: false,
107
+ capture: true,
108
+ });
109
+ document.addEventListener('touchstart', onTouchStart, {
110
+ passive: true,
111
+ capture: true,
112
+ });
113
+ document.addEventListener('touchmove', onTouchMove, {
114
+ passive: false,
115
+ capture: true,
116
+ });
117
+ document.addEventListener('keydown', onKeyDown, { capture: true });
118
+ }
119
+ refCount++;
120
+ }
121
+
122
+ function deactivate() {
123
+ refCount = Math.max(0, refCount - 1);
124
+ if (refCount === 0) {
125
+ document.removeEventListener('wheel', onWheel, { capture: true });
126
+ document.removeEventListener('touchstart', onTouchStart, { capture: true });
127
+ document.removeEventListener('touchmove', onTouchMove, { capture: true });
128
+ document.removeEventListener('keydown', onKeyDown, { capture: true });
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Sticky-safe scroll lock: prevents page scroll by intercepting wheel/touch/key
134
+ * events at `document` capture phase, instead of mutating `body { overflow }`.
135
+ * The page scrollbar stays visible and `position: sticky` keeps working.
136
+ *
137
+ * Pass the element(s) whose own scroll should still work (e.g. a popover's
138
+ * inner search list) as `allowedScrollContainer`.
139
+ */
140
+ export function useEventScrollLock(opts: UseEventScrollLockOptions): void {
141
+ if (typeof document === 'undefined') return;
142
+
143
+ let registered: HTMLElement[] = [];
144
+ let activeNow = false;
145
+
146
+ const register = () => {
147
+ const raw = toValue(opts.allowedScrollContainer);
148
+ const list = Array.isArray(raw) ? (raw.filter(Boolean) as HTMLElement[]) : raw ? [raw] : [];
149
+ for (const el of list) allowedContainers.add(el);
150
+ registered = list;
151
+ };
152
+ const unregister = () => {
153
+ for (const el of registered) allowedContainers.delete(el);
154
+ registered = [];
155
+ };
156
+
157
+ const stopActive = watch(
158
+ () => (typeof opts.active === 'function' ? opts.active() : opts.active.value),
159
+ (v) => {
160
+ if (v && !activeNow) {
161
+ register();
162
+ activate();
163
+ activeNow = true;
164
+ } else if (!v && activeNow) {
165
+ deactivate();
166
+ unregister();
167
+ activeNow = false;
168
+ }
169
+ },
170
+ { immediate: true, flush: 'post' }
171
+ );
172
+
173
+ const stopContainer = watch(
174
+ () => toValue(opts.allowedScrollContainer),
175
+ () => {
176
+ if (activeNow) {
177
+ unregister();
178
+ register();
179
+ }
180
+ },
181
+ { flush: 'post' }
182
+ );
183
+
184
+ onBeforeUnmount(() => {
185
+ stopActive();
186
+ stopContainer();
187
+ if (activeNow) {
188
+ deactivate();
189
+ unregister();
190
+ activeNow = false;
191
+ }
192
+ });
193
+ }
@@ -0,0 +1,8 @@
1
+ export { default as APopover } from './components/APopover.vue';
2
+ export { default as APopoverTrigger } from './components/APopoverTrigger.vue';
3
+ export { default as APopoverContent } from './components/APopoverContent.vue';
4
+ export { default as APopoverOverlay } from './components/APopoverOverlay.vue';
5
+ export {
6
+ useEventScrollLock,
7
+ type UseEventScrollLockOptions,
8
+ } from './composables/useEventScrollLock';
@@ -0,0 +1,67 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { useMediaQuery } from '@vueuse/core';
4
+ import { APopover } from '@/entries/popover';
5
+ import { ADrawer } from '@/entries/drawer';
6
+ import { provideResponsivePopoverContext } from '../composables/useResponsivePopoverContext';
7
+
8
+ export type ScrollLockMode = 'events' | 'body' | 'none';
9
+
10
+ const props = withDefaults(
11
+ defineProps<{
12
+ /** CSS media query for the desktop break. Below this width we render a vaul drawer. */
13
+ breakpoint?: string;
14
+ /**
15
+ * @deprecated prefer `scrollLock`. Kept for back-compat: `modal=false` is a shorthand
16
+ * for `scrollLock="none"` (tooltip-style popover). `modal=true` (default) defers to
17
+ * `scrollLock`, which controls how page scroll is blocked.
18
+ */
19
+ modal?: boolean;
20
+ /**
21
+ * How desktop page scroll is blocked while the popover is open:
22
+ * - `'events'` (default) — wheel/touch/keyboard intercepted at document level.
23
+ * Page scrollbar stays visible; `position: sticky` keeps working.
24
+ * - `'body'` — legacy `document.body.style.overflow='hidden'` lock. Use when the
25
+ * page must reflow as the scrollbar goes away.
26
+ * - `'none'` — no page-scroll lock at all.
27
+ *
28
+ * Drawer (mobile) branch is unaffected — vaul-vue owns its own lock.
29
+ */
30
+ scrollLock?: ScrollLockMode;
31
+ }>(),
32
+ { breakpoint: '(min-width: 768px)', modal: true, scrollLock: 'events' }
33
+ );
34
+
35
+ const open = defineModel<boolean>('open');
36
+
37
+ const isDesktop = useMediaQuery(() => props.breakpoint);
38
+
39
+ /**
40
+ * Pre-imported on both branches — do NOT lazy-load. Switching the component identity at runtime
41
+ * means we still hydrate the right tree client-side.
42
+ */
43
+ const Root = computed(() => (isDesktop.value ? APopover : ADrawer));
44
+
45
+ /**
46
+ * Only `scrollLock='body'` triggers reka-ui's `PopoverContentModal` (and its
47
+ * `useBodyScrollLock`). For `'events'` we install our own document-level event lock in
48
+ * `AResponsivePopoverContent`. For `'none'` nothing locks. Legacy `modal=false` still
49
+ * forces non-modal regardless of `scrollLock`.
50
+ */
51
+ const rekaModal = computed(() => {
52
+ if (props.modal === false) return false;
53
+ return props.scrollLock === 'body';
54
+ });
55
+
56
+ provideResponsivePopoverContext({
57
+ open: computed(() => open.value ?? false),
58
+ isDesktop: computed(() => isDesktop.value),
59
+ scrollLock: computed(() => props.scrollLock),
60
+ });
61
+ </script>
62
+
63
+ <template>
64
+ <component :is="Root" v-model:open="open" :modal="rekaModal" data-slot="responsive-popover">
65
+ <slot :is-desktop="isDesktop" />
66
+ </component>
67
+ </template>
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import { computed } from 'vue';
4
+ import { useMediaQuery } from '@vueuse/core';
5
+ import { APopoverContent, useEventScrollLock } from '@/entries/popover';
6
+ import { ADrawerContent } from '@/entries/drawer';
7
+ import { useResponsivePopoverContext } from '../composables/useResponsivePopoverContext';
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ breakpoint?: string;
12
+ /** Classes applied on both branches. Avoid width / inset classes here. */
13
+ class?: HTMLAttributes['class'];
14
+ /** Classes applied only when the popover (desktop) branch is rendered. */
15
+ popoverClass?: HTMLAttributes['class'];
16
+ /** Classes applied only when the drawer (mobile) branch is rendered. */
17
+ drawerClass?: HTMLAttributes['class'];
18
+ /**
19
+ * Render the dimmed overlay on the desktop popover branch. Defaults to `false` — popovers
20
+ * on desktop are non-modal-looking by convention. The mobile drawer always has its own
21
+ * overlay (vaul-vue's `DrawerOverlay`) regardless of this prop.
22
+ */
23
+ overlay?: boolean;
24
+ align?: 'start' | 'center' | 'end';
25
+ sideOffset?: number;
26
+ }>(),
27
+ {
28
+ breakpoint: '(min-width: 768px)',
29
+ align: 'start',
30
+ sideOffset: 4,
31
+ overlay: false,
32
+ }
33
+ );
34
+
35
+ const ctx = useResponsivePopoverContext();
36
+
37
+ // Prefer the root's media query (so both layers agree). Fall back to a local one when this
38
+ // component is used outside `AResponsivePopover` (unusual but supported).
39
+ const fallbackIsDesktop = useMediaQuery(() => props.breakpoint);
40
+ const isDesktop = computed(() => ctx?.isDesktop.value ?? fallbackIsDesktop.value);
41
+
42
+ const scrollLockMode = computed(() => ctx?.scrollLock.value ?? 'events');
43
+ const overlayLockScroll = computed(() => scrollLockMode.value === 'body');
44
+
45
+ const mergedClass = computed(() => [
46
+ props.class,
47
+ isDesktop.value ? props.popoverClass : props.drawerClass,
48
+ ]);
49
+
50
+ // Sticky-safe scroll lock — only active while the popover is open on desktop and the root
51
+ // asked for the event-based strategy. The getter resolves every responsive popover content
52
+ // element currently in the DOM, which lets stacked popovers share the lock cleanly.
53
+ useEventScrollLock({
54
+ allowedScrollContainer: () => {
55
+ if (typeof document === 'undefined') return [];
56
+ return Array.from(
57
+ document.querySelectorAll<HTMLElement>('[data-responsive-popover-scroll-container="true"]')
58
+ );
59
+ },
60
+ active: computed(() => !!ctx?.open.value && isDesktop.value && scrollLockMode.value === 'events'),
61
+ });
62
+ </script>
63
+
64
+ <template>
65
+ <APopoverContent
66
+ v-if="isDesktop"
67
+ :overlay="props.overlay"
68
+ :overlay-lock-scroll="overlayLockScroll"
69
+ :align="props.align"
70
+ :side-offset="props.sideOffset"
71
+ :class="mergedClass"
72
+ data-slot="responsive-popover-content"
73
+ data-responsive-popover-scroll-container="true"
74
+ >
75
+ <slot />
76
+ </APopoverContent>
77
+ <ADrawerContent v-else :class="mergedClass" data-slot="responsive-popover-content">
78
+ <slot />
79
+ </ADrawerContent>
80
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { useMediaQuery } from '@vueuse/core';
4
+ import { APopoverTrigger } from '@/entries/popover';
5
+ import { ADrawerTrigger } from '@/entries/drawer';
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ breakpoint?: string;
10
+ asChild?: boolean;
11
+ }>(),
12
+ { breakpoint: '(min-width: 768px)' }
13
+ );
14
+
15
+ const isDesktop = useMediaQuery(() => props.breakpoint);
16
+ const Trigger = computed(() => (isDesktop.value ? APopoverTrigger : ADrawerTrigger));
17
+ </script>
18
+
19
+ <template>
20
+ <component :is="Trigger" :as-child="props.asChild" data-slot="responsive-popover-trigger">
21
+ <slot />
22
+ </component>
23
+ </template>
@@ -0,0 +1,20 @@
1
+ import { inject, provide, type ComputedRef, type InjectionKey } from 'vue';
2
+ import type { ScrollLockMode } from '../components/AResponsivePopover.vue';
3
+
4
+ export interface ResponsivePopoverContext {
5
+ open: ComputedRef<boolean>;
6
+ isDesktop: ComputedRef<boolean>;
7
+ scrollLock: ComputedRef<ScrollLockMode>;
8
+ }
9
+
10
+ const RESPONSIVE_POPOVER_CONTEXT: InjectionKey<ResponsivePopoverContext> = Symbol(
11
+ 'AResponsivePopoverContext'
12
+ );
13
+
14
+ export function provideResponsivePopoverContext(ctx: ResponsivePopoverContext) {
15
+ provide(RESPONSIVE_POPOVER_CONTEXT, ctx);
16
+ }
17
+
18
+ export function useResponsivePopoverContext(): ResponsivePopoverContext | null {
19
+ return inject(RESPONSIVE_POPOVER_CONTEXT, null);
20
+ }
@@ -0,0 +1,3 @@
1
+ export { default as AResponsivePopover } from './components/AResponsivePopover.vue';
2
+ export { default as AResponsivePopoverTrigger } from './components/AResponsivePopoverTrigger.vue';
3
+ export { default as AResponsivePopoverContent } from './components/AResponsivePopoverContent.vue';
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import { computed, ref, watch } from 'vue';
4
+ import { cn } from '@/utils';
5
+ import { defaultFlagUrl, type FlagUrlBuilder } from '../utils/flag-url';
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ /** ISO 3166-1 alpha-2 country code, case-insensitive. */
10
+ iso2: string;
11
+ /** Pixel width served by flagcdn. 40 is crisp at retina up to ~24px wide. */
12
+ width?: number;
13
+ /** Optional explicit URL override. When set, `iso2` / `width` / `flagUrl` are ignored. */
14
+ src?: string | null;
15
+ /** Function `(iso2, width) => string` — fully replace the URL builder. */
16
+ flagUrl?: FlagUrlBuilder;
17
+ alt?: string;
18
+ class?: HTMLAttributes['class'];
19
+ }>(),
20
+ { width: 40 }
21
+ );
22
+
23
+ const url = computed(() => {
24
+ if (props.src) return props.src;
25
+ if (!props.iso2) return null;
26
+ return (props.flagUrl ?? defaultFlagUrl)(props.iso2, props.width);
27
+ });
28
+
29
+ // Image load failure → fall back to the ISO2 text badge. The flag URL can change as the
30
+ // user switches country, so reset the error flag whenever the URL changes.
31
+ const failed = ref(false);
32
+ watch(url, () => {
33
+ failed.value = false;
34
+ });
35
+
36
+ const iso2Label = computed(() => (props.iso2 ?? '').slice(0, 2).toUpperCase());
37
+ </script>
38
+
39
+ <template>
40
+ <img
41
+ v-if="url && !failed"
42
+ :src="url"
43
+ :alt="props.alt ?? `${props.iso2} flag`"
44
+ loading="lazy"
45
+ data-slot="country-flag"
46
+ :class="cn('ring-border/40 inline-block h-4 w-6 rounded-sm object-cover ring-1', props.class)"
47
+ @error="failed = true"
48
+ />
49
+ <span
50
+ v-else-if="iso2Label"
51
+ data-slot="country-flag-fallback"
52
+ :aria-label="props.alt ?? `${props.iso2} flag`"
53
+ :class="
54
+ cn(
55
+ 'ring-border/40 bg-muted text-muted-foreground inline-flex h-4 w-6 items-center justify-center rounded-sm text-[8px] font-semibold leading-none tracking-tight ring-1',
56
+ props.class
57
+ )
58
+ "
59
+ >
60
+ {{ iso2Label }}
61
+ </span>
62
+ <slot v-else name="empty">
63
+ <span
64
+ data-slot="country-flag-empty"
65
+ :class="cn('bg-muted inline-block h-4 w-6 rounded-sm', props.class)"
66
+ />
67
+ </slot>
68
+ </template>