adata-ui 2.1.39 → 2.1.40-beta.1

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 (31) hide show
  1. package/.playground/app.vue +102 -0
  2. package/components/elements/button-login/index.vue +6 -10
  3. package/components/features/color-mode/AColorMode.client.vue +74 -32
  4. package/components/features/dropdown/ADropdownV2.vue +141 -0
  5. package/components/features/lang-switcher/lang-switcher.vue +120 -40
  6. package/components/features/pk-mobile-services/APkMobileServices.vue +5 -27
  7. package/components/features//321/201hange-version/AChangeVersion.vue +1 -1
  8. package/components/navigation/header/AHeader.vue +56 -33
  9. package/components/navigation/header/AlmatyContacts.vue +1 -1
  10. package/components/navigation/header/CardGallery.vue +5 -3
  11. package/components/navigation/header/ContactMenu.vue +26 -92
  12. package/components/navigation/header/HeaderLink.vue +189 -215
  13. package/components/navigation/header/HeaderUsage.vue +125 -0
  14. package/components/navigation/header/NavList.vue +56 -91
  15. package/components/navigation/header/ProductMenu.vue +79 -127
  16. package/components/navigation/header/ProfileMenu.vue +131 -150
  17. package/components/navigation/header/SystemNotification.vue +110 -0
  18. package/components/navigation/mobile-navigation/AMobileNavigation.vue +23 -15
  19. package/components/navigation/pill-tabs/APillTabs.vue +7 -2
  20. package/components/overlays/tooltip/ATooltipV2.vue +233 -0
  21. package/components/overlays/tooltip/types.ts +26 -0
  22. package/components/overlays/tooltip/useTooltipTrigger.ts +101 -0
  23. package/composables/useActiveNavigation.ts +84 -0
  24. package/composables/useHeaderNavigationLinks.ts +14 -7
  25. package/icons/gauge.vue +17 -0
  26. package/icons/sun.vue +13 -3
  27. package/lang/en.ts +6 -0
  28. package/lang/kk.ts +6 -0
  29. package/lang/ru.ts +6 -0
  30. package/package.json +1 -1
  31. package/components/navigation/header/TopHeader.vue +0 -196
@@ -0,0 +1,102 @@
1
+ <script setup lang="ts">
2
+ const manualOpen = ref(false)
3
+ const placements = ['top', 'top-start', 'bottom', 'bottom-end', 'left', 'right'] as const
4
+ </script>
5
+
6
+ <template>
7
+ <div class="min-h-screen bg-gray-50 p-10 dark:bg-gray-950">
8
+ <div class="mx-auto flex max-w-3xl flex-col gap-10">
9
+ <div class="flex items-center justify-between">
10
+ <h1 class="text-xl font-bold text-deepblue-900 dark:text-gray-100">
11
+ ATooltipV2
12
+ </h1>
13
+ <a-color-mode />
14
+ </div>
15
+
16
+ <!-- Placements + arrow (hover/focus) -->
17
+ <section class="flex flex-wrap gap-6">
18
+ <a-tooltip-v2
19
+ v-for="p in placements"
20
+ :key="p"
21
+ :placement="p"
22
+ arrow
23
+ :open-delay="80"
24
+ >
25
+ <button class="rounded-lg bg-blue-700 px-3 py-2 text-sm text-white dark:bg-blue-500 dark:text-gray-900">
26
+ {{ p }}
27
+ </button>
28
+ <template #content>
29
+ <div class="text-[11px] text-deepblue-900 dark:text-gray-100">
30
+ Placement <b>{{ p }}</b>
31
+ </div>
32
+ </template>
33
+ </a-tooltip-v2>
34
+ </section>
35
+
36
+ <!-- Click trigger, interactive content -->
37
+ <section>
38
+ <a-tooltip-v2
39
+ trigger="click"
40
+ placement="bottom-start"
41
+ arrow
42
+ content-class="w-56"
43
+ >
44
+ <button class="rounded-lg px-3 py-2 text-sm text-deepblue-900 ring-1 ring-gray-200 dark:text-gray-100 dark:ring-gray-700">
45
+ Click me
46
+ </button>
47
+ <template #content="{ close }">
48
+ <div class="flex flex-col gap-2 text-[11px] text-deepblue-900 dark:text-gray-100">
49
+ <p>Interactive — outside-click &amp; Esc close.</p>
50
+ <button
51
+ class="self-start text-blue-700 dark:text-blue-400"
52
+ @click="close"
53
+ >
54
+ Close
55
+ </button>
56
+ </div>
57
+ </template>
58
+ </a-tooltip-v2>
59
+ </section>
60
+
61
+ <!-- Manual via v-model:open -->
62
+ <section class="flex items-center gap-4">
63
+ <button
64
+ class="rounded-lg bg-deepblue-900 px-3 py-2 text-sm text-white dark:bg-gray-100 dark:text-gray-900"
65
+ @click="manualOpen = !manualOpen"
66
+ >
67
+ Toggle manual ({{ manualOpen }})
68
+ </button>
69
+ <a-tooltip-v2
70
+ v-model:open="manualOpen"
71
+ trigger="manual"
72
+ placement="right"
73
+ arrow
74
+ >
75
+ <span class="text-sm text-gray-500 dark:text-gray-400">anchor</span>
76
+ <template #content>
77
+ <div class="text-[11px] text-deepblue-900 dark:text-gray-100">
78
+ Manually controlled
79
+ </div>
80
+ </template>
81
+ </a-tooltip-v2>
82
+ </section>
83
+
84
+ <!-- Teleport out of an overflow-hidden container -->
85
+ <section class="h-24 overflow-hidden rounded-lg p-3 ring-1 ring-gray-200 dark:ring-gray-700">
86
+ <a-tooltip-v2
87
+ placement="top"
88
+ arrow
89
+ >
90
+ <button class="rounded-lg bg-green-500 px-3 py-2 text-sm text-white dark:text-gray-900">
91
+ Inside overflow-hidden (teleported)
92
+ </button>
93
+ <template #content>
94
+ <div class="text-[11px] text-deepblue-900 dark:text-gray-100">
95
+ Not clipped
96
+ </div>
97
+ </template>
98
+ </a-tooltip-v2>
99
+ </section>
100
+ </div>
101
+ </div>
102
+ </template>
@@ -1,22 +1,18 @@
1
1
  <script setup lang="ts">
