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
package/.nuxtrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
typescript.includeWorkspace = true
|
|
1
|
+
typescript.includeWorkspace = true
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export default defineAppConfig({
|
|
2
|
-
myLayer: {
|
|
3
|
-
name: 'My amazing Nuxt layer (overwritten)'
|
|
4
|
-
}
|
|
5
|
-
})
|
|
1
|
+
export default defineAppConfig({
|
|
2
|
+
myLayer: {
|
|
3
|
+
name: 'My amazing Nuxt layer (overwritten)'
|
|
4
|
+
}
|
|
5
|
+
})
|
|
@@ -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>
|
package/README.md
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
# Adata UI with Nuxt 3 using Layers
|
|
2
|
-
|
|
3
|
-
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
|
|
7
|
-
Make sure to install the dependencies:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
# npm
|
|
11
|
-
npm install
|
|
12
|
-
|
|
13
|
-
# pnpm
|
|
14
|
-
pnpm install
|
|
15
|
-
|
|
16
|
-
# yarn
|
|
17
|
-
yarn install
|
|
18
|
-
|
|
19
|
-
# bun
|
|
20
|
-
bun install
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Development Server
|
|
24
|
-
|
|
25
|
-
Start the development server on `https://localhost:3000`:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
# npm
|
|
29
|
-
npm run dev
|
|
30
|
-
|
|
31
|
-
# pnpm
|
|
32
|
-
pnpm run dev
|
|
33
|
-
|
|
34
|
-
# yarn
|
|
35
|
-
yarn dev
|
|
36
|
-
|
|
37
|
-
# bun
|
|
38
|
-
bun run dev
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Production
|
|
42
|
-
|
|
43
|
-
Build the application for production:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
# npm
|
|
47
|
-
npm run build
|
|
48
|
-
|
|
49
|
-
# pnpm
|
|
50
|
-
pnpm run build
|
|
51
|
-
|
|
52
|
-
# yarn
|
|
53
|
-
yarn build
|
|
54
|
-
|
|
55
|
-
# bun
|
|
56
|
-
bun run build
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Locally preview production build:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
# npm
|
|
63
|
-
npm run preview
|
|
64
|
-
|
|
65
|
-
# pnpm
|
|
66
|
-
pnpm run preview
|
|
67
|
-
|
|
68
|
-
# yarn
|
|
69
|
-
yarn preview
|
|
70
|
-
|
|
71
|
-
# bun
|
|
72
|
-
bun run preview
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
1
|
+
# Adata UI with Nuxt 3 using Layers
|
|
2
|
+
|
|
3
|
+
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Make sure to install the dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# npm
|
|
11
|
+
npm install
|
|
12
|
+
|
|
13
|
+
# pnpm
|
|
14
|
+
pnpm install
|
|
15
|
+
|
|
16
|
+
# yarn
|
|
17
|
+
yarn install
|
|
18
|
+
|
|
19
|
+
# bun
|
|
20
|
+
bun install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Development Server
|
|
24
|
+
|
|
25
|
+
Start the development server on `https://localhost:3000`:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# npm
|
|
29
|
+
npm run dev
|
|
30
|
+
|
|
31
|
+
# pnpm
|
|
32
|
+
pnpm run dev
|
|
33
|
+
|
|
34
|
+
# yarn
|
|
35
|
+
yarn dev
|
|
36
|
+
|
|
37
|
+
# bun
|
|
38
|
+
bun run dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Production
|
|
42
|
+
|
|
43
|
+
Build the application for production:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# npm
|
|
47
|
+
npm run build
|
|
48
|
+
|
|
49
|
+
# pnpm
|
|
50
|
+
pnpm run build
|
|
51
|
+
|
|
52
|
+
# yarn
|
|
53
|
+
yarn build
|
|
54
|
+
|
|
55
|
+
# bun
|
|
56
|
+
bun run build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Locally preview production build:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# npm
|
|
63
|
+
npm run preview
|
|
64
|
+
|
|
65
|
+
# pnpm
|
|
66
|
+
pnpm run preview
|
|
67
|
+
|
|
68
|
+
# yarn
|
|
69
|
+
yarn preview
|
|
70
|
+
|
|
71
|
+
# bun
|
|
72
|
+
bun run preview
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
# button, alerts, dropdown
|
|
1
|
+
# button, alerts, dropdown
|
|
@@ -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>
|
|
@@ -81,7 +81,11 @@ const toggleExpand = (node) => {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const toggleCheckbox = (id, checked) => {
|
|
84
|
-
|
|
84
|
+
if (props.onlyOne && !checked) {
|
|
85
|
+
reset()
|
|
86
|
+
} else {
|
|
87
|
+
setState(id, checked ? 'checked' : 'unchecked')
|
|
88
|
+
}
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
function openFounded(nodes: TreeNestedNode[]) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
]"
|
|
7
7
|
>
|
|
8
8
|
<div class="flex items-center gap-1 p-2" v-if="!node?.hidden"
|
|
9
|
-
:class="{ 'bg-deepblue-50 dark:bg-gray-200/5': disabled }"
|
|
9
|
+
:class="{ 'bg-deepblue-50 dark:bg-gray-200/5': disabled && node.state === 'unchecked' }"
|
|
10
10
|
>
|
|
11
11
|
<div class="min-w-4">
|
|
12
12
|
<a-icon-chevron-down
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<input
|
|
22
22
|
:id="node.id.toString()"
|
|
23
23
|
type="checkbox"
|
|
24
|
-
:disabled="disabled"
|
|
24
|
+
:disabled="disabled && node.state === 'unchecked'"
|
|
25
25
|
class="hidden"
|
|
26
26
|
:checked="node.state === 'checked'"
|
|
27
27
|
@change="handleCheckboxChange"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
:name="`${node.id}`"
|
|
42
42
|
:intermediate="node.state === 'indeterminate'"
|
|
43
43
|
side="right"
|
|
44
|
-
:disabled="disabled"
|
|
44
|
+
:disabled="disabled && node.state === 'unchecked'"
|
|
45
45
|
always-intermediate
|
|
46
46
|
@change="handleCheckboxChange"
|
|
47
47
|
>
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
:toggle-expand="toggleExpand"
|
|
58
58
|
:toggle-checkbox="toggleCheckbox"
|
|
59
59
|
:type="type"
|
|
60
|
+
:disabled="disabled"
|
|
60
61
|
/>
|
|
61
62
|
</template>
|
|
62
63
|
</ul>
|
|
@@ -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>
|