adata-ui 2.1.40-beta.1 → 2.1.40-beta.10

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 (48) hide show
  1. package/components/elements/a-select-row/ASelectRowV2.vue +214 -0
  2. package/components/elements/button/AButtonV2.vue +112 -0
  3. package/components/elements/leave-note/ALeaveNote.vue +38 -17
  4. package/components/elements/segmented/ASegmentedV2.vue +58 -0
  5. package/components/elements/select/ASelectV2.vue +589 -0
  6. package/components/elements/show-more/AShowMoreV2.vue +26 -0
  7. package/components/elements/star-rating/AStarRating.vue +12 -4
  8. package/components/elements/status-badge-v2/StatusBadgeV2.vue +78 -0
  9. package/components/forms/checkbox/ACheckboxV2.vue +229 -0
  10. package/components/forms/feedback-form/FeedbackForm.vue +8 -8
  11. package/components/forms/input/AInputV2.vue +542 -0
  12. package/components/forms/input/textarea/ATextareaV2.vue +248 -0
  13. package/components/forms/toggle/AToggleV2.vue +71 -0
  14. package/components/modals/AnotherDeviceModal.vue +22 -18
  15. package/components/modals/ApplicationAcceptedModal.vue +12 -15
  16. package/components/modals/ConnectingTariffModal.vue +13 -20
  17. package/components/modals/Insufficient-funds-modal.vue +18 -33
  18. package/components/modals/LimitReachedModal.vue +4 -3
  19. package/components/modals/NoAccessModal.vue +9 -11
  20. package/components/modals/PaymentMethodModal.vue +1 -1
  21. package/components/modals/ReportBugConfirmModal.vue +12 -10
  22. package/components/modals/ReportBugModal.vue +42 -36
  23. package/components/modals/Resend.vue +9 -10
  24. package/components/modals/SubmitApplicationModal.vue +27 -24
  25. package/components/modals/id/IdConfirmAccountOtpModal.vue +4 -9
  26. package/components/modals/id/IdLoginModal.vue +3 -8
  27. package/components/modals/id/IdModals.vue +23 -10
  28. package/components/modals/id/IdTwoFactorModal.vue +3 -8
  29. package/components/navigation/header/AHeader.vue +5 -5
  30. package/components/navigation/header/HeaderLink.vue +15 -21
  31. package/components/navigation/header/NavList.vue +8 -32
  32. package/components/navigation/header/ProductMenu.vue +52 -50
  33. package/components/navigation/pill-tabs/APillTabsV2.vue +118 -0
  34. package/components/overlays/modal/AModalV2.vue +388 -0
  35. package/composables/useAccessTokenCookie.ts +25 -0
  36. package/composables/useChipOverflow.ts +92 -0
  37. package/composables/useHeaderNavigationLinks.ts +53 -23
  38. package/icons/arrow/arrow-right.vue +14 -0
  39. package/icons/briefcase.vue +14 -0
  40. package/icons/car-side.vue +16 -0
  41. package/icons/cart.vue +15 -0
  42. package/icons/download-rounded.vue +15 -0
  43. package/icons/shield-verified.vue +14 -0
  44. package/lang/en.ts +34 -4
  45. package/lang/kk.ts +35 -5
  46. package/lang/ru.ts +36 -6
  47. package/package.json +1 -1
  48. package/shared/constans/pages.ts +8 -2
@@ -3,28 +3,30 @@ const { t } = useI18n()
3
3
 
4
4
  const isOpen = defineModel<boolean>({ required: true })
5
5
  </script>
6
+
6
7
  <template>
7
- <a-modal
8
+ <a-modal-v2
8
9
  v-model="isOpen"
9
10
  width="424px"
10
11
  >
11
- <template #header>
12
+ <div class="flex flex-col items-center justify-center gap-4 px-6 py-4">
13
+ <a-ill-ok />
12
14
  <p class="heading-02 text-center font-bold">
13
15
  {{ t('modals.report_bug.confirmed') }}
14
16
  </p>
15
- </template>
16
- <div class="flex flex-col gap-5 items-center">
17
- <a-ill-ok />
18
- <p class="text-sm text-center">
17
+ <p class="body-400 text-center">
19
18
  {{ t('modals.report_bug.gratitude') }}
20
19
  </p>