2
- import IconLogout from '#adata-ui/icons/logout.vue'
2
+ defineEmits(['click'])
3
3
 
4
4
  const { t } = useI18n()
5
-
6
- defineEmits(['click'])
7
5
  </script>
8
6
 
9
7
  <template>
10
- <div
11
- class="hidden w-max cursor-pointer items-center gap-2 rounded-2xl bg-blue-700 px-4 py-[6px] font-semibold dark:bg-blue-500 lg:flex"
8
+ <button
9
+ type="button"
10
+ class="hidden h-8 cursor-pointer items-center rounded-xl bg-blue-700 px-3.5 text-sm font-semibold text-white transition-colors duration-150 hover:bg-blue-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:ring-offset-1 dark:bg-blue-500 dark:text-gray-900 dark:hover:bg-blue-600 dark:focus-visible:ring-offset-gray-900 lg:inline-flex"
12
11
  data-test-id="header-login-button"
13
12
  @click="$emit('click')"
14
13
  >
15
- <icon-logout class="text-white dark:text-gray-900 lg:h-4 lg:w-4" />
16
- <span class="hidden text-sm font-semibold text-white dark:text-gray-900 lg:inline-block min-w-[44px]">
17
- {{ t('header.login') }}
18
- </span>
19
- </div>
14
+ {{ t('header.login') }}
15
+ </button>
20
16
  </template>
21
17
 
22
18
  <style scoped></style>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import AToggle from "#adata-ui/components/forms/toggle/AToggle.vue";
3
- import Sun from "#adata-ui/icons/sun.vue";
4
- import Moon from "#adata-ui/icons/moon.vue";
2
+ import Moon from '#adata-ui/icons/moon.vue'
3
+ import Sun from '#adata-ui/icons/sun.vue'
4
+ import { useEventListener } from '@vueuse/core'
5
5
 
6
6
  const colorMode = useColorMode()
7
7
  const setColorMode = useCookie('colorMode')
@@ -10,45 +10,87 @@ if (setColorMode.value) {
10
10
  colorMode.preference = setColorMode.value
11
11
  }
12
12
 
