@alikhalilll/ui 1.2.3 → 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 (34) hide show
  1. package/entries/drawer/components/ADrawer.vue +16 -0
  2. package/entries/drawer/components/ADrawerContent.vue +35 -0
  3. package/entries/drawer/components/ADrawerOverlay.vue +25 -0
  4. package/entries/drawer/components/ADrawerTrigger.vue +13 -0
  5. package/entries/drawer/index.ts +4 -0
  6. package/entries/input/components/AInput.vue +111 -0
  7. package/entries/input/index.ts +1 -0
  8. package/entries/popover/components/APopover.vue +19 -0
  9. package/entries/popover/components/APopoverContent.vue +65 -0
  10. package/entries/popover/components/APopoverOverlay.vue +69 -0
  11. package/entries/popover/components/APopoverTrigger.vue +13 -0
  12. package/entries/popover/composables/useEventScrollLock.ts +193 -0
  13. package/entries/popover/index.ts +8 -0
  14. package/entries/responsive-popover/components/AResponsivePopover.vue +67 -0
  15. package/entries/responsive-popover/components/AResponsivePopoverContent.vue +80 -0
  16. package/entries/responsive-popover/components/AResponsivePopoverTrigger.vue +23 -0
  17. package/entries/responsive-popover/composables/useResponsivePopoverContext.ts +20 -0
  18. package/entries/responsive-popover/index.ts +3 -0
  19. package/entries/tell-input/components/ACountryFlag.vue +68 -0
  20. package/entries/tell-input/components/ACountrySelect.vue +522 -0
  21. package/entries/tell-input/components/ATellInput.vue +616 -0
  22. package/entries/tell-input/composables/useCountryDetection.ts +247 -0
  23. package/entries/tell-input/composables/useCountryMatching.ts +213 -0
  24. package/entries/tell-input/composables/usePhoneValidation.ts +573 -0
  25. package/entries/tell-input/composables/useTellInputValidation.ts +136 -0
  26. package/entries/tell-input/composables/useTypingPhase.ts +88 -0
  27. package/entries/tell-input/index.ts +29 -0
  28. package/entries/tell-input/utils/digits.ts +42 -0
  29. package/entries/tell-input/utils/flag-url.ts +10 -0
  30. package/entries/tell-input/utils/types.ts +169 -0
  31. package/package.json +4 -1
  32. package/utils/cn.ts +6 -0
  33. package/utils/index.ts +10 -0
  34. package/utils/sizes.ts +48 -0
@@ -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>