adata-ui 2.1.40-beta → 2.1.40-beta.2
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/components/elements/a-select-row/ASelectRowV2.vue +213 -0
- package/components/elements/button/AButtonV2.vue +89 -0
- package/components/elements/segmented/ASegmentedV2.vue +58 -0
- package/components/elements/select/ASelectV2.vue +581 -0
- package/components/elements/show-more/AShowMoreV2.vue +26 -0
- package/components/features/pk-mobile-services/APkMobileServices.vue +5 -27
- package/components/forms/checkbox/ACheckboxV2.vue +229 -0
- package/components/forms/input/AInputV2.vue +542 -0
- package/components/forms/toggle/AToggleV2.vue +71 -0
- package/components/navigation/header/NavList.vue +28 -48
- package/components/navigation/header/ProductMenu.vue +16 -10
- package/components/navigation/pill-tabs/APillTabsV2.vue +118 -0
- package/components/overlays/modal/AModalV2.vue +388 -0
- package/composables/useActiveNavigation.ts +84 -0
- package/composables/useChipOverflow.ts +82 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { useActiveNavigation } from '#adata-ui/composables/useActiveNavigation'
|
|
3
3
|
|
|
4
4
|
interface NavList {
|
|
5
5
|
title: string
|
|
@@ -18,61 +18,28 @@ const props = defineProps<{
|
|
|
18
18
|
currentModule: boolean
|
|
19
19
|
}>()
|
|
20
20
|
|
|
21
|
+
const { isActiveService } = useActiveNavigation()
|
|
22
|
+
|
|
21
23
|
function isActive(itemPath: string) {
|
|
22
24
|
if (!props.currentModule) return false
|
|
23
|
-
|
|
24
|
-
const currentUrl = window.location.href.split('/').filter(item => !['kk', 'en'].includes(item)).join('/')
|
|
25
|
-
const section = PAGES[props.id]
|
|
26
|
-
|
|
27
|
-
const currentPath = itemPath.split('/').filter(item => !['kk', 'en'].includes(item)).join('/')
|
|
28
|
-
|
|
29
|
-
if (currentUrl === currentPath) return true
|
|
30
|
-
|
|
31
|
-
if (currentPath.endsWith('/')) {
|
|
32
|
-
let atLeastOne = false
|
|
33
|
-
|
|
34
|
-
for (const key in section) {
|
|
35
|
-
const path = section[key]
|
|
36
|
-
if (key !== 'main') {
|
|
37
|
-
const includes = currentUrl.includes(path) || currentUrl.includes('car-result')
|
|
38
|
-
if (includes) {
|
|
39
|
-
atLeastOne = true
|
|
40
|
-
break
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return !atLeastOne
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (currentPath.includes('check-car')) {
|
|
49
|
-
if (currentUrl.includes('car-result')) {
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
for (const key in section) {
|
|
55
|
-
const path = section[key]
|
|
56
|
-
if (key !== 'main') {
|
|
57
|
-
const includesBoth = currentUrl.includes(path) && currentUrl.startsWith(currentPath)
|
|
58
|
-
if (includesBoth) {
|
|
59
|
-
return true
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return false
|
|
25
|
+
return isActiveService(itemPath)
|
|
65
26
|
}
|
|
66
27
|
</script>
|
|
67
28
|
|
|
68
29
|
<template>
|
|
69
|
-
<section
|
|
30
|
+
<section
|
|
31
|
+
class="group/section overflow-hidden rounded-xl border p-3 shadow-sm transition-colors"
|
|
32
|
+
:class="currentModule
|
|
33
|
+
? 'border-blue-700/20 bg-blue-50 hover:border-blue-700/40 dark:border-blue-500/20 dark:bg-blue-500/5 dark:hover:border-blue-500/40'
|
|
34
|
+
: 'border-gray-200 bg-white hover:border-blue-700/40 dark:border-gray-800 dark:bg-gray-950 dark:hover:border-blue-500/40'"
|
|
35
|
+
>
|
|
70
36
|
<div class="mb-3 flex items-start justify-between gap-2">
|
|
71
37
|
<nuxt-link-locale
|
|
72
38
|
:to="link"
|
|
73
39
|
class="group/title flex min-w-0 items-center gap-2 text-sm font-bold text-deepblue-900 transition-colors hover:text-blue-700 dark:text-gray-100 dark:hover:text-blue-500"
|
|
74
40
|
>
|
|
75
|
-
<span
|
|
41
|
+
<span
|
|
42
|
+
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-blue-700/10 text-blue-700 transition-colors group-hover/title:bg-blue-700 group-hover/title:text-white dark:bg-blue-500/10 dark:text-blue-500">
|
|
76
43
|
<component
|
|
77
44
|
:is="icon"
|
|
78
45
|
class="size-4"
|
|
@@ -86,7 +53,8 @@ function isActive(itemPath: string) {
|
|
|
86
53
|
v-if="badge"
|
|
87
54
|
class="rounded-full bg-blue-700/10 px-1.5 py-0.5 text-[10px] font-bold uppercase leading-none text-blue-700 dark:bg-blue-500/10 dark:text-blue-500"
|
|
88
55
|
>NEW</span>
|
|
89
|
-
<a-icon-arrow-side-up
|
|
56
|
+
<a-icon-arrow-side-up
|
|
57
|
+
class="size-4 text-gray-500 transition-colors group-hover/section:text-blue-700 dark:group-hover/section:text-blue-500"/>
|
|
90
58
|
</div>
|
|
91
59
|
</div>
|
|
92
60
|
|
|
@@ -102,13 +70,25 @@ function isActive(itemPath: string) {
|
|
|
102
70
|
class="group/item flex items-center gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-blue-100 dark:hover:bg-gray-200/10"
|
|
103
71
|
:class="{ 'bg-blue-100 dark:bg-gray-200/10': isActive(item.to) }"
|
|
104
72
|
>
|
|
105
|
-
<span
|
|
73
|
+
<span
|
|
74
|
+
class="flex size-7 shrink-0 items-center justify-center rounded-md transition-colors group-hover/item:bg-blue-700/10 group-hover/item:text-blue-700 dark:group-hover/item:text-blue-500"
|
|
75
|
+
:class="{
|
|
76
|
+
'bg-blue-700/10 text-blue-700 dark:bg-blue-500/10 dark:text-blue-500': isActive(item.to),
|
|
77
|
+
'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300': !isActive(item.to),
|
|
78
|
+
// 'bg-white text-gray-600 dark:bg-white dark:text-gray-300': currentModule,
|
|
79
|
+
}"
|
|
80
|
+
>
|
|
106
81
|
<component
|
|
107
82
|
:is="item.icon"
|
|
108
83
|
class="size-[14px]"
|
|
109
84
|
/>
|
|
110
85
|
</span>
|
|
111
|
-
<span
|
|
86
|
+
<span
|
|
87
|
+
class="truncate text-sm font-semibold normal-case transition-colors group-hover/item:text-deepblue-900 dark:group-hover/item:text-gray-100"
|
|
88
|
+
:class="isActive(item.to)
|
|
89
|
+
? 'text-blue-700 dark:text-blue-500'
|
|
90
|
+
: 'text-gray-600 dark:text-gray-300'"
|
|
91
|
+
>{{ item.title }}</span>
|
|
112
92
|
</nuxt-link-locale>
|
|
113
93
|
</li>
|
|
114
94
|
</ul>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import CardGallery from '#adata-ui/components/navigation/header/CardGallery.vue'
|
|
3
3
|
import NavList from '#adata-ui/components/navigation/header/NavList.vue'
|
|
4
|
+
import { useActiveNavigation } from '#adata-ui/composables/useActiveNavigation'
|
|
4
5
|
import { useHeaderNavigationLinks } from '#adata-ui/composables/useHeaderNavigationLinks'
|
|
5
6
|
|
|
6
7
|
defineProps<{ url?: string }>()
|
|
@@ -8,6 +9,7 @@ defineEmits(['outerClick', 'mouseOver'])
|
|
|
8
9
|
|
|
9
10
|
const { t } = useI18n()
|
|
10
11
|
const modules = useHeaderNavigationLinks()
|
|
12
|
+
const { isActiveModule } = useActiveNavigation()
|
|
11
13
|
|
|
12
14
|
// Fixed EDO promo card on the left (like edo-editor); EDO is excluded from the catalog grid below.
|
|
13
15
|
const edoModule = computed(() => modules.value.find(module => module.key === 'edo') ?? null)
|
|
@@ -32,35 +34,39 @@ const catalogColumns = computed(() =>
|
|
|
32
34
|
<nuxt-link-locale
|
|
33
35
|
v-if="edoModule"
|
|
34
36
|
:to="edoModule.link"
|
|
35
|
-
class="group block overflow-hidden rounded-xl border border-blue-700/20
|
|
37
|
+
class="group block overflow-hidden rounded-xl border border-blue-700/20 p-4 shadow-sm transition-colors hover:border-blue-700/40 hover:bg-blue-100 dark:border-blue-500/20 dark:hover:border-blue-500/40 dark:hover:bg-blue-500/10"
|
|
36
38
|
:data-test-id="edoModule.data_attribute"
|
|
37
39
|
>
|
|
38
40
|
<div class="flex items-start gap-3">
|
|
39
|
-
<div
|
|
41
|
+
<div
|
|
42
|
+
class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-blue-700 text-white shadow-sm">
|
|
40
43
|
<component
|
|
41
44
|
:is="edoModule.icon"
|
|
42
45
|
class="size-5"
|
|
43
46
|
/>
|
|
44
47
|
</div>
|
|
45
48
|
<div class="min-w-0">
|
|
46
|
-
<div class="flex items-center gap-2">
|
|
49
|
+
<div class="flex items-center justify-between gap-2">
|
|
47
50
|
<h3 class="text-base font-bold normal-case text-deepblue-900 dark:text-gray-100">
|
|
48
51
|
{{ edoModule.name }}
|
|
49
52
|
</h3>
|
|
50
|
-
<
|
|
53
|
+
<div class="flex items-center gap-1 justify-end">
|
|
54
|
+
<span
|
|
55
|
+
v-if="edoModule.is_new"
|
|
56
|
+
class="rounded-full bg-blue-700/10 px-1.5 py-0.5 text-[10px] font-bold uppercase leading-none text-blue-700 dark:bg-blue-500/10 dark:text-blue-500"
|
|
57
|
+
>NEW</span>
|
|
58
|
+
<a-icon-arrow-side-up
|
|
59
|
+
class="size-4 text-gray-500 transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-blue-700 dark:text-gray-400 dark:group-hover:text-blue-500"/>
|
|
60
|
+
</div>
|
|
51
61
|
</div>
|
|
52
62
|
<p class="mt-1 text-sm normal-case leading-snug text-gray-600 dark:text-gray-300">
|
|
53
63
|
{{ t('header.products.edo.heroSubtitle') }}
|
|
54
64
|
</p>
|
|
55
65
|
</div>
|
|
56
66
|
</div>
|
|
57
|
-
<div class="mt-4 inline-flex items-center gap-1.5 text-sm font-semibold normal-case text-blue-700 dark:text-blue-500">
|
|
58
|
-
{{ t('header.products.edo.heroCta') }}
|
|
59
|
-
<a-icon-arrow-side-up class="size-3.5" />
|
|
60
|
-
</div>
|
|
61
67
|
</nuxt-link-locale>
|
|
62
68
|
|
|
63
|
-
<card-gallery class="hidden lg:block"
|
|
69
|
+
<card-gallery class="hidden lg:block"/>
|
|
64
70
|
</aside>
|
|
65
71
|
|
|
66
72
|
<!-- Catalog grid: products as bordered section cards (EDO lives in the hero). -->
|
|
@@ -74,7 +80,7 @@ const catalogColumns = computed(() =>
|
|
|
74
80
|
v-for="module in column"
|
|
75
81
|
:id="module.key"
|
|
76
82
|
:key="module.key"
|
|
77
|
-
:current-module="
|
|
83
|
+
:current-module="isActiveModule(module)"
|
|
78
84
|
:title="module.name"
|
|
79
85
|
:link="module.link"
|
|
80
86
|
:nav-list="module.items"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
type CountColor = 'gray' | 'blue' | 'yellow' | 'red' | 'green' | 'neutral'
|
|
3
|
+
|
|
4
|
+
export interface PillTab {
|
|
5
|
+
key: string
|
|
6
|
+
name: string
|
|
7
|
+
count?: number
|
|
8
|
+
countColor?: CountColor
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
hideCount?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<{
|
|
14
|
+
modelValue: string
|
|
15
|
+
options: PillTab[]
|
|
16
|
+
showCount?: boolean
|
|
17
|
+
ariaLabel?: string
|
|
18
|
+
block?: boolean
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
size?: 'sm' | 'md'
|
|
21
|
+
}>(), {
|
|
22
|
+
showCount: false,
|
|
23
|
+
block: false,
|
|
24
|
+
disabled: false,
|
|
25
|
+
size: 'sm',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{ (e: 'update:modelValue', value: string): void }>()
|
|
29
|
+
|
|
30
|
+
const SIZE_CLASS: Record<'sm' | 'md', string> = {
|
|
31
|
+
sm: 'text-xs',
|
|
32
|
+
md: 'text-sm',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const COUNT_COLOR_CLASS: Record<CountColor, string> = {
|
|
36
|
+
gray: 'bg-gray-200/70 text-gray-600 dark:bg-white/[0.08] dark:text-gray-300',
|
|
37
|
+
blue: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
|
38
|
+
yellow: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300',
|
|
39
|
+
red: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
|
|
40
|
+
green: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
|
|
41
|
+
neutral: 'bg-gray-100 text-gray-500 dark:bg-white/[0.06] dark:text-gray-400',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { t, te } = useI18n()
|
|
45
|
+
|
|
46
|
+
function localize(label: string) {
|
|
47
|
+
return te(label) ? t(label) : label
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isDisabled(option: PillTab) {
|
|
51
|
+
return props.disabled || option.disabled === true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function select(option: PillTab) {
|
|
55
|
+
if (isDisabled(option)) return
|
|
56
|
+
emit('update:modelValue', option.key)
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<div
|
|
62
|
+
class="pill-tabs-v2 min-w-0 max-w-full rounded-xl border border-gray-200 bg-gray-50 p-1 dark:border-white/10 dark:bg-white/[0.03]"
|
|
63
|
+
:class="[
|
|
64
|
+
block ? 'w-full' : 'w-fit',
|
|
65
|
+
disabled ? 'pill-tabs-v2--disabled opacity-60' : '',
|
|
66
|
+
]"
|
|
67
|
+
:aria-disabled="disabled || undefined"
|
|
68
|
+
>
|
|
69
|
+
<div
|
|
70
|
+
role="tablist"
|
|
71
|
+
:aria-label="ariaLabel"
|
|
72
|
+
class="pill-tabs-v2__list flex w-full flex-nowrap items-center gap-1 overflow-x-auto"
|
|
73
|
+
>
|
|
74
|
+
<button
|
|
75
|
+
v-for="option in options"
|
|
76
|
+
:key="option.key"
|
|
77
|
+
type="button"
|
|
78
|
+
role="tab"
|
|
79
|
+
:aria-selected="modelValue === option.key"
|
|
80
|
+
:aria-disabled="isDisabled(option) || undefined"
|
|
81
|
+
:disabled="isDisabled(option)"
|
|
82
|
+
:tabindex="modelValue === option.key ? 0 : -1"
|
|
83
|
+
class="inline-flex items-center gap-1.5 whitespace-nowrap rounded-lg px-3 py-1.5 font-medium transition-colors duration-150"
|
|
84
|
+
:class="[
|
|
85
|
+
SIZE_CLASS[size],
|
|
86
|
+
isDisabled(option)
|
|
87
|
+
? 'cursor-not-allowed text-gray-300 dark:text-gray-600'
|
|
88
|
+
: modelValue === option.key
|
|
89
|
+
? 'text-deepblue-900 bg-white shadow-sm dark:bg-gray-700 dark:text-gray-100 dark:shadow-none dark:ring-1 dark:ring-inset dark:ring-white/10'
|
|
90
|
+
: 'text-gray-700 hover:bg-white/70 hover:text-gray-900 dark:text-gray-200 dark:hover:bg-white/[0.06] dark:hover:text-gray-100',
|
|
91
|
+
block ? 'flex-1 basis-0 justify-center' : 'shrink-0',
|
|
92
|
+
]"
|
|
93
|
+
@click="select(option)"
|
|
94
|
+
>
|
|
95
|
+
<slot name="option" :option="option">
|
|
96
|
+
<span>{{ localize(option.name) }}</span>
|
|
97
|
+
|
|
98
|
+
<span
|
|
99
|
+
v-if="showCount && !option.hideCount"
|
|
100
|
+
class="inline-flex min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-semibold leading-5"
|
|
101
|
+
:class="COUNT_COLOR_CLASS[option.countColor ?? 'gray']"
|
|
102
|
+
>
|
|
103
|
+
{{ option.count }}
|
|
104
|
+
</span>
|
|
105
|
+
</slot>
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<style scoped>
|
|
112
|
+
.pill-tabs-v2__list {
|
|
113
|
+
scrollbar-width: none;
|
|
114
|
+
}
|
|
115
|
+
.pill-tabs-v2__list::-webkit-scrollbar {
|
|
116
|
+
display: none;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onKeyStroke, useScrollLock, useWindowSize } from '@vueuse/core'
|
|
3
|
+
|
|
4
|
+
type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'container' | 'fullscreen'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
title?: string
|
|
8
|
+
size?: ModalSize
|
|
9
|
+
width?: string
|
|
10
|
+
closeOnOverlay?: boolean
|
|
11
|
+
closeOnEsc?: boolean
|
|
12
|
+
lockScroll?: boolean
|
|
13
|
+
persistent?: boolean
|
|
14
|
+
transition?: boolean
|
|
15
|
+
contentClass?: string
|
|
16
|
+
hideHeader?: boolean
|
|
17
|
+
blur?: boolean | 'sm' | 'md' | 'lg' | 'xl'
|
|
18
|
+
mobileSheet?: boolean
|
|
19
|
+
swipeToClose?: boolean
|
|
20
|
+
swipeThreshold?: number
|
|
21
|
+
showDragHandle?: boolean
|
|
22
|
+
}>(), {
|
|
23
|
+
size: 'md',
|
|
24
|
+
closeOnOverlay: true,
|
|
25
|
+
closeOnEsc: true,
|
|
26
|
+
lockScroll: true,
|
|
27
|
+
persistent: false,
|
|
28
|
+
transition: true,
|
|
29
|
+
hideHeader: false,
|
|
30
|
+
blur: false,
|
|
31
|
+
mobileSheet: true,
|
|
32
|
+
swipeToClose: true,
|
|
33
|
+
swipeThreshold: 65,
|
|
34
|
+
showDragHandle: true,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits<{
|
|
38
|
+
(e: 'close'): void
|
|
39
|
+
(e: 'open'): void
|
|
40
|
+
}>()
|
|
41
|
+
|
|
42
|
+
const open = defineModel<boolean>({ required: true })
|
|
43
|
+
|
|
44
|
+
const contentRef = ref<HTMLElement | null>(null)
|
|
45
|
+
const overlayRef = ref<HTMLElement | null>(null)
|
|
46
|
+
const swipeClosing = ref(false)
|
|
47
|
+
const bodyScrollLocked = useScrollLock(import.meta.client ? document.body : null)
|
|
48
|
+
const { width: windowWidth } = useWindowSize()
|
|
49
|
+
|
|
50
|
+
const isMobile = computed(() => windowWidth.value < 1025)
|
|
51
|
+
const isSheet = computed(() => isMobile.value && props.mobileSheet)
|
|
52
|
+
|
|
53
|
+
const sizeStyle = computed(() => {
|
|
54
|
+
if (isSheet.value) return { width: '100%' }
|
|
55
|
+
if (props.width) return { width: props.width }
|
|
56
|
+
|
|
57
|
+
switch (props.size) {
|
|
58
|
+
case 'sm': return { width: '380px' }
|
|
59
|
+
case 'md': return { width: '520px' }
|
|
60
|
+
case 'lg': return { width: '720px' }
|
|
61
|
+
case 'xl': return { width: '1100px' }
|
|
62
|
+
case 'container': return {}
|
|
63
|
+
case 'fullscreen': return { width: '100vw', height: '100vh' }
|
|
64
|
+
default: return { width: '520px' }
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const blurClass = computed(() => {
|
|
69
|
+
if (!props.blur) return ''
|
|
70
|
+
|
|
71
|
+
switch (props.blur) {
|
|
72
|
+
case 'sm': return 'backdrop-blur-sm'
|
|
73
|
+
case 'md': return 'backdrop-blur-md'
|
|
74
|
+
case 'lg': return 'backdrop-blur-lg'
|
|
75
|
+
case 'xl': return 'backdrop-blur-xl'
|
|
76
|
+
default: return 'backdrop-blur'
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const overlayClass = computed(() => {
|
|
81
|
+
const base = ['modal-v2-overlay fixed inset-0 z-[10000] flex bg-black/40 dark:bg-black/60']
|
|
82
|
+
|
|
83
|
+
if (isSheet.value) {
|
|
84
|
+
base.push('items-end justify-stretch')
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
base.push(props.size === 'container' ? 'items-center justify-center' : 'items-center justify-center p-4')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (blurClass.value) base.push(blurClass.value)
|
|
91
|
+
|
|
92
|
+
return base.join(' ')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const contentBaseClass = computed(() => {
|
|
96
|
+
const base = ['modal-v2-content flex flex-col overflow-hidden border bg-white shadow-2xl dark:bg-gray-900']
|
|
97
|
+
|
|
98
|
+
if (isSheet.value) {
|
|
99
|
+
base.push('w-full max-w-none max-h-[100dvh] rounded-t-2xl rounded-b-none border-x-0 border-b-0 border-t border-gray-200 dark:border-gray-800')
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const radius = props.size === 'fullscreen' ? 'rounded-none' : 'rounded-2xl'
|
|
103
|
+
const widthCls = props.size === 'container'
|
|
104
|
+
? ' w-full max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg xl:max-w-screen-xl 2xl:max-w-screen-2xl'
|
|
105
|
+
: ''
|
|
106
|
+
base.push(`max-h-[calc(100vh-2rem)] border-gray-200 dark:border-gray-800 ${radius}${widthCls}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return base.join(' ')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const overlayTransitionName = computed(() => {
|
|
113
|
+
if (swipeClosing.value) return ''
|
|
114
|
+
return props.transition ? 'modal-v2-overlay' : ''
|
|
115
|
+
})
|
|
116
|
+
const contentTransitionName = computed(() => {
|
|
117
|
+
if (swipeClosing.value || !props.transition) return ''
|
|
118
|
+
return isSheet.value ? 'modal-v2-sheet' : 'modal-v2-content'
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
function closeIfAllowed() {
|
|
122
|
+
if (props.persistent) return
|
|
123
|
+
open.value = false
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function onOverlayClick(event: MouseEvent) {
|
|
127
|
+
if (!props.closeOnOverlay || props.persistent) return
|
|
128
|
+
if (event.target === event.currentTarget) {
|
|
129
|
+
open.value = false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onKeyStroke('Escape', () => {
|
|
134
|
+
if (!open.value) return
|
|
135
|
+
if (!props.closeOnEsc || props.persistent) return
|
|
136
|
+
open.value = false
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
function setScrollbarCompensation(active: boolean) {
|
|
140
|
+
if (!import.meta.client) return
|
|
141
|
+
|
|
142
|
+
if (active) {
|
|
143
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
|
|
144
|
+
document.body.style.paddingRight = scrollbarWidth > 0 ? `${scrollbarWidth}px` : ''
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
document.body.style.paddingRight = ''
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
watch(open, (value) => {
|
|
152
|
+
if (props.lockScroll) {
|
|
153
|
+
if (value) setScrollbarCompensation(true)
|
|
154
|
+
bodyScrollLocked.value = value
|
|
155
|
+
if (!value) setScrollbarCompensation(false)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (value) emit('open')
|
|
159
|
+
else emit('close')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
onBeforeUnmount(() => {
|
|
163
|
+
if (bodyScrollLocked.value) {
|
|
164
|
+
bodyScrollLocked.value = false
|
|
165
|
+
}
|
|
166
|
+
setScrollbarCompensation(false)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const dragging = ref(false)
|
|
170
|
+
const dragStartY = ref(0)
|
|
171
|
+
const dragDelta = ref(0)
|
|
172
|
+
|
|
173
|
+
function canSwipe() {
|
|
174
|
+
return isSheet.value && props.swipeToClose && !props.persistent
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setTransform(value: string) {
|
|
178
|
+
if (contentRef.value) {
|
|
179
|
+
contentRef.value.style.transform = value
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function onTouchStart(event: TouchEvent) {
|
|
184
|
+
if (!canSwipe()) return
|
|
185
|
+
dragging.value = true
|
|
186
|
+
dragStartY.value = event.touches[0].clientY
|
|
187
|
+
dragDelta.value = 0
|
|
188
|
+
|
|
189
|
+
if (contentRef.value) {
|
|
190
|
+
contentRef.value.style.transition = 'none'
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function onTouchMove(event: TouchEvent) {
|
|
195
|
+
if (!dragging.value || !canSwipe()) return
|
|
196
|
+
|
|
197
|
+
const delta = event.touches[0].clientY - dragStartY.value
|
|
198
|
+
if (delta < 0) {
|
|
199
|
+
dragDelta.value = 0
|
|
200
|
+
setTransform('')
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
dragDelta.value = delta
|
|
205
|
+
setTransform(`translateY(${delta}px)`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function prefersReducedMotion() {
|
|
209
|
+
return import.meta.client && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function animateClose() {
|
|
213
|
+
const content = contentRef.value
|
|
214
|
+
|
|
215
|
+
if (!content || prefersReducedMotion()) {
|
|
216
|
+
open.value = false
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
swipeClosing.value = true
|
|
221
|
+
|
|
222
|
+
let done = false
|
|
223
|
+
const finish = () => {
|
|
224
|
+
if (done) return
|
|
225
|
+
done = true
|
|
226
|
+
content.removeEventListener('transitionend', onTransitionEnd)
|
|
227
|
+
open.value = false
|
|
228
|
+
swipeClosing.value = false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function onTransitionEnd(event: TransitionEvent) {
|
|
232
|
+
if (event.target === content && event.propertyName === 'transform') {
|
|
233
|
+
finish()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
content.addEventListener('transitionend', onTransitionEnd)
|
|
238
|
+
setTimeout(finish, 320)
|
|
239
|
+
|
|
240
|
+
requestAnimationFrame(() => {
|
|
241
|
+
content.style.transition = 'transform 260ms cubic-bezier(0.22, 1, 0.36, 1)'
|
|
242
|
+
content.style.transform = 'translateY(100%)'
|
|
243
|
+
|
|
244
|
+
if (overlayRef.value) {
|
|
245
|
+
overlayRef.value.style.transition = 'opacity 260ms ease'
|
|
246
|
+
overlayRef.value.style.opacity = '0'
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function onTouchEnd() {
|
|
252
|
+
if (!dragging.value) return
|
|
253
|
+
dragging.value = false
|
|
254
|
+
|
|
255
|
+
const shouldClose = dragDelta.value > props.swipeThreshold && canSwipe()
|
|
256
|
+
|
|
257
|
+
if (shouldClose) {
|
|
258
|
+
animateClose()
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
if (contentRef.value) {
|
|
262
|
+
contentRef.value.style.transition = 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)'
|
|
263
|
+
contentRef.value.style.transform = ''
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
dragDelta.value = 0
|
|
268
|
+
}
|
|
269
|
+
</script>
|
|
270
|
+
|
|
271
|
+
<template>
|
|
272
|
+
<client-only>
|
|
273
|
+
<teleport to="body">
|
|
274
|
+
<transition :name="overlayTransitionName">
|
|
275
|
+
<div
|
|
276
|
+
v-if="open"
|
|
277
|
+
ref="overlayRef"
|
|
278
|
+
:class="overlayClass"
|
|
279
|
+
@mousedown="onOverlayClick"
|
|
280
|
+
>
|
|
281
|
+
<transition :name="contentTransitionName" appear>
|
|
282
|
+
<div
|
|
283
|
+
v-if="open"
|
|
284
|
+
ref="contentRef"
|
|
285
|
+
:class="contentBaseClass"
|
|
286
|
+
:style="sizeStyle"
|
|
287
|
+
@mousedown.stop
|
|
288
|
+
>
|
|
289
|
+
<div
|
|
290
|
+
v-if="isSheet && showDragHandle"
|
|
291
|
+
class="flex shrink-0 cursor-grab touch-none justify-center py-2"
|
|
292
|
+
@touchstart.passive="onTouchStart"
|
|
293
|
+
@touchmove="onTouchMove"
|
|
294
|
+
@touchend="onTouchEnd"
|
|
295
|
+
@touchcancel="onTouchEnd"
|
|
296
|
+
>
|
|
297
|
+
<div class="h-1 w-10 rounded-full bg-gray-300 dark:bg-gray-600" />
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<header
|
|
301
|
+
v-if="!hideHeader && (title || $slots.header)"
|
|
302
|
+
class="flex shrink-0 items-center justify-between gap-3 border-b border-gray-200 px-5 py-4 dark:border-gray-800"
|
|
303
|
+
@touchstart.passive="onTouchStart"
|
|
304
|
+
@touchmove="onTouchMove"
|
|
305
|
+
@touchend="onTouchEnd"
|
|
306
|
+
@touchcancel="onTouchEnd"
|
|
307
|
+
>
|
|
308
|
+
<slot name="header">
|
|
309
|
+
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
|
310
|
+
{{ title }}
|
|
311
|
+
</h2>
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
class="rounded-md p-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
|
315
|
+
@click="closeIfAllowed"
|
|
316
|
+
>
|
|
317
|
+
<a-icon-x-mark class="size-4" />
|
|
318
|
+
</button>
|
|
319
|
+
</slot>
|
|
320
|
+
</header>
|
|
321
|
+
|
|
322
|
+
<div class="min-h-0 flex-1 overflow-auto" :class="contentClass">
|
|
323
|
+
<slot :close="closeIfAllowed" />
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<footer
|
|
327
|
+
v-if="$slots.footer"
|
|
328
|
+
class="shrink-0 border-t border-gray-200 px-5 py-4 dark:border-gray-800"
|
|
329
|
+
>
|
|
330
|
+
<slot name="footer" :close="closeIfAllowed" />
|
|
331
|
+
</footer>
|
|
332
|
+
</div>
|
|
333
|
+
</transition>
|
|
334
|
+
</div>
|
|
335
|
+
</transition>
|
|
336
|
+
</teleport>
|
|
337
|
+
</client-only>
|
|
338
|
+
</template>
|
|
339
|
+
|
|
340
|
+
<style scoped lang="scss">
|
|
341
|
+
.modal-v2-overlay-enter-active {
|
|
342
|
+
transition: opacity 200ms ease-out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.modal-v2-overlay-leave-active {
|
|
346
|
+
transition: opacity 150ms ease-in;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.modal-v2-overlay-enter-from,
|
|
350
|
+
.modal-v2-overlay-leave-to {
|
|
351
|
+
opacity: 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.modal-v2-content-enter-active {
|
|
355
|
+
transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease-out;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.modal-v2-content-leave-active {
|
|
359
|
+
transition: transform 150ms ease-in, opacity 150ms ease-in;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.modal-v2-content-enter-from,
|
|
363
|
+
.modal-v2-content-leave-to {
|
|
364
|
+
opacity: 0;
|
|
365
|
+
transform: translateY(8px) scale(0.98);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.modal-v2-sheet-enter-active,
|
|
369
|
+
.modal-v2-sheet-leave-active {
|
|
370
|
+
transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.modal-v2-sheet-enter-from,
|
|
374
|
+
.modal-v2-sheet-leave-to {
|
|
375
|
+
transform: translateY(100%);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
@media (prefers-reduced-motion: reduce) {
|
|
379
|
+
.modal-v2-overlay-enter-active,
|
|
380
|
+
.modal-v2-overlay-leave-active,
|
|
381
|
+
.modal-v2-content-enter-active,
|
|
382
|
+
.modal-v2-content-leave-active,
|
|
383
|
+
.modal-v2-sheet-enter-active,
|
|
384
|
+
.modal-v2-sheet-leave-active {
|
|
385
|
+
transition: none;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
</style>
|