13
- const mode = computed({
14
- get() {
15
- return colorMode.value === 'dark'
16
- },
17
- set() {
18
- const value = colorMode.value === 'dark' ? 'light' : 'dark'
19
- const hostname = location.hostname.split('.').reverse()
20
- const maxAge = 60 * 60 * 24 * 365 // 1 год
13
+ const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365
21
14
 
22
- colorMode.preference = value
23
- document.cookie = `colorMode=${value}; max-age=${maxAge}; domain=.${hostname[1]}.${hostname[0]}; path=/`
24
- }
25
- })
15
+ function cookieDomainAttr(): string {
16
+ const parts = location.hostname.split('.').reverse()
17
+ if (parts.length < 2) return ''
18
+ return `; domain=.${parts[1]}.${parts[0]}`
19
+ }
20
+
21
+ const isDark = computed(() => colorMode.value === 'dark')
26
22
 
27
- onMounted(() => {
28
- window.addEventListener('storage', (event) => {
29
- if (event.key === 'nuxt-color-mode') {
30
- colorMode.preference = event.newValue;
31
- }
32
- });
23
+ function toggle() {
24
+ const value = isDark.value ? 'light' : 'dark'
25
+ colorMode.preference = value
26
+ document.cookie = `colorMode=${value}; max-age=${ONE_YEAR_SECONDS}${cookieDomainAttr()}; path=/`
27
+ }
28
+
29
+ useEventListener(window, 'storage', (event: StorageEvent) => {
30
+ if (event.key === 'nuxt-color-mode' && event.newValue) {
31
+ colorMode.preference = event.newValue
32
+ }
33
33
  })
34
34
  </script>
35
35
 
36
36
  <template>
37
37
  <client-only>
38
- <a-toggle
39
- v-model="mode"
40
- size="2xl"
41
- active-class="dark:bg-gray-800"
42
- :off-icon="Sun"
43
- off-icon-class="w-4 h-4"
44
- on-icon-class="w-4 h-4"
45
- :on-icon="Moon"
46
- active-container-class="dark:bg-black"
38
+ <button
39
+ type="button"
40
+ class="relative inline-flex size-8 items-center justify-center overflow-hidden rounded-xl text-deepblue-900/80 transition-colors duration-150 hover:bg-deepblue-900/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:ring-offset-1 dark:text-gray-200 dark:hover:bg-white/10 dark:focus-visible:ring-offset-gray-900"
41
+ :aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
47
42
  data-test-id="header-switch-theme-toggle"
48
- />
43
+ @click="toggle"
44
+ >
45
+ <transition
46
+ enter-active-class="color-mode__icon-enter-active"
47
+ enter-from-class="color-mode__icon-enter-from"
48
+ enter-to-class="color-mode__icon-enter-to"
49
+ leave-active-class="color-mode__icon-leave-active"
50
+ leave-from-class="color-mode__icon-leave-from"
51
+ leave-to-class="color-mode__icon-leave-to"
52
+ mode="out-in"
53
+ >
54
+ <component
55
+ :is="isDark ? Moon : Sun"
56
+ :key="isDark ? 'moon' : 'sun'"
57
+ class="size-5"
58
+ :font-controlled="false"
59
+ filled
60
+ />
61
+ </transition>
62
+ </button>
49
63
  </client-only>
50
64
  </template>
51
65
 
66
+ <style scoped>
67
+ .color-mode__icon-enter-active,
68
+ .color-mode__icon-leave-active {
69
+ transition:
70
+ opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
71
+ transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
72
+ }
73
+
74
+ .color-mode__icon-enter-from {
75
+ opacity: 0;
76
+ transform: rotate(-90deg) scale(0.6);
77
+ }
78
+
79
+ .color-mode__icon-leave-to {
80
+ opacity: 0;
81
+ transform: rotate(90deg) scale(0.6);
82
+ }
83
+
84
+ .color-mode__icon-enter-to,
85
+ .color-mode__icon-leave-from {
86
+ opacity: 1;
87
+ transform: rotate(0) scale(1);
88
+ }
52
89
 
53
- <style>
90
+ @media (prefers-reduced-motion: reduce) {
91
+ .color-mode__icon-enter-active,
92
+ .color-mode__icon-leave-active {
93
+ transition: none;
94
+ }
95
+ }
54
96
  </style>
@@ -0,0 +1,141 @@
1
+ <script setup lang="ts">
2
+ import type { Placement } from '@floating-ui/vue'
3
+ import { autoUpdate, flip, offset as offsetMiddleware, shift, useFloating } from '@floating-ui/vue'
4
+ import { onClickOutside, onKeyStroke } from '@vueuse/core'
5
+
6
+ defineOptions({ name: 'ADropdownV2' })
7
+
8
+ const props = withDefaults(defineProps<{
9
+ placement?: Placement
10
+ offset?: number
11
+ }>(), {
12
+ placement: 'bottom-end',
13
+ offset: 8,
14
+ })
15
+
16
+ const wrapper = ref<HTMLElement | null>(null)
17
+ const reference = ref<HTMLElement | null>(null)
18
+ const floating = ref<HTMLElement | null>(null)
19
+ const isOpen = ref(false)
20
+
21
+ const { x, y, strategy, update, placement: resolvedPlacement } = useFloating(reference, floating, {
22
+ placement: computed(() => props.placement),
23
+ strategy: 'absolute',
24
+ middleware: computed(() => [offsetMiddleware(props.offset), flip(), shift({ padding: 8 })]),
25
+ })
26
+
27
+ // Anchor the grow/shrink animation to the trigger so the panel feels attached.
28
+ const transformOrigin = computed(() => {
29
+ const [side, align] = resolvedPlacement.value.split('-')
30
+ const vertical = side === 'top' ? 'bottom' : 'top'
31
+ const horizontal = align === 'start' ? 'left' : align === 'end' ? 'right' : 'center'
32
+ return `${vertical} ${horizontal}`
33
+ })
34
+
35
+ let cleanup: (() => void) | null = null
36
+ watch(isOpen, (open) => {
37
+ if (open && reference.value && floating.value) {
38
+ cleanup = autoUpdate(reference.value, floating.value, update)
39
+ }
40
+ else {
41
+ cleanup?.()
42
+ cleanup = null
43
+ }
44
+ })
45
+ onUnmounted(() => cleanup?.())
46
+
47
+ function toggle() {
48
+ isOpen.value = !isOpen.value
49
+ }
50
+ function close() {
51
+ isOpen.value = false
52
+ }
53
+
54
+ onClickOutside(wrapper, () => {
55
+ if (isOpen.value) close()
56
+ })
57
+ onKeyStroke('Escape', () => {
58
+ if (isOpen.value) close()
59
+ })
60
+ </script>
61
+
62
+ <template>
63
+ <div
64
+ ref="wrapper"
65
+ class="relative"
66
+ >
67
+ <div
68
+ ref="reference"
69
+ class="inline-flex"
70
+ >
71
+ <slot
72
+ :toggle="toggle"
73
+ :is-open="isOpen"
74
+ />
75
+ </div>
76
+
77
+ <transition
78
+ enter-active-class="a-dropdown-v2__enter-active"
79
+ enter-from-class="a-dropdown-v2__enter-from"
80
+ enter-to-class="a-dropdown-v2__enter-to"
81
+ leave-active-class="a-dropdown-v2__leave-active"
82
+ leave-from-class="a-dropdown-v2__leave-from"
83
+ leave-to-class="a-dropdown-v2__leave-to"
84
+ >
85
+ <div
86
+ v-if="isOpen"
87
+ ref="floating"
88
+ class="z-[60] overflow-hidden rounded-xl shadow-lg shadow-gray-900/10 dark:shadow-black/30"
89
+ :style="{
90
+ position: strategy,
91
+ left: x != null ? `${x}px` : '',
92
+ top: y != null ? `${y}px` : '',
93
+ transformOrigin,
94
+ }"
95
+ >
96
+ <slot
97
+ name="content"
98
+ :close="close"
99
+ />
100
+ </div>
101
+ </transition>
102
+ </div>
103
+ </template>
104
+
105
+ <style scoped>
106
+ .a-dropdown-v2__enter-active {
107
+ transition:
108
+ opacity 200ms ease,
109
+ transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
110
+ }
111
+
112
+ .a-dropdown-v2__leave-active {
113
+ transition:
114
+ opacity 140ms ease,
115
+ transform 160ms cubic-bezier(0.4, 0, 1, 1);
116
+ }
117
+
118
+ .a-dropdown-v2__enter-from,
119
+ .a-dropdown-v2__leave-to {
120
+ opacity: 0;
121
+ transform: translateY(-8px) scale(0.96);
122
+ }
123
+
124
+ .a-dropdown-v2__enter-to,
125
+ .a-dropdown-v2__leave-from {
126
+ opacity: 1;
127
+ transform: translateY(0) scale(1);
128
+ }
129
+
130
+ @media (prefers-reduced-motion: reduce) {
131
+ .a-dropdown-v2__enter-active,
132
+ .a-dropdown-v2__leave-active {
133
+ transition: opacity 120ms ease;
134
+ }
135
+
136
+ .a-dropdown-v2__enter-from,
137
+ .a-dropdown-v2__leave-to {
138
+ transform: none;
139
+ }
140
+ }
141
+ </style>
@@ -1,73 +1,153 @@
1
1
  <script setup lang="ts">
2
- import { onClickOutside } from "@vueuse/core";
2
+ import Check from '#adata-ui/icons/check/check.vue'
3
+ import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue'
4
+ import { onClickOutside } from '@vueuse/core'
5
+
6
+ type LocaleItem = string | { code: string }
3
7
 
4
8
  const props = withDefaults(defineProps<{
5
9
  isMobile?: boolean
6
10
  }>(), {
7
- isMobile: false
11
+ isMobile: false,
8
12
  })
9
13
 
10
- const langSwitcher = ref<HTMLDivElement | null>(null)
11
-
12
14
  const switchLocalePath = useSwitchLocalePath()
13
- const { locale, locales } = useI18n()
14
- const availableLocales = computed(() => {
15
- return (locales.value).filter(i => i.code !== locale.value)
16
- })
15
+ const { t, locale, locales } = useI18n()
16
+
17
+ const testIdSuffix = computed(() => props.isMobile ? '-mobile' : '')
17
18
 
19
+ const wrapper = ref<HTMLDivElement | null>(null)
20
+ const reference = ref<HTMLButtonElement | null>(null)
21
+ const floating = ref<HTMLDivElement | null>(null)
18
22
  const isOpen = ref(false)
19
23
 
20
- onClickOutside(langSwitcher,() => {
21
- isOpen.value = false
24
+ const placement = computed(() => props.isMobile ? 'bottom-end' as const : 'bottom-start' as const)
25
+
26
+ const { x, y, strategy, update } = useFloating(reference, floating, {
27
+ placement,
28
+ strategy: 'absolute',
29
+ middleware: computed(() => [
30
+ offset(6),
31
+ flip({ fallbackPlacements: props.isMobile ? ['top-end'] : ['top-start'] }),
32
+ shift({ padding: 8 }),
33
+ ]),
22
34
  })
23
35
 
24
- const { t } = useI18n()
36
+ let cleanup: (() => void) | null = null
37
+ watch(isOpen, (open) => {
38
+ if (open && reference.value && floating.value) {
39
+ cleanup = autoUpdate(reference.value, floating.value, update)
40
+ }
41
+ else {
42
+ cleanup?.()
43
+ cleanup = null
44
+ }
45
+ })
46
+ onUnmounted(() => cleanup?.())
25
47
 
26
- const testIdSuffix = computed(() => props.isMobile ? '-mobile' : '')
48
+ onClickOutside(wrapper, () => {
49
+ if (isOpen.value) isOpen.value = false
50
+ })
27
51
 
28
- function onClick(loc: any) {
29
- if ((loc?.code || loc) === locale.value) return
52
+ function localeCode(loc: LocaleItem): string {
53
+ return typeof loc === 'string' ? loc : loc.code
54
+ }
30
55
 
56
+ function onSelect(loc: LocaleItem) {
31
57
  isOpen.value = false
32
- window.location.assign(switchLocalePath(loc?.code || loc))
58
+ const code = localeCode(loc)
59
+ if (code === locale.value) return
60
+ window.location.assign(switchLocalePath(code))
33
61
  }
34
62
  </script>
35
63
 
36
64
  <template>
37
- <div
38
- ref="langSwitcher"
39
- class="relative"
40
- >
65
+ <div ref="wrapper" class="relative">
41
66
  <button
42
- class="text-xs font-semibold py-1 px-2 bg-gray-100 dark:bg-gray-200/5 rounded"
43
- @click="isOpen = true"
44
- aria-hidden="true"
67
+ ref="reference"
68
+ type="button"
69
+ class="inline-flex size-8 items-center justify-center rounded-xl text-xs font-semibold text-gray-900 transition-colors duration-150 hover:bg-deepblue-900/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:ring-offset-1 dark:text-gray-100 dark:hover:bg-white/10 dark:focus-visible:ring-offset-gray-900"
70
+ :class="[
71
+ isOpen
72
+ ? 'bg-blue-50 text-blue-700 ring-2 ring-blue-500/20 dark:bg-blue-900/40 dark:text-blue-300'
73
+ : '',
74
+ ]"
45
75
  :data-test-id="`header-switch-language-button${testIdSuffix}`"
76
+ :aria-expanded="isOpen"
77
+ aria-haspopup="listbox"
78
+ @click="isOpen = !isOpen"
46
79
  >
47
80
  {{ t(`lang.${locale}.short`) }}
48
81
  </button>
49
- <div
50
- v-show="isOpen"
51
- :class="['absolute z-20 bg-white shadow rounded-md p-2 right-0 lg:right-auto lg:left-0 left-auto flex flex-col dark:bg-gray-900']"
82
+
83
+ <transition
84
+ enter-active-class="lang-switcher__dropdown-enter-active"
85
+ enter-from-class="lang-switcher__dropdown-enter-from"
86
+ enter-to-class="lang-switcher__dropdown-enter-to"
87
+ leave-active-class="lang-switcher__dropdown-leave-active"
88
+ leave-from-class="lang-switcher__dropdown-leave-from"
89
+ leave-to-class="lang-switcher__dropdown-leave-to"
52
90
  >
53
- <nuxt-link
54
- v-for="loc in locales"
55
- :data-test-id="`header-switch-${loc?.code}-language-button${testIdSuffix}`"
56
- :class="[
57
- 'text-sm cursor-pointer px-4 py-2.5 flex justify-between',
58
- {'text-primary bg-deepblue-500/5': locale === loc.code}
59
- ]"
60
- :to="switchLocalePath(loc?.code || loc)"
61
- @click.prevent="onClick(loc)"
91
+ <div
92
+ v-if="isOpen"
93
+ ref="floating"
94
+ class="z-[10000] w-fit rounded-xl border border-gray-200 bg-white shadow-lg shadow-gray-900/10 dark:border-gray-700 dark:bg-gray-900 dark:shadow-black/30"
95
+ :style="{
96
+ position: strategy,
97
+ left: x != null ? `${x}px` : '',
98
+ top: y != null ? `${y}px` : '',
99
+ }"
62
100
  >
63
- {{ t(`lang.${loc?.code || loc}.long`) }}
64
- </nuxt-link>
65
- </div>
101
+ <ul class="flex flex-col gap-1 p-1.5" role="listbox">
102
+ <li v-for="loc in locales" :key="localeCode(loc)" role="option" :aria-selected="localeCode(loc) === locale">
103
+ <nuxt-link
104
+ :data-test-id="`header-switch-${localeCode(loc)}-language-button${testIdSuffix}`"
105
+ :to="switchLocalePath(localeCode(loc))"
106
+ class="flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm transition-colors duration-100"
107
+ :class="[
108
+ localeCode(loc) === locale
109
+ ? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
110
+ : 'text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800/60',
111
+ ]"
112
+ @click.prevent="onSelect(loc)"
113
+ >
114
+ <span>{{ t(`lang.${localeCode(loc)}.long`) }}</span>
115
+ <check
116
+ v-if="localeCode(loc) === locale"
117
+ class="size-4 shrink-0 text-blue-600 dark:text-blue-400"
118
+ />
119
+ </nuxt-link>
120
+ </li>
121
+ </ul>
122
+ </div>
123
+ </transition>
66
124
  </div>