21
- <a-button
20
+ </div>
21
+ <template #footer>
22
+ <a-button-v2
22
23
  view="outline"
24
+ color="blue"
23
25
  block
24
26
  @click="isOpen = false"
25
27
  >
26
28
  {{ t('actions.close') }}
27
- </a-button>
28
- </div>
29
- </a-modal>
29
+ </a-button-v2>
30
+ </template>
31
+ </a-modal-v2>
30
32
  </template>
@@ -18,7 +18,7 @@ const env = myLayer.mode
18
18
 
19
19
  const mask = new Mask({ mask: '# (###) ### ## ##' })
20
20
 
21
- const isOpen = defineModel<boolean>()
21
+ const isOpen = defineModel<boolean>({ default: false })
22
22
  const form: any = reactive({
23
23
  name: '',
24
24
  email: '',
@@ -35,6 +35,7 @@ const scheme = z.object({
35
35
  })
36
36
 
37
37
  const submitted = ref(false)
38
+ const sending = ref(false)
38
39
 
39
40
  const { validation, getError } = useUIValidation(
40
41
  scheme,
@@ -58,6 +59,7 @@ watch(phone, () => {
58
59
  async function send() {
59
60
  submitted.value = true
60
61
  if (validation.value) return
62
+ sending.value = true
61
63
  try {
62
64
  await $fetch<any>(`https://users.${env}.kz/api/v1/message/error`, {
63
65
  method: 'POST',
@@ -79,86 +81,90 @@ async function send() {
79
81
  form.comment = ''
80
82
  phone.value = ''
81
83
  submitted.value = false
84
+ sending.value = false
82
85
  emit('close')
83
86
  }
84
87
  }
85
88
  </script>
86
89
 
87
90
  <template>
88
- <a-modal v-model="isOpen" data-test-id="report-form-modal">
91
+ <a-modal-v2 v-model="isOpen" width="424px">
89
92
  <template #header>
90
- <p class="heading-02 text-center" data-test-id="report-form-title">
93
+ <h2 class="text-base font-semibold text-gray-900 dark:text-gray-100" data-test-id="report-form-title">
91
94
  {{ t('modals.report_bug.title') }}
92
- </p>
95
+ </h2>
93
96
  </template>
94
- <div class="flex flex-col gap-5">
95
- <div class="flex flex-col items-center justify-center gap-5">
96
- <a-ill-stop-sign class="size-[185px]" />
97
+
98
+ <div class="flex flex-col gap-5 px-6 py-4">
99
+ <div class="flex flex-col items-center justify-center gap-4">
100
+ <a-ill-stop-sign class="size-[140px]" />
97
101
  <slot name="title">
98
102
  <p class="label-400 text-center">
99
103
  {{ name }}
100
104
  </p>
101
105
  </slot>
102
106
  </div>
103
- <div class="flex flex-col gap-2">
104
- <a-input-standard
107
+
108
+ <div class="flex flex-col gap-3">
109
+ <a-input-v2
105
110
  v-model="form.name"
106
111
  required
107
- :data-test-id="'report-form-name-input'"
108
112
  :label="t('modals.report_bug.labels.name')"
109
113
  :error="getError('name')"
114
+ data-test-id="report-form-name-input"
110
115
  />
111
- <a-input-standard
116
+ <a-input-v2
112
117
  v-model="form.email"
113
118
  required
114
- :data-test-id="'report-form-email-input'"
119
+ type="email"
115
120
  :label="t('modals.report_bug.labels.email')"
116
121
  :error="getError('email')"
122
+ data-test-id="report-form-email-input"
117
123
  />
118
- <a-input-standard
124
+ <a-input-v2
119
125
  v-model="phone"
120
- v-maska
121
126
  required
122
- data-maska="8 (###) ### ## ##"
127
+ type="tel"
123
128
  clearable
124
- :data-test-id="'report-form-phone-input'"
129
+ mask="8 (###) ### ## ##"
125
130
  :label="t('modals.report_bug.labels.phone')"
126
131
  :error="getError('phone_number')"
132
+ data-test-id="report-form-phone-input"
133
+ />
134
+ <a-textarea-v2
135
+ v-model="form.comment"
136
+ required
137
+ :rows="4"
138
+ resize="none"
139
+ :label="t('modals.report_bug.labels.comment')"
140
+ :error="getError('comment')"
141
+ data-test-id="report-form-comment-textarea"
127
142
  />
128
- <div>
129
- <a-textarea
130
- v-model="form.comment"
131
- required
132
- :data-test-id="'report-form-comment-textarea'"
133
- :label="t('modals.report_bug.labels.comment')"
134
- size="md"
135
- :error="getError('comment')"
136
- class="min-h-[120px]"
137
- :resizeable="false"
138
- />
139
- </div>
140
143
  </div>
141
144
  </div>
145
+
142
146
  <template #footer>
143
- <div class="flex w-full gap-2">
144
- <a-button
147
+ <div class="flex gap-2">
148
+ <a-button-v2
145
149
  view="outline"
146
- class="w-full"
150
+ color="blue"
151
+ block
147
152
  data-test-id="report-form-close-button"
148
153
  @click="emit('close')"
149
154
  >
150
155
  {{ t('actions.close') }}
151
- </a-button>
152
- <a-button
153
- class="w-full"
156
+ </a-button-v2>
157
+ <a-button-v2
158
+ block
159
+ :loading="sending"
154
160
  data-test-id="report-form-submit-button"
155
161
  @click="send"
156
162
  >
157
163
  {{ t('actions.send') }}
158
- </a-button>
164
+ </a-button-v2>
159
165
  </div>
160
166
  </template>
161
- </a-modal>
167
+ </a-modal-v2>
162
168
  </template>
163
169
 
164
170
  <style scoped></style>
@@ -1,5 +1,4 @@
1
1
  <script setup lang="ts">
2
-
3
2
  import { useUrls } from '#adata-ui/composables/useUrls'
4
3
 
5
4
  const emit = defineEmits<{
@@ -17,33 +16,33 @@ function runTimer() {
17
16
  const intervalId = setInterval(() => {
18
17
  if (timer.value <= 0) {
19
18
  isResend.value = true
20
- clearInterval(intervalId);
21
- return;
19
+ clearInterval(intervalId)
20
+ return
22
21
  }
23
- timer.value--;
24
- }, 1000);
22
+ timer.value--
23
+ }, 1000)
25
24
  }
26
25
 
27
26
  onMounted(() => resend())
28
27
 
29
- const resend = () => {
28
+ function resend() {
30
29
  isResend.value = false
31
30
  runTimer()
32
31
  emit('resend')
33
32
  }
34
- const goToMain = () => {
33
+ function goToMain() {
35
34
  if (route.query.url) {
36
35
  document.location.replace(route.query.url)
37
- } else {
36
+ }
37
+ else {
38
38
  document.location.replace(landing)
39
39
  }
40
40
  emit('close')
41
-
42
41
  }
43
42
  </script>
44
43
 
45
44
  <template>
46
- <div class="flex flex-col justify-center items-center gap-5">
45
+ <div class="flex flex-col items-center justify-center gap-5">
47
46
  <p class="heading-02">
48
47
  {{ $t('login.modal.title') }}
49
48
  </p>
@@ -57,65 +57,68 @@ onMounted(() => {
57
57
  </script>
58
58
 
59
59
  <template>
60
- <a-modal
60
+ <a-modal-v2
61
61
  v-model="isOpen"
62
- name="submit-application"
63
- :prevent-close="true"
62
+ width="424px"
63
+ persistent
64
64
  >
65
- <div class="flex flex-col gap-5">
66
- <p class="text-center heading-02">
65
+ <div class="flex flex-col gap-5 px-6 py-4">
66
+ <p class="heading-02 text-center">
67
67
  {{ t('modals.submit_application_modal.title') }}
68
68
  </p>
69
69
  <a-ill-mail class="w-full" />
70
- <div class="flex flex-col gap-2">
71
- <a-input-standard
70
+ <div class="flex flex-col gap-3">
71
+ <a-input-v2
72
72
  v-model="form.name"
73
+ required
73
74
  :label="t('modals.submit_application_modal.labels.name')"
74
75
  :error="getError('name')"
75
- required
76
76
  />
77
- <a-input-standard
77
+ <a-input-v2
78
78
  v-model="form.email"
79
+ required
80
+ type="email"
79
81
  :label="t('modals.submit_application_modal.labels.email')"
80
82
  :error="getError('email')"
81
- required
82
83
  />
83
- <a-input-standard
84
+ <a-input-v2
84
85
  v-model="form.phone"
85
- v-maska
86
+ required
87
+ type="tel"
88
+ clearable
89
+ mask="8 (###) ###-##-##"
86
90
  :label="t('modals.submit_application_modal.labels.phone')"
87
- data-maska="8 (###) ###-##-##"
88
91
  :error="getError('phone')"
89
- clearable
90
- required
91
92
  />
92
- <a-textarea
93
+ <a-textarea-v2
93
94
  v-model="form.comment"
95
+ required
96
+ :rows="4"
97
+ resize="none"
94
98
  :label="t('modals.submit_application_modal.labels.comment')"
95
99
  :error="getError('comment')"
96
- resizeable="none"
97
- required
98
100
  />
99
101
  </div>
100
102
  </div>
101
103
  <template #footer>
102
- <div class="flex gap-2 ">
103
- <a-button
104
+ <div class="flex gap-2">
105
+ <a-button-v2
104
106
  view="outline"
107
+ color="blue"
105
108
  block
106
109
  @click="cancel"
107
110
  >
108
111
  {{ t('reuse.close') }}
109
- </a-button>
110
- <a-button
112
+ </a-button-v2>
113
+ <a-button-v2
111
114
  block
112
115
  @click="send"
113
116
  >
114
117
  {{ t('reuse.send') }}
115
- </a-button>
118
+ </a-button-v2>
116
119
  </div>
117
120
  </template>
118
- </a-modal>
121
+ </a-modal-v2>
119
122
  </template>
120
123
 
121
124
  <style scoped>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
3
2
  import IdOtpInput from '#adata-ui/components/modals/id/IdOtpInput.vue'
3
+ import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
4
+ import { writeAccessTokenCookie } from '#adata-ui/composables/useAccessTokenCookie'
4
5
 
5
6
  const { $toast } = useNuxtApp()
6
7
  const { t, locale } = useI18n()
@@ -40,17 +41,11 @@ async function loginUser() {
40
41
  },
41
42
  })
42
43
  const { data: cookiesData } = await response.json()
43
- if (cookiesData) {
44
+ if (cookiesData?.access_token) {
44
45
  const { access_token, expire_in } = cookiesData
45
- const hostname = location.hostname.split('.').reverse()
46
46
  useCookie('autoLogout').value = true
47
47
 
48
- useCookie('accessToken', {
49
- maxAge: expire_in,
50
- domain: `.${hostname[1]}.${hostname[0]}`,
51
- path: '/',
52
- secure: true,
53
- }).value = access_token
48
+ await writeAccessTokenCookie(access_token, expire_in)
54
49
  }
55
50
  }
56
51
 
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
3
+ import { writeAccessTokenCookie } from '#adata-ui/composables/useAccessTokenCookie'
3
4
  import { navigateToLocalizedPage } from '#adata-ui/utils/localizedNavigation'
4
5
  import * as z from 'zod'
5
6
  import { useUrls } from '#adata-ui/composables/useUrls'
@@ -127,17 +128,11 @@ async function submit() {
127
128
  },
128
129
  })
129
130
  const { data: cookiesData } = await response.json()
130
- if (cookiesData) {
131
+ if (cookiesData?.access_token) {
131
132
  const { access_token, expire_in } = cookiesData
132
- const hostname = location.hostname.split('.').reverse()
133
133
  useCookie('autoLogout').value = true
134
134
 
135
- useCookie('accessToken', {
136
- maxAge: expire_in,
137
- domain: COOKIE_DOMAIN,
138
- path: '/',
139
- secure: true,
140
- }).value = access_token
135
+ await writeAccessTokenCookie(access_token, expire_in)
141
136
  }
142
137
  $toast.success(t('reuse.successfully'))
143
138
 
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
2
+ import IdAutoLogoutModal from '#adata-ui/components/modals/id/IdAutoLogoutModal.vue'
3
3
  import IdConfirmAccountOtpModal from '#adata-ui/components/modals/id/IdConfirmAccountOtpModal.vue'
4
4
  import IdConfirmSuccessfulModal from '#adata-ui/components/modals/id/IdConfirmSuccessfulModal.vue'
5
5
  import IdLoginModal from '#adata-ui/components/modals/id/IdLoginModal.vue'
@@ -9,7 +9,8 @@ import IdRecoveryModal from '#adata-ui/components/modals/id/IdRecoveryModal.vue'
9
9
  import IdRegistrationModal from '#adata-ui/components/modals/id/IdRegistrationModal.vue'
10
10
  import IdResetPasswordOtpModal from '#adata-ui/components/modals/id/IdResetPasswordOtpModal.vue'
11
11
  import IdTwoFactorModal from '#adata-ui/components/modals/id/IdTwoFactorModal.vue'
12
- import IdAutoLogoutModal from '#adata-ui/components/modals/id/IdAutoLogoutModal.vue'
12
+ import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
13
+ import { writeAccessTokenCookie } from '#adata-ui/composables/useAccessTokenCookie'
13
14
 
14
15
  const emit = defineEmits<{
15
16
  (e: 'closed', value: boolean): void
@@ -35,6 +36,10 @@ const authApiURL = commonAuth.authApiURL
35
36
  const accessToken = useCookie('accessToken')
36
37
  const isAuthenticated = computed(() => !!accessToken.value)
37
38
 
39
+ // one-shot флаг: обмен по auth-сессии перезагружает страницу максимум один раз,
40
+ // иначе нечитаемая кука accessToken зацикливает reload
41
+ const AUTH_RELOAD_FLAG = 'auth-exchange-reloaded'
42
+
38
43
  onMounted(async () => {
39
44
  const response = await fetch(`${removeTrailingSlash(authApiURL)}/access/cookie`, {
40
45
  method: 'GET',
@@ -46,17 +51,25 @@ onMounted(async () => {
46
51
  const { data: cookiesData } = await response.json()
47
52
  if (cookiesData?.access_token && !accessToken.value) {
48
53
  const { access_token, expire_in } = cookiesData
49
- const hostname = location.hostname.split('.').reverse()
50
54
  useCookie('autoLogout').value = true
51
55
 
52
- useCookie('accessToken', {
53
- maxAge: expire_in,
54
- domain: `.${hostname[1]}.${hostname[0]}`,
55
- path: '/',
56
- secure: true,
57
- }).value = access_token
56
+ const persisted = await writeAccessTokenCookie(access_token, expire_in)
58
57
 
59
- window.location.reload()
58
+ if (persisted && !sessionStorage.getItem(AUTH_RELOAD_FLAG)) {
59
+ sessionStorage.setItem(AUTH_RELOAD_FLAG, '1')
60
+ window.location.reload()
61
+ }
62
+ else {
63
+ sessionStorage.removeItem(AUTH_RELOAD_FLAG)
64
+ console.warn(
65
+ persisted
66
+ ? '[auth] accessToken cookie is erased between page loads (SSR resetTokens?), reload skipped to avoid a loop'
67
+ : '[auth] accessToken cookie was not persisted (domain/secure/maxAge rejected by browser), reload skipped',
68
+ )
69
+ }
70
+ }
71
+ else {
72
+ sessionStorage.removeItem(AUTH_RELOAD_FLAG)
60
73
  }
61
74
  })
62
75
 
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
3
2
  import IdOtpInput from '#adata-ui/components/modals/id/IdOtpInput.vue'
3
+ import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
4
+ import { writeAccessTokenCookie } from '#adata-ui/composables/useAccessTokenCookie'
4
5
  import { useUrls } from '#adata-ui/composables/useUrls'
5
6
 
6
7
  const { $toast } = useNuxtApp()
@@ -54,15 +55,9 @@ async function onConfirm() {
54
55
  const { data: cookiesData } = await response.json()
55
56
  if (cookiesData?.access_token) {
56
57
  const { access_token, expire_in } = cookiesData
57
- const hostname = location.hostname.split('.').reverse()
58
58
  useCookie('autoLogout').value = true
59
59
 
60
- useCookie('accessToken', {
61
- maxAge: expire_in,
62
- domain: `.${hostname[1]}.${hostname[0]}`,
63
- path: '/',
64
- secure: true,
65
- }).value = access_token
60
+ await writeAccessTokenCookie(access_token, expire_in)
66
61
  }
67
62
  $toast.success(t('login.successfully'))
68
63
  twoFactorModal.value = false
@@ -109,7 +109,7 @@ onMounted(() => {
109
109
  <system-notification />
110
110
  <header
111
111
  ref="headerRef"
112
- class="relative h-16 border-b border-deepblue-900/10 bg-gray-50/30 backdrop-blur-lg dark:border-gray-200/10 dark:bg-gray-900/30"
112
+ class="relative h-16 border-b border-deepblue-900/10 bg-gray-50/20 backdrop-blur-lg dark:border-gray-200/10 dark:bg-gray-900/30"
113
113
  >
114
114
  <div class="a-container mobile-padding flex h-full items-center justify-between gap-2">
115
115
  <!-- Desktop hidden -->
@@ -135,18 +135,18 @@ onMounted(() => {
135
135
  </div>
136
136
  <div
137
137
  v-else-if="mobileHeaderType === 'search'"
138
- class="flex w-full items-center gap-2 bg-white dark:bg-gray-900 lg:hidden"
138
+ class="flex w-full items-center gap-2 lg:hidden"
139
139
  >
140
140
  <logo
141
141
  class="dark:text-gray-200"
142
142
  @click="goToAnotherModule"
143
143
  />
144
144
  <button
145
- class="flex w-full items-center gap-2 rounded bg-gray-50 px-4 py-1.5 dark:bg-gray-800 dark:text-gray-500"
145
+ class="flex w-full items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-1.5 shadow-sm dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400"
146
146
  @click="$emit('search')"
147
147
  >
148
- <a-icon-search />
149
- <span class="body-400">Найти</span>
148
+ <a-icon-search class="text-gray-400 dark:text-gray-500" />
149
+ <span class="body-400 text-gray-500 dark:text-gray-400">Найти</span>
150
150
  </button>
151
151
  </div>
152
152
  </section>
@@ -22,18 +22,18 @@ interface HeaderNav {
22
22
 
23
23
  const navs: HeaderNav[] = [
24
24
  { label: 'header.navs.products', dataAttribute: 'header-products-and-solutions-button', menu: ProductMenu },
25
- { label: 'header.navs.contacts', dataAttribute: 'header-contacts-button', menu: ContactMenu },
25
+ { label: 'header.navs.contacts', dataAttribute: 'header-contacts-button' },
26
26
  { label: 'header.navs.tariffs', dataAttribute: 'header-tariffs-button' },
27
27
  ]
28
28
 
29
- const OPEN_DELAY = 500 // hover dwell before opening, so a pass-through doesn't trigger it
30
- const SWITCH_DELAY = 80 // snappier when moving between items with the menu already open
29
+ const OPEN_DELAY = 500
30
+ const SWITCH_DELAY = 80
31
31
 
32
32
  const menu = ref<HTMLElement | null>(null)
33
33
  const show = ref(false)
34
- const locked = ref(false) // a click pins the menu; hover can no longer close it
34
+ const locked = ref(false)
35
35
  const currentIndex = ref(0)
36
- const swapName = ref('swap-fade') // panel transition: directional slide on switch, fade on fresh open
36
+ const swapName = ref('swap-fade')
37
37
  const hoverTimer = ref<ReturnType<typeof setTimeout> | null>(null)
38
38
 
39
39
  const currentMenu = computed(() => navs[currentIndex.value]?.menu ?? null)
@@ -42,8 +42,6 @@ function isActive(index: number) {
42
42
  return show.value && currentIndex.value === index
43
43
  }
44
44
 
45
- // Open `index`'s panel. Slide toward the direction of travel when switching
46
- // between already-open panels; fade (no sideways shift) on a fresh open.
47
45
  function openMenu(index: number) {
48
46
  swapName.value = show.value && index !== currentIndex.value
49
47
  ? (index > currentIndex.value ? 'slide-next' : 'slide-prev')
@@ -57,6 +55,10 @@ function closeMenu() {
57
55
  locked.value = false
58
56
  }
59
57
 
58
+ function goToContacts() {
59
+ navigateToLocalizedPage({ locale, projectUrl: landing, path: PAGES.contacts, target: '_blank' })
60
+ }
61
+
60
62
  function goToTariffs() {
61
63
  const host = useRequestURL().hostname
62
64
  const codeMap: Record<string, string> = {
@@ -73,11 +75,15 @@ function goToTariffs() {
73
75
  navigateToLocalizedPage({ locale, projectUrl: landing, path: PAGES.tariffs + code, target: '_blank' })
74
76
  }
75
77
 
76
- // Click pins the menu open; items without a menu (Tariffs) navigate instead.
77
78
  function toggleMenu(index: number) {
78
79
  const nav = navs[index]
79
80
  if (!nav?.menu) {
80
- goToTariffs()
81
+ if (index === 1) {
82
+ goToContacts()
83
+ }
84
+ else if (index === 2) {
85
+ goToTariffs()
86
+ }
81
87
  return
82
88
  }
83
89
  if (show.value && currentIndex.value === index) {
@@ -95,7 +101,6 @@ function clearHoverTimer() {
95
101
  }
96
102
  }
97
103
 
98
- // Hover opens menu items; hovering Tariffs dismisses an unpinned menu.
99
104
  function scheduleOpen(index: number) {
100
105
  clearHoverTimer()
101
106
  hoverTimer.value = setTimeout(() => {
@@ -104,14 +109,10 @@ function scheduleOpen(index: number) {
104
109
  }, show.value ? SWITCH_DELAY : OPEN_DELAY)
105
110
  }
106
111
 
107
- // Overlay closes on click regardless of the pin; on hover only while unpinned.
108
112
  function onOverlayHover() {
109
113
  if (!locked.value) closeMenu()
110
114
  }
111
115
 
112
- // Lock body scroll while the mega-menu is open. `scrollbar-gutter: stable` is set
113
- // globally, so we don't reserve the gap to avoid a double-compensated page shift.
114
- // The panel is mounted via v-if, so wait a tick for the ref on open.
115
116
  watch(show, async (open) => {
116
117
  if (open) {
117
118
  await nextTick()
@@ -157,7 +158,6 @@ onBeforeUnmount(() => {
157
158
  </li>
158
159
  </ul>
159
160
 
160
- <!-- One transition drives the whole panel's open/close (appear + disappear). -->
161
161
  <transition name="menu">
162
162
  <div
163
163
  v-if="show"
@@ -165,14 +165,12 @@ onBeforeUnmount(() => {
165
165
  class="fixed inset-x-0 z-20"
166
166
  :style="{ top: `${menuTop}px`, height: `calc(100dvh - ${menuTop}px)` }"
167
167
  >
168
- <!-- Full-page dimmed blur backdrop behind the panel. -->
169
168
  <div
170
169
  class="absolute inset-0 bg-gray-900/30 backdrop-blur dark:bg-black/50"
171
170
  @click="closeMenu"
172
171
  @mouseover="onOverlayHover"
173
172
  />
174
173
 
175
- <!-- Panel on top; slides left/right when switching between menu items. -->
176
174
  <transition
177
175
  :name="swapName"
178
176
  mode="out-in"
@@ -181,7 +179,6 @@ onBeforeUnmount(() => {
181
179
  :is="currentMenu"
182
180
  :key="currentIndex"
183
181
  class="relative"
184
- :url="buildLocalizedUrl(locale, landing, PAGES.contacts)"
185
182
  />
186
183
  </transition>
187
184
  </div>
@@ -193,7 +190,6 @@ onBeforeUnmount(() => {
193
190
  $ease: cubic-bezier(0.22, 1, 0.36, 1);
194
191
  $shift: 24px;
195
192
 
196
- // Whole menu: open and close (vertical fade, no horizontal motion).
197
193
  .menu-enter-active,
198
194
  .menu-leave-active {
199
195
  transition:
@@ -207,7 +203,6 @@ $shift: 24px;
207
203
  transform: translateY(-12px);
208
204
  }
209
205
 
210
- // Switching panels: slide toward the direction of travel (out-in, so no overlap).
211
206
  .slide-next-enter-active,
212
207
  .slide-next-leave-active,
213
208
  .slide-prev-enter-active,
@@ -239,7 +234,6 @@ $shift: 24px;
239
234
  transform: translateX($shift);
240
235
  }
241
236
 
242
- // Fresh open (no switch): fade only, so the panel never slides sideways.
243
237
  .swap-fade-enter-from,
244
238
  .swap-fade-leave-to {
245
239
  opacity: 0;