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.
Files changed (41) hide show
  1. package/.nuxtrc +1 -1
  2. package/.playground/app.config.ts +5 -5
  3. package/.playground/app.vue +102 -0
  4. package/README.md +75 -75
  5. package/components/elements/README.md +1 -1
  6. package/components/elements/button-login/index.vue +6 -10
  7. package/components/elements/tree-select/ATreeSelect.vue +5 -1
  8. package/components/elements/tree-select/components/tree-select-nodes.vue +4 -3
  9. package/components/features/color-mode/AColorMode.client.vue +74 -32
  10. package/components/features/dropdown/ADropdownV2.vue +141 -0
  11. package/components/features/lang-switcher/lang-switcher.vue +120 -40
  12. package/components/features//321/201hange-version/AChangeVersion.vue +1 -1
  13. package/components/forms/README.md +1 -1
  14. package/components/navigation/README.md +1 -1
  15. package/components/navigation/footer/AFooter.vue +1 -1
  16. package/components/navigation/header/AHeader.vue +56 -33
  17. package/components/navigation/header/AlmatyContacts.vue +1 -1
  18. package/components/navigation/header/CardGallery.vue +5 -3
  19. package/components/navigation/header/ContactMenu.vue +26 -92
  20. package/components/navigation/header/HeaderLink.vue +189 -215
  21. package/components/navigation/header/HeaderUsage.vue +125 -0
  22. package/components/navigation/header/NavList.vue +35 -50
  23. package/components/navigation/header/ProductMenu.vue +72 -126
  24. package/components/navigation/header/ProfileMenu.vue +131 -150
  25. package/components/navigation/header/SystemNotification.vue +110 -0
  26. package/components/navigation/mobile-navigation/AMobileNavigation.vue +23 -15
  27. package/components/navigation/pill-tabs/APillTabs.vue +7 -2
  28. package/components/overlays/README.md +1 -1
  29. package/components/overlays/tooltip/ATooltipV2.vue +233 -0
  30. package/components/overlays/tooltip/types.ts +26 -0
  31. package/components/overlays/tooltip/useTooltipTrigger.ts +101 -0
  32. package/composables/useHeaderNavigationLinks.ts +15 -8
  33. package/composables/useUrls.ts +1 -1
  34. package/icons/gauge.vue +17 -0
  35. package/icons/sun.vue +13 -3
  36. package/lang/en.ts +6 -0
  37. package/lang/kk.ts +6 -0
  38. package/lang/ru.ts +6 -0
  39. package/package.json +1 -1
  40. package/shared/constans/pages.ts +1 -1
  41. package/components/navigation/header/TopHeader.vue +0 -196
@@ -1,286 +1,260 @@
1
1
  <script setup lang="ts">
2
- import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'
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 { buildLocalizedUrl } from '#adata-ui/utils/localizedNavigation'
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
- const { landing, pk } = useUrls()
10
+ withDefaults(defineProps<{ menuTop?: number }>(), {
11
+ menuTop: 64,
12
+ })
13
+
14
+ const { landing } = useUrls()
12
15
  const { t, locale } = useI18n()
13
16
 