67
125
  </template>
68
126
 
69
- <style lang="scss" scoped>
70
- .shadow {
71
- box-shadow: 0 1px 8px 0 #8B929C4D;
127
+ <style scoped>
128
+ .lang-switcher__dropdown-enter-active,
129
+ .lang-switcher__dropdown-leave-active {
130
+ transition:
131
+ opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
132
+ transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
133
+ }
134
+
135
+ .lang-switcher__dropdown-enter-from,
136
+ .lang-switcher__dropdown-leave-to {
137
+ opacity: 0;
138
+ transform: translateY(-4px);
139
+ }
140
+
141
+ .lang-switcher__dropdown-enter-to,
142
+ .lang-switcher__dropdown-leave-from {
143
+ opacity: 1;
144
+ transform: translateY(0);
145
+ }
146
+
147
+ @media (prefers-reduced-motion: reduce) {
148
+ .lang-switcher__dropdown-enter-active,
149
+ .lang-switcher__dropdown-leave-active {
150
+ transition: none;
151
+ }
72
152
  }
73
153
  </style>
@@ -1,16 +1,10 @@
1
1
  <script setup lang="ts">
2
- import { PAGES } from '#adata-ui/shared/constans/pages'
3
2
  import { usePkServicesLinks } from '#adata-ui/composables/useHeaderNavigationLinks'
4
- import { useUrls } from '#adata-ui/composables/useUrls'
3
+ import { useActiveNavigation } from '#adata-ui/composables/useActiveNavigation'
5
4
  import { NuxtLinkLocale } from '#components'
6
5
 
7
6
  const services = usePkServicesLinks()
8
- const route = useRoute()
9
- const localePath = useLocalePath()
10
- const { landing } = useUrls()
11
- const { locale } = useI18n()
12
-
13
- const pageUrl = useRequestURL()
7
+ const { isActiveService } = useActiveNavigation()
14
8
 
15
9
  const blockStyles = [
16
10
  'first-border-gradient',
@@ -23,22 +17,6 @@ const blockStyles = [
23
17
  'eighth-border-gradient',
24
18
  'ninth-border-gradient',
25
19
  ]
26
- const linkByIndex = [
27
- PAGES.pk.main,
28
- PAGES.pk.employees,
29
- PAGES.pk.connections,
30
- PAGES.pk.offshore,
31
- PAGES.pk.foreign,
32
- PAGES.pk.unload,
33
- PAGES.pk.compare,
34
- PAGES.pk.sanctions,
35
- buildLocalizedUrl(locale, landing, '/all-services'),
36
- ]
37
-
38
- const normalize = (path: string) => {
39
- const cleaned = path.replace(/\/+$/, '')
40
- return cleaned === '' ? '/' : cleaned
41
- }
42
20
  </script>
43
21
 
44
22
  <template>
@@ -46,15 +24,15 @@ const normalize = (path: string) => {
46
24
  <component
47
25
  v-for="(service, index) in services"
48
26
  :key="index"
49
- :is="normalize(localePath(service.short)) === normalize(route.path) && pageUrl.hostname.startsWith('pk') ? 'div' : NuxtLinkLocale"
50
- :to="normalize(localePath(service.short)) === normalize(route.path) ? '' : service.to"
27
+ :is="isActiveService(service.to) ? 'div' : NuxtLinkLocale"
28
+ :to="isActiveService(service.to) ? '' : service.to"
51
29
  :class="['flex flex-col items-center gap-2 p-2', blockStyles[index]]"
52
30
  >
53
31
  <div
54
32
  class="size-10 p-2 rounded-lg"
55
33
  :class="[
56
34
  'bg-deepblue-900/5 dark:bg-gray-200/5',
57
- {'!bg-blue-700 text-white dark:!bg-blue-500 ': route.path.replace(/\/+$/, '') === localePath(linkByIndex[index]).replace(/\/+$/, '')}
35
+ {'!bg-blue-700 text-white dark:!bg-blue-500 ': isActiveService(service.to)}
58
36
  ]"
59
37
  >
60
38
  <component
@@ -10,7 +10,7 @@ const { t } = useI18n()
10
10
  <nuxt-link
11
11
  :to="url"
12
12
  target="_blank"
13
- class="flex cursor-pointer items-center justify-center gap-1 rounded-md bg-blue-700/10 px-3 py-2 text-blue-700"
13
+ class="flex cursor-pointer items-center justify-center gap-1 rounded-md bg-blue-700/10 px-3 py-2 text-blue-700 transition-colors duration-150 hover:bg-blue-700/15 dark:bg-blue-400/10 dark:text-blue-300 dark:hover:bg-blue-400/20"
14
14
  >
15
15
  <span class="body-400">{{ t('header.oldVersion') }}</span>
16
16
  <a-icon-arrow-bottom-left-on-square class="rotate-180" />