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.
- package/.playground/app.vue +102 -0
- package/components/elements/button-login/index.vue +6 -10
- 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/pk-mobile-services/APkMobileServices.vue +5 -27
- package/components/features//321/201hange-version/AChangeVersion.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 +56 -91
- package/components/navigation/header/ProductMenu.vue +79 -127
- 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/tooltip/ATooltipV2.vue +233 -0
- package/components/overlays/tooltip/types.ts +26 -0
- package/components/overlays/tooltip/useTooltipTrigger.ts +101 -0
- package/composables/useActiveNavigation.ts +84 -0
- package/composables/useHeaderNavigationLinks.ts +14 -7
- 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/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 & 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
|
-
|
|
2
|
+
defineEmits(['click'])
|
|
3
3
|
|
|
4
4
|
const { t } = useI18n()
|
|
5
|
-
|
|
6
|
-
defineEmits(['click'])
|
|
7
5
|
</script>
|
|
8
6
|
|
|
9
7
|
<template>
|
|
10
|
-
<
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
3
|
-
import Sun from
|
|
4
|
-
import
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
onClickOutside(wrapper, () => {
|
|
49
|
+
if (isOpen.value) isOpen.value = false
|
|
50
|
+
})
|
|
27
51
|
|
|
28
|
-
function
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
<
|
|
54
|
-
v-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
70
|
-
.
|
|
71
|
-
|
|
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 {
|
|
3
|
+
import { useActiveNavigation } from '#adata-ui/composables/useActiveNavigation'
|
|
5
4
|
import { NuxtLinkLocale } from '#components'
|
|
6
5
|
|
|
7
6
|
const services = usePkServicesLinks()
|
|
8
|
-
const
|
|
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="
|
|
50
|
-
: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 ':
|
|
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" />
|