14
- const navs: any = [
15
- {
16
- label: 'header.navs.products',
17
- to: landing + PAGES.totalServices,
18
- data_attribute: 'header-products-and-solutions-button',
19
- },
20
- {
21
- label: 'header.navs.contacts',
22
- to: landing + PAGES.contacts,
23
- data_attribute: 'header-contacts-button',
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
- // const pageUrl = useRequestURL()
37
- // if (pageUrl.hostname.startsWith('pk')) {
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 isMenuLocked = ref(false)
52
- const animation = ref('slide-fade')
53
- const currentMenu = shallowRef(null)
54
- const delayTimer = ref<ReturnType<typeof setTimeout> | null>(null)
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 currentMenuToShow = (index) => {
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
- let code = ''
41
+ function isActive(index: number) {
42
+ return show.value && currentIndex.value === index
43
+ }
71
44
 
72
- if (pageUrl.hostname.includes('pk')) code = '?code=pk'
73
- if (pageUrl.hostname.includes('avto')) code = '?code=auto'
74
- if (pageUrl.hostname.includes('tnved')) code = '?code=ved'
75
- if (pageUrl.hostname.includes('analytics')) code = '?code=analytics'
76
- if (pageUrl.hostname.includes('ac')) code = '?code=compliance'
77
- if (pageUrl.hostname.includes('zakupki')) code = '?code=procurement'
78
- if (pageUrl.hostname.includes('edo')) code = '?code=edo'
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
- navigateToLocalizedPage({ locale, projectUrl: landing, path: PAGES.tariffs + code, target: '_blank' })
81
- }
82
- // else if (index === 3) {
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
- setTimeout(() => {
110
- isMenuLocked.value = false
111
- currentMenu.value = null
112
- }, 200)
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
- const onMenuMouseEnter = (index: number) => {
116
- if (![2, 3, 4].includes(index)) {
117
- if (show.value === false) {
118
- delayTimer.value = setTimeout(() => {
119
- toggleMenu(index)
120
- }, 600)
121
- } else {
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
- const lockMenu = () => {
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
- enableBodyScroll(el)
87
+ openMenu(index)
88
+ locked.value = true
145
89
  }
146
90
 
147
- onBeforeUnmount(() => {
148
- if (show.value) toggleBodyScroll(menu.value, false)
149
- })
150
-
151
- watch(
152
- () => [show.value, menu.value],
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
- if (isShown) {
163
- toggleBodyScroll(menuEl, true)
164
- return
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
- if (!isClosing || isBodyAlreadyLocked.value) return
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
- setTimeout(() => {
170
- if (menuEl) toggleBodyScroll(menuEl, false)
171
- }, 100)
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="group inline-block border-r border-[#2C3E501A] px-2 last-of-type:border-none dark:border-[#E3E5E81A]"
183
- :data-test-id="nav.data_attribute"
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
- <div
188
- class="relative cursor-pointer font-bold uppercase text-gray-800 hover:text-gray-600 dark:text-gray-200 hover:dark:text-gray-600"
189
- @mouseenter="onMenuMouseEnter(index)"
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
- </div>
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
- <div
198
- v-show="show"
199
- ref="menu"
200
- class="top-[104px] fixed left-0 z-20 h-full w-full"
201
- >
202
- <div class="h-full w-full">
203
- <transition :name="animation">
204
- <keep-alive>
205
- <component
206
- :is="currentMenu"
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
- v-show="show"
218
- class="absolute left-[-25%] h-full w-[150%] bg-[#E3E5E833] backdrop-blur transition"
219
- @click="closeMenu(true)"
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
- </div>
188
+ </transition>
224
189
  </div>
225
190
  </template>
226
191
 
227
192
  <style scoped lang="scss">
228
- $percent: 30%;
229
- .next-enter-from,
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
- .next-enter-active {
237
- transition: all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
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
- .next-leave-active {
241
- transition: all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
204
+ .menu-enter-from,
205
+ .menu-leave-to {
242
206
  opacity: 0;
243
- transform: translateX(-$percent);
207
+ transform: translateY(-12px);
244
208
  }
245
209
 
246
- // Backwards transition
247
-
248
- .prev-enter-from,
249
- .prev-leave-to {
250
- position: absolute;
251
-
252
- opacity: 0;
253
- transform: translateX(-$percent);
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
- .prev-enter-active {
257
- transition: all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
222
+ .slide-next-enter-from {
223
+ opacity: 0;
224
+ transform: translateX($shift);
258
225
  }
259
226
 
260
- .prev-leave-active {
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($percent);
229
+ transform: translateX(-$shift);
264
230
  }
265
231
 
266
- .slide-fade-enter-active,
267
- .slide-fade-leave-active {
268
- transition: all 0.2s ease-in-out;
232
+ .slide-prev-enter-from {
233
+ opacity: 0;
234
+ transform: translateX(-$shift);
269
235
  }
270
236
 
271
- .slide-fade-enter-from,
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
- .slide-change-enter-active,
278
- .slide-change-leave-active {
279
- transition: all 0.2s ease-in-out;
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
- .slide-change-enter-from,
283
- .slide-change-leave-to {
284
- opacity: 0;
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>