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.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { PAGES } from '#adata-ui/shared/constans/pages'
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 class="group/section overflow-hidden rounded-xl border border-gray-200 bg-white p-3 shadow-sm transition-colors hover:border-blue-700/40 dark:border-gray-800 dark:bg-gray-950 dark:hover:border-blue-500/40">
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 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">
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 class="size-4 text-gray-500 transition-colors group-hover/section:text-blue-700 dark:group-hover/section:text-blue-500" />
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 class="flex size-7 shrink-0 items-center justify-center rounded-md bg-gray-100 text-gray-600 transition-colors group-hover/item:bg-blue-700/10 group-hover/item:text-blue-700 dark:bg-gray-800 dark:text-gray-300 dark:group-hover/item:text-blue-500">
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 class="truncate text-sm font-semibold normal-case text-gray-600 transition-colors group-hover/item:text-deepblue-900 dark:text-gray-300 dark:group-hover/item:text-gray-100">{{ item.title }}</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 bg-blue-50 p-4 shadow-sm transition-colors hover:border-blue-700/40 hover:bg-blue-100 dark:border-blue-500/20 dark:bg-blue-500/5 dark:hover:border-blue-500/40 dark:hover:bg-blue-500/10"
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 class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-blue-700 text-white shadow-sm">
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
- <a-icon-arrow-side-up class="size-4 text-blue-700 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5 dark:text-blue-500" />
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="false"
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>