adata-ui 2.1.38 → 2.1.40-beta
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/.nuxtrc +1 -1
- package/.playground/app.config.ts +5 -5
- package/.playground/app.vue +102 -0
- package/README.md +75 -75
- package/components/elements/README.md +1 -1
- package/components/elements/button-login/index.vue +6 -10
- package/components/elements/tree-select/ATreeSelect.vue +5 -1
- package/components/elements/tree-select/components/tree-select-nodes.vue +4 -3
- package/components/features/color-mode/AColorMode.client.vue +74 -32
- package/components/features/dropdown/ADropdownV2.vue +141 -0
- package/components/features/lang-switcher/lang-switcher.vue +120 -40
- package/components/features//321/201hange-version/AChangeVersion.vue +1 -1
- package/components/forms/README.md +1 -1
- package/components/navigation/README.md +1 -1
- package/components/navigation/footer/AFooter.vue +1 -1
- package/components/navigation/header/AHeader.vue +56 -33
- package/components/navigation/header/AlmatyContacts.vue +1 -1
- package/components/navigation/header/CardGallery.vue +5 -3
- package/components/navigation/header/ContactMenu.vue +26 -92
- package/components/navigation/header/HeaderLink.vue +189 -215
- package/components/navigation/header/HeaderUsage.vue +125 -0
- package/components/navigation/header/NavList.vue +35 -50
- package/components/navigation/header/ProductMenu.vue +72 -126
- package/components/navigation/header/ProfileMenu.vue +131 -150
- package/components/navigation/header/SystemNotification.vue +110 -0
- package/components/navigation/mobile-navigation/AMobileNavigation.vue +23 -15
- package/components/navigation/pill-tabs/APillTabs.vue +7 -2
- package/components/overlays/README.md +1 -1
- package/components/overlays/tooltip/ATooltipV2.vue +233 -0
- package/components/overlays/tooltip/types.ts +26 -0
- package/components/overlays/tooltip/useTooltipTrigger.ts +101 -0
- package/composables/useHeaderNavigationLinks.ts +15 -8
- package/composables/useUrls.ts +1 -1
- package/icons/gauge.vue +17 -0
- package/icons/sun.vue +13 -3
- package/lang/en.ts +6 -0
- package/lang/kk.ts +6 -0
- package/lang/ru.ts +6 -0
- package/package.json +1 -1
- package/shared/constans/pages.ts +1 -1
- package/components/navigation/header/TopHeader.vue +0 -196
|
@@ -1,286 +1,260 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { PAGES } from '#adata-ui/shared/constans/pages'
|
|
5
|
-
import ProductMenu from '#adata-ui/components/navigation/header/ProductMenu.vue'
|
|
6
|
-
|
|
2
|
+
import type { Component } from 'vue'
|
|
7
3
|
import ContactMenu from '#adata-ui/components/navigation/header/ContactMenu.vue'
|
|
8
|
-
import
|
|
4
|
+
import ProductMenu from '#adata-ui/components/navigation/header/ProductMenu.vue'
|
|
9
5
|
import { useUrls } from '#adata-ui/composables/useUrls'
|
|
6
|
+
import { PAGES } from '#adata-ui/shared/constans/pages'
|
|
7
|
+
import { buildLocalizedUrl, navigateToLocalizedPage } from '#adata-ui/utils/localizedNavigation'
|
|
8
|
+
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
withDefaults(defineProps<{ menuTop?: number }>(), {
|
|
11
|
+
menuTop: 64,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const { landing } = useUrls()
|
|
12
15
|
const { t, locale } = useI18n()
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
label: 'header.navs.tariffs',
|
|
27
|
-
to: landing + PAGES.tariffs,
|
|
28
|
-
data_attribute: 'header-tariffs-button',
|
|
29
|
-
},
|
|
30
|
-
// {
|
|
31
|
-
// label: 'header.navs.api',
|
|
32
|
-
// to: landing + PAGES.api
|
|
33
|
-
// },
|
|
17
|
+
interface HeaderNav {
|
|
18
|
+
label: string
|
|
19
|
+
dataAttribute: string
|
|
20
|
+
menu?: Component
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const navs: HeaderNav[] = [
|
|
24
|
+
{ label: 'header.navs.products', dataAttribute: 'header-products-and-solutions-button', menu: ProductMenu },
|
|
25
|
+
{ label: 'header.navs.contacts', dataAttribute: 'header-contacts-button', menu: ContactMenu },
|
|
26
|
+
{ label: 'header.navs.tariffs', dataAttribute: 'header-tariffs-button' },
|
|
34
27
|
]
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
// navs.push(
|
|
39
|
-
// {
|
|
40
|
-
// label: 'header.navs.services',
|
|
41
|
-
// to: pk + PAGES.services + '/kgd'
|
|
42
|
-
// }
|
|
43
|
-
// )
|
|
44
|
-
// }
|
|
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
|
|
45
31
|
|
|
46
32
|
const menu = ref<HTMLElement | null>(null)
|
|
47
|
-
const isBodyAlreadyLocked = ref<boolean>(false)
|
|
48
|
-
const currentIndex = ref(0)
|
|
49
|
-
const forward = ref(true)
|
|
50
33
|
const show = ref(false)
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
34
|
+
const locked = ref(false) // a click pins the menu; hover can no longer close it
|
|
35
|
+
const currentIndex = ref(0)
|
|
36
|
+
const swapName = ref('swap-fade') // panel transition: directional slide on switch, fade on fresh open
|
|
37
|
+
const hoverTimer = ref<ReturnType<typeof setTimeout> | null>(null)
|
|
55
38
|
|
|
56
|
-
const
|
|
57
|
-
switch (index) {
|
|
58
|
-
case 0:
|
|
59
|
-
return ProductMenu
|
|
60
|
-
case 1:
|
|
61
|
-
return ContactMenu
|
|
62
|
-
case 2:
|
|
63
|
-
return null
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const toggleMenu = (index: number) => {
|
|
67
|
-
if (index === 2) {
|
|
68
|
-
const pageUrl = useRequestURL()
|
|
39
|
+
const currentMenu = computed(() => navs[currentIndex.value]?.menu ?? null)
|
|
69
40
|
|
|
70
|
-
|
|
41
|
+
function isActive(index: number) {
|
|
42
|
+
return show.value && currentIndex.value === index
|
|
43
|
+
}
|
|
71
44
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
function openMenu(index: number) {
|
|
48
|
+
swapName.value = show.value && index !== currentIndex.value
|
|
49
|
+
? (index > currentIndex.value ? 'slide-next' : 'slide-prev')
|
|
50
|
+
: 'swap-fade'
|
|
51
|
+
currentIndex.value = index
|
|
52
|
+
show.value = true
|
|
53
|
+
}
|
|
79
54
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// navigateToLocalizedPage({ locale, projectUrl: landing, path: PAGES.api, target: '_blank' })
|
|
84
|
-
// }
|
|
85
|
-
// else if (index === 4) {
|
|
86
|
-
// navigateToLocalizedPage({ locale, projectUrl: pk, path: PAGES.services + '/kgd', target: '_blank' })
|
|
87
|
-
// }
|
|
88
|
-
else {
|
|
89
|
-
currentMenu.value = currentMenuToShow(index)
|
|
90
|
-
const prev = currentIndex.value
|
|
91
|
-
if (prev < index) {
|
|
92
|
-
forward.value = false
|
|
93
|
-
} else if (prev > index) {
|
|
94
|
-
forward.value = true
|
|
95
|
-
}
|
|
96
|
-
animation.value = forward.value ? ' prev' : 'next'
|
|
97
|
-
if (!show.value) animation.value = 'slide-fade'
|
|
98
|
-
currentIndex.value = index
|
|
99
|
-
show.value = true
|
|
100
|
-
}
|
|
55
|
+
function closeMenu() {
|
|
56
|
+
show.value = false
|
|
57
|
+
locked.value = false
|
|
101
58
|
}
|
|
102
|
-
const closeMenu = (forceClose = false) => {
|
|
103
|
-
if (isMenuLocked.value && !forceClose) {
|
|
104
|
-
return
|
|
105
|
-
} else {
|
|
106
|
-
animation.value = 'slide-fade'
|
|
107
|
-
show.value = false
|
|
108
59
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
60
|
+
function goToTariffs() {
|
|
61
|
+
const host = useRequestURL().hostname
|
|
62
|
+
const codeMap: Record<string, string> = {
|
|
63
|
+
pk: 'pk',
|
|
64
|
+
avto: 'auto',
|
|
65
|
+
tnved: 'ved',
|
|
66
|
+
analytics: 'analytics',
|
|
67
|
+
ac: 'compliance',
|
|
68
|
+
zakupki: 'procurement',
|
|
69
|
+
edo: 'edo',
|
|
113
70
|
}
|
|
71
|
+
const key = Object.keys(codeMap).find(item => host.includes(item))
|
|
72
|
+
const code = key ? `?code=${codeMap[key]}` : ''
|
|
73
|
+
navigateToLocalizedPage({ locale, projectUrl: landing, path: PAGES.tariffs + code, target: '_blank' })
|
|
114
74
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
delayTimer.value = setTimeout(() => {
|
|
123
|
-
toggleMenu(index)
|
|
124
|
-
}, 100)
|
|
125
|
-
}
|
|
126
|
-
} else show.value = false
|
|
127
|
-
}
|
|
128
|
-
const onMenuMouseLeave = () => {
|
|
129
|
-
if (delayTimer.value) {
|
|
130
|
-
clearTimeout(delayTimer.value)
|
|
131
|
-
delayTimer.value = null
|
|
75
|
+
|
|
76
|
+
// Click pins the menu open; items without a menu (Tariffs) navigate instead.
|
|
77
|
+
function toggleMenu(index: number) {
|
|
78
|
+
const nav = navs[index]
|
|
79
|
+
if (!nav?.menu) {
|
|
80
|
+
goToTariffs()
|
|
81
|
+
return
|
|
132
82
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
isMenuLocked.value = true
|
|
136
|
-
}
|
|
137
|
-
const toggleBodyScroll = (el: HTMLBodyElement | HTMLDivElement, lock: boolean) => {
|
|
138
|
-
if (lock) {
|
|
139
|
-
setTimeout(() => {
|
|
140
|
-
disableBodyScroll(el, { reserveScrollBarGap: true })
|
|
141
|
-
}, 0)
|
|
83
|
+
if (show.value && currentIndex.value === index) {
|
|
84
|
+
closeMenu()
|
|
142
85
|
return
|
|
143
86
|
}
|
|
144
|
-
|
|
87
|
+
openMenu(index)
|
|
88
|
+
locked.value = true
|
|
145
89
|
}
|
|
146
90
|
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
(newVal, oldVal) => {
|
|
154
|
-
const wasShown = oldVal ? oldVal[0] : false
|
|
155
|
-
const [isShown, menuEl] = newVal
|
|
156
|
-
const isOpening = isShown
|
|
157
|
-
const isClosing = wasShown && !isShown
|
|
158
|
-
|
|
159
|
-
if (!menuEl) return
|
|
160
|
-
if (isOpening) isBodyAlreadyLocked.value = !!document.body.style.overflow
|
|
91
|
+
function clearHoverTimer() {
|
|
92
|
+
if (hoverTimer.value) {
|
|
93
|
+
clearTimeout(hoverTimer.value)
|
|
94
|
+
hoverTimer.value = null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
161
97
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
98
|
+
// Hover opens menu items; hovering Tariffs dismisses an unpinned menu.
|
|
99
|
+
function scheduleOpen(index: number) {
|
|
100
|
+
clearHoverTimer()
|
|
101
|
+
hoverTimer.value = setTimeout(() => {
|
|
102
|
+
if (navs[index]?.menu) openMenu(index)
|
|
103
|
+
else if (show.value && !locked.value) closeMenu()
|
|
104
|
+
}, show.value ? SWITCH_DELAY : OPEN_DELAY)
|
|
105
|
+
}
|
|
166
106
|
|
|
167
|
-
|
|
107
|
+
// Overlay closes on click regardless of the pin; on hover only while unpinned.
|
|
108
|
+
function onOverlayHover() {
|
|
109
|
+
if (!locked.value) closeMenu()
|
|
110
|
+
}
|
|
168
111
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
watch(show, async (open) => {
|
|
116
|
+
if (open) {
|
|
117
|
+
await nextTick()
|
|
118
|
+
if (menu.value) disableBodyScroll(menu.value, { reserveScrollBarGap: false })
|
|
119
|
+
}
|
|
120
|
+
else if (menu.value) {
|
|
121
|
+
enableBodyScroll(menu.value)
|
|
172
122
|
}
|
|
173
|
-
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
onBeforeUnmount(() => {
|
|
126
|
+
clearHoverTimer()
|
|
127
|
+
if (menu.value) enableBodyScroll(menu.value)
|
|
128
|
+
})
|
|
174
129
|
</script>
|
|
175
130
|
|
|
176
131
|
<template>
|
|
177
132
|
<div class="relative">
|
|
178
|
-
<ul>
|
|
133
|
+
<ul class="flex items-center">
|
|
179
134
|
<li
|
|
180
135
|
v-for="(nav, index) in navs"
|
|
181
136
|
:key="nav.label"
|
|
182
|
-
class="
|
|
183
|
-
:data-test-id="nav.
|
|
184
|
-
@click="lockMenu"
|
|
137
|
+
class="relative after:absolute after:right-0 after:top-1/2 after:h-6 after:w-px after:-translate-y-1/2 after:bg-gray-300 last:after:hidden dark:after:bg-gray-700"
|
|
138
|
+
:data-test-id="nav.dataAttribute"
|
|
185
139
|
role="listitem"
|
|
186
140
|
>
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
class="group relative cursor-pointer px-3 py-2 font-bold uppercase tracking-tight transition-colors duration-150"
|
|
144
|
+
:class="isActive(index)
|
|
145
|
+
? 'text-blue-700 dark:text-blue-400'
|
|
146
|
+
: 'text-deepblue-900 hover:text-blue-700 dark:text-gray-200 dark:hover:text-blue-400'"
|
|
147
|
+
@mouseenter="scheduleOpen(index)"
|
|
148
|
+
@mouseleave="clearHoverTimer"
|
|
190
149
|
@click="toggleMenu(index)"
|
|
191
|
-
@mouseleave="onMenuMouseLeave"
|
|
192
150
|
>
|
|
193
151
|
{{ t(nav.label) }}
|
|
194
|
-
|
|
152
|
+
<span
|
|
153
|
+
class="absolute inset-x-3 -bottom-0.5 h-0.5 origin-left rounded-full bg-blue-600 transition-transform duration-200 dark:bg-blue-400"
|
|
154
|
+
:class="isActive(index) ? 'scale-x-100' : 'scale-x-0 group-hover:scale-x-100'"
|
|
155
|
+
/>
|
|
156
|
+
</button>
|
|
195
157
|
</li>
|
|
196
158
|
</ul>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
v-show="show"
|
|
208
|
-
:animation="forward ? 'next' : 'prev'"
|
|
209
|
-
:index="currentIndex"
|
|
210
|
-
:url="buildLocalizedUrl(locale, landing, PAGES.contacts)"
|
|
211
|
-
@outer-click="closeMenu(true)"
|
|
212
|
-
@mouse-over="closeMenu"
|
|
213
|
-
/>
|
|
214
|
-
</keep-alive>
|
|
215
|
-
</transition>
|
|
159
|
+
|
|
160
|
+
<!-- One transition drives the whole panel's open/close (appear + disappear). -->
|
|
161
|
+
<transition name="menu">
|
|
162
|
+
<div
|
|
163
|
+
v-if="show"
|
|
164
|
+
ref="menu"
|
|
165
|
+
class="fixed inset-x-0 z-20"
|
|
166
|
+
:style="{ top: `${menuTop}px`, height: `calc(100dvh - ${menuTop}px)` }"
|
|
167
|
+
>
|
|
168
|
+
<!-- Full-page dimmed blur backdrop behind the panel. -->
|
|
216
169
|
<div
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
@
|
|
220
|
-
@mouseover="closeMenu(false)"
|
|
170
|
+
class="absolute inset-0 bg-gray-900/30 backdrop-blur dark:bg-black/50"
|
|
171
|
+
@click="closeMenu"
|
|
172
|
+
@mouseover="onOverlayHover"
|
|
221
173
|
/>
|
|
174
|
+
|
|
175
|
+
<!-- Panel on top; slides left/right when switching between menu items. -->
|
|
176
|
+
<transition
|
|
177
|
+
:name="swapName"
|
|
178
|
+
mode="out-in"
|
|
179
|
+
>
|
|
180
|
+
<component
|
|
181
|
+
:is="currentMenu"
|
|
182
|
+
:key="currentIndex"
|
|
183
|
+
class="relative"
|
|
184
|
+
:url="buildLocalizedUrl(locale, landing, PAGES.contacts)"
|
|
185
|
+
/>
|
|
186
|
+
</transition>
|
|
222
187
|
</div>
|
|
223
|
-
</
|
|
188
|
+
</transition>
|
|
224
189
|
</div>
|
|
225
190
|
</template>
|
|
226
191
|
|
|
227
192
|
<style scoped lang="scss">
|
|
228
|
-
$
|
|
229
|
-
|
|
230
|
-
.next-leave-to {
|
|
231
|
-
opacity: 0;
|
|
232
|
-
transform: translateX($percent);
|
|
233
|
-
position: absolute;
|
|
234
|
-
}
|
|
193
|
+
$ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
194
|
+
$shift: 24px;
|
|
235
195
|
|
|
236
|
-
.
|
|
237
|
-
|
|
196
|
+
// Whole menu: open and close (vertical fade, no horizontal motion).
|
|
197
|
+
.menu-enter-active,
|
|
198
|
+
.menu-leave-active {
|
|
199
|
+
transition:
|
|
200
|
+
opacity 0.25s $ease,
|
|
201
|
+
transform 0.25s $ease;
|
|
238
202
|
}
|
|
239
203
|
|
|
240
|
-
.
|
|
241
|
-
|
|
204
|
+
.menu-enter-from,
|
|
205
|
+
.menu-leave-to {
|
|
242
206
|
opacity: 0;
|
|
243
|
-
transform:
|
|
207
|
+
transform: translateY(-12px);
|
|
244
208
|
}
|
|
245
209
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
.
|
|
249
|
-
.prev-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
210
|
+
// Switching panels: slide toward the direction of travel (out-in, so no overlap).
|
|
211
|
+
.slide-next-enter-active,
|
|
212
|
+
.slide-next-leave-active,
|
|
213
|
+
.slide-prev-enter-active,
|
|
214
|
+
.slide-prev-leave-active,
|
|
215
|
+
.swap-fade-enter-active,
|
|
216
|
+
.swap-fade-leave-active {
|
|
217
|
+
transition:
|
|
218
|
+
opacity 0.2s $ease,
|
|
219
|
+
transform 0.2s $ease;
|
|
254
220
|
}
|
|
255
221
|
|
|
256
|
-
.
|
|
257
|
-
|
|
222
|
+
.slide-next-enter-from {
|
|
223
|
+
opacity: 0;
|
|
224
|
+
transform: translateX($shift);
|
|
258
225
|
}
|
|
259
226
|
|
|
260
|
-
.
|
|
261
|
-
transition: all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
227
|
+
.slide-next-leave-to {
|
|
262
228
|
opacity: 0;
|
|
263
|
-
transform: translateX(
|
|
229
|
+
transform: translateX(-$shift);
|
|
264
230
|
}
|
|
265
231
|
|
|
266
|
-
.slide-
|
|
267
|
-
|
|
268
|
-
|
|
232
|
+
.slide-prev-enter-from {
|
|
233
|
+
opacity: 0;
|
|
234
|
+
transform: translateX(-$shift);
|
|
269
235
|
}
|
|
270
236
|
|
|
271
|
-
.slide-
|
|
272
|
-
.slide-fade-leave-to {
|
|
273
|
-
transform: translateY(-12px);
|
|
237
|
+
.slide-prev-leave-to {
|
|
274
238
|
opacity: 0;
|
|
239
|
+
transform: translateX($shift);
|
|
275
240
|
}
|
|
276
241
|
|
|
277
|
-
.
|
|
278
|
-
.
|
|
279
|
-
|
|
242
|
+
// Fresh open (no switch): fade only, so the panel never slides sideways.
|
|
243
|
+
.swap-fade-enter-from,
|
|
244
|
+
.swap-fade-leave-to {
|
|
245
|
+
opacity: 0;
|
|
280
246
|
}
|
|
281
247
|
|
|
282
|
-
|
|
283
|
-
.
|
|
284
|
-
|
|
248
|
+
@media (prefers-reduced-motion: reduce) {
|
|
249
|
+
.menu-enter-active,
|
|
250
|
+
.menu-leave-active,
|
|
251
|
+
.slide-next-enter-active,
|
|
252
|
+
.slide-next-leave-active,
|
|
253
|
+
.slide-prev-enter-active,
|
|
254
|
+
.slide-prev-leave-active,
|
|
255
|
+
.swap-fade-enter-active,
|
|
256
|
+
.swap-fade-leave-active {
|
|
257
|
+
transition: none;
|
|
258
|
+
}
|
|
285
259
|
}
|
|
286
260
|
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Calendar from '#adata-ui/icons/calendar.vue'
|
|
3
|
+
import Gauge from '#adata-ui/icons/gauge.vue'
|
|
4
|
+
|
|
5
|
+
defineOptions({ name: 'HeaderUsage' })
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
8
|
+
limitRemaining: 0,
|
|
9
|
+
maxLimit: 300,
|
|
10
|
+
daysRemaining: 0,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
limitRemaining?: number
|
|
15
|
+
maxLimit?: number
|
|
16
|
+
daysRemaining?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Bar fill represents the remaining quota: full bar = plenty of requests left.
|
|
20
|
+
const percent = computed(() => {
|
|
21
|
+
if (props.maxLimit <= 0) return 0
|
|
22
|
+
const ratio = (props.limitRemaining / props.maxLimit) * 100
|
|
23
|
+
return Math.min(100, Math.max(0, ratio))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Animate the fill from 0 on mount and on every value change.
|
|
27
|
+
const displayPercent = ref(0)
|
|
28
|
+
onMounted(() => {
|
|
29
|
+
displayPercent.value = percent.value
|
|
30
|
+
})
|
|
31
|
+
watch(percent, (value) => {
|
|
32
|
+
displayPercent.value = value
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const isLow = computed(() => percent.value < 20)
|
|
36
|
+
|
|
37
|
+
const barColorClass = computed(() =>
|
|
38
|
+
isLow.value
|
|
39
|
+
? 'bg-red-500 dark:bg-red-400'
|
|
40
|
+
: 'bg-blue-600 dark:bg-blue-500',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const valueColorClass = computed(() =>
|
|
44
|
+
isLow.value ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-gray-100',
|
|
45
|
+
)
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<client-only>
|
|
50
|
+
<div
|
|
51
|
+
class="inline-flex items-center gap-2.5"
|
|
52
|
+
data-test-id="header-usage"
|
|
53
|
+
>
|
|
54
|
+
<a-tooltip-v2
|
|
55
|
+
class="shrink-0 items-center gap-2.5"
|
|
56
|
+
placement="bottom"
|
|
57
|
+
arrow
|
|
58
|
+
:z-index="10001"
|
|
59
|
+
content-class="py-0.5"
|
|
60
|
+
>
|
|
61
|
+
<gauge class="size-4 shrink-0 text-gray-600 dark:text-gray-300" />
|
|
62
|
+
|
|
63
|
+
<div class="h-1.5 w-20 rounded-full bg-deepblue-900/30 dark:bg-white/30">
|
|
64
|
+
<div
|
|
65
|
+
class="usage-bar h-full rounded-full"
|
|
66
|
+
:class="barColorClass"
|
|
67
|
+
:style="{ width: `${displayPercent}%` }"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<span
|
|
72
|
+
class="text-sm tabular-nums leading-none"
|
|
73
|
+
data-test-id="header-usage-requests"
|
|
74
|
+
>
|
|
75
|
+
<span
|
|
76
|
+
class="font-bold transition-colors duration-300"
|
|
77
|
+
:class="valueColorClass"
|
|
78
|
+
>{{ limitRemaining }}</span><span class="font-medium text-gray-600 dark:text-gray-300">/{{ maxLimit }}</span>
|
|
79
|
+
</span>
|
|
80
|
+
|
|
81
|
+
<template #content>
|
|
82
|
+
<span class="whitespace-nowrap text-[11px] leading-none text-deepblue-900 dark:text-gray-100">
|
|
83
|
+
Лимит запросов: {{ limitRemaining }}
|
|
84
|
+
</span>
|
|
85
|
+
</template>
|
|
86
|
+
</a-tooltip-v2>
|
|
87
|
+
|
|
88
|
+
<span class="h-4 w-px shrink-0 bg-deepblue-900/15 dark:bg-white/15" />
|
|
89
|
+
|
|
90
|
+
<a-tooltip-v2
|
|
91
|
+
class="shrink-0"
|
|
92
|
+
placement="bottom"
|
|
93
|
+
arrow
|
|
94
|
+
:z-index="10001"
|
|
95
|
+
content-class="py-0.5"
|
|
96
|
+
>
|
|
97
|
+
<div
|
|
98
|
+
class="flex shrink-0 items-center gap-1.5"
|
|
99
|
+
data-test-id="header-usage-days"
|
|
100
|
+
>
|
|
101
|
+
<calendar class="size-4 shrink-0 text-gray-600 dark:text-gray-300" />
|
|
102
|
+
<span class="text-sm font-bold tabular-nums text-gray-900 dark:text-gray-100">{{ daysRemaining }}</span>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<template #content>
|
|
106
|
+
<span class="whitespace-nowrap text-[11px] leading-none text-deepblue-900 dark:text-gray-100">
|
|
107
|
+
Остаток дней: {{ daysRemaining }}
|
|
108
|
+
</span>
|
|
109
|
+
</template>
|
|
110
|
+
</a-tooltip-v2>
|
|
111
|
+
</div>
|
|
112
|
+
</client-only>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<style scoped>
|
|
116
|
+
.usage-bar {
|
|
117
|
+
transition: width 700ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@media (prefers-reduced-motion: reduce) {
|
|
121
|
+
.usage-bar {
|
|
122
|
+
transition: none;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
</style>
|