adata-ui 2.1.40-beta.18 → 2.1.40-beta.19

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 (211) hide show
  1. package/.playground/app.vue +102 -0
  2. package/assets/styles/index.scss +67 -5
  3. package/components/elements/a-select-row/ASelectRowV2.vue +214 -0
  4. package/components/elements/a-select-row/index.vue +3 -5
  5. package/components/elements/accordion/AAccordion.vue +5 -3
  6. package/components/elements/accordion/AAccordionGroup/AAccordionGroup.vue +3 -3
  7. package/components/elements/alert/AAlert.vue +34 -47
  8. package/components/elements/bg-block/ABgBlock.vue +1 -1
  9. package/components/elements/button/AButton.vue +3 -3
  10. package/components/elements/button/AButtonV2.vue +133 -0
  11. package/components/elements/button-login/index.vue +6 -10
  12. package/components/elements/chip-wrapper/AChipWrapper.vue +1 -1
  13. package/components/elements/chips/AChips.vue +2 -2
  14. package/components/elements/companies/AOurClients.vue +3 -1
  15. package/components/elements/curve-block-v2/ACurveBlockV2.vue +85 -0
  16. package/components/elements/digit-badge/ADigitBadge.vue +24 -22
  17. package/components/elements/feature-description/AFeatureDescription.vue +1 -1
  18. package/components/elements/leave-note/ALeaveNote.vue +39 -18
  19. package/components/elements/pagination/APagination.vue +7 -7
  20. package/components/elements/segmented/ASegmentedV2.vue +151 -0
  21. package/components/elements/select/ASelect.vue +4 -4
  22. package/components/elements/select/ASelectMobile.vue +2 -2
  23. package/components/elements/select/ASelectV2.vue +588 -0
  24. package/components/elements/select/ui/select-button.vue +1 -1
  25. package/components/elements/show-more/AShowMoreV2.vue +26 -0
  26. package/components/elements/slider/ASlider.vue +174 -0
  27. package/components/elements/slider/ASliderItem.vue +31 -0
  28. package/components/elements/slider/types.ts +21 -0
  29. package/components/elements/star-rating/AStarRating.vue +12 -4
  30. package/components/elements/status-badge/AStatusBadge.vue +91 -44
  31. package/components/elements/step-slider/AStepSliderColCard.vue +232 -0
  32. package/components/elements/step-slider/AStepSliderColNumber.vue +178 -0
  33. package/components/elements/table/ATable.vue +1 -1
  34. package/components/elements/table/table.config.ts +2 -2
  35. package/components/elements/tree-select/ATreeSelect.vue +97 -113
  36. package/components/elements/tree-select/TreeService.ts +142 -207
  37. package/components/elements/tree-select/components/ATreeSelectNode.vue +104 -0
  38. package/components/elements/tree-select/types.ts +40 -11
  39. package/components/elements/tree-select/utils.ts +30 -0
  40. package/components/elements/tree-select-modal/ATreeSelectModal.vue +217 -0
  41. package/components/features/color-mode/AColorMode.client.vue +74 -32
  42. package/components/features/dropdown/ADropdownV2.vue +140 -0
  43. package/components/features/lang-switcher/lang-switcher.vue +125 -40
  44. package/components/features/payment/banner/BasicPlusLimitBanner.vue +3 -3
  45. package/components/features/payment/banner/PaymentBanner.vue +3 -3
  46. package/components/features/payment/banner/UpSellBanner.vue +3 -3
  47. package/components/features/payment/process/PaymentKaspiQrSidePanel.vue +45 -27
  48. package/components/features/payment/process/PaymentKaspiRedirectSidePanel.vue +34 -23
  49. package/components/features/payment/process/PaymentMethodSidePanel.vue +66 -49
  50. package/components/features/payment/process/PaymentProcess.vue +6 -6
  51. package/components/features/payment/process/PaymentTopUpSidePanel.vue +49 -63
  52. package/components/features/pk-mobile-services/APkMobileServices.vue +6 -28
  53. package/components/features//321/201hange-version/AChangeVersion.vue +2 -2
  54. package/components/forms/checkbox/ACheckbox.vue +1 -1
  55. package/components/forms/checkbox/ACheckboxV2.vue +229 -0
  56. package/components/forms/feedback-form/FeedbackForm.vue +8 -8
  57. package/components/forms/feedback-form/FeedbackFormV2.vue +182 -0
  58. package/components/forms/input/AInputV2.vue +542 -0
  59. package/components/forms/input/date/AInputDate.vue +2 -2
  60. package/components/forms/input/password/AInputPassword.vue +8 -2
  61. package/components/forms/input/standard/AInputStandard.vue +3 -3
  62. package/components/forms/input/textarea/ATextarea.vue +2 -2
  63. package/components/forms/input/textarea/ATextareaV2.vue +248 -0
  64. package/components/forms/toggle/AToggle.vue +2 -3
  65. package/components/forms/toggle/AToggleV2.vue +71 -0
  66. package/components/modals/AnotherDeviceModal.vue +22 -18
  67. package/components/modals/ApplicationAcceptedModal.vue +12 -15
  68. package/components/modals/ConnectingTariffModal.vue +13 -20
  69. package/components/modals/ContactsMobileModel.vue +1 -1
  70. package/components/modals/ContentNavigationModal.vue +77 -333
  71. package/components/modals/Insufficient-funds-modal.vue +18 -33
  72. package/components/modals/LimitReachedModal.vue +17 -14
  73. package/components/modals/NoAccessModal.vue +9 -11
  74. package/components/modals/PaymentMethodModal.vue +1 -1
  75. package/components/modals/ReportBugConfirmModal.vue +12 -10
  76. package/components/modals/ReportBugModal.vue +46 -38
  77. package/components/modals/Resend.vue +9 -10
  78. package/components/modals/SubmitApplicationModal.vue +27 -24
  79. package/components/modals/id/IdBanner.vue +3 -3
  80. package/components/modals/id/IdConfirmAccountOtpModal.vue +4 -9
  81. package/components/modals/id/IdLoginModal.vue +3 -8
  82. package/components/modals/id/IdModals.vue +23 -10
  83. package/components/modals/id/IdRecoveryModal.vue +1 -1
  84. package/components/modals/id/IdTwoFactorModal.vue +3 -8
  85. package/components/navigation/bottom-navigation/ABottomNavigation.vue +1 -1
  86. package/components/navigation/breadcrumbs/ABreadcrumbs.vue +106 -45
  87. package/components/navigation/breadcrumbs/types.ts +3 -0
  88. package/components/navigation/footer/AFooter.vue +39 -40
  89. package/components/navigation/footer/ui/a-footer-accordion.vue +9 -9
  90. package/components/navigation/header/AHeader.vue +68 -51
  91. package/components/navigation/header/AlmatyContacts.vue +78 -78
  92. package/components/navigation/header/CardGallery.vue +9 -7
  93. package/components/navigation/header/ContactMenu.vue +26 -92
  94. package/components/navigation/header/HeaderLink.vue +181 -213
  95. package/components/navigation/header/HeaderUsage.vue +139 -0
  96. package/components/navigation/header/ListItem.vue +2 -2
  97. package/components/navigation/header/NavCard.vue +1 -1
  98. package/components/navigation/header/NavList.vue +33 -92
  99. package/components/navigation/header/NotificationsMenu.vue +2 -3
  100. package/components/navigation/header/ProductMenu.vue +81 -127
  101. package/components/navigation/header/ProfileMenu.vue +122 -151
  102. package/components/navigation/header/SystemNotification.vue +108 -0
  103. package/components/navigation/mobile-navigation/AMobileNavigation.vue +23 -15
  104. package/components/navigation/pill-tabs/APillTabs.vue +2 -2
  105. package/components/navigation/pill-tabs/APillTabsV2.vue +226 -0
  106. package/components/overlays/drawer/ADrawerV2.vue +335 -0
  107. package/components/overlays/modal/AModal.vue +4 -4
  108. package/components/overlays/modal/AModalV2.vue +397 -0
  109. package/components/overlays/side-panel/ASidePanel.vue +10 -7
  110. package/components/overlays/sideover/ASlideover.vue +6 -6
  111. package/components/overlays/tooltip/ATooltip.vue +1 -1
  112. package/components/overlays/tooltip/ATooltipV2.vue +223 -0
  113. package/components/overlays/tooltip/types.ts +26 -0
  114. package/components/overlays/tooltip/useTooltipTrigger.ts +101 -0
  115. package/components/transitions/ATransitionHeight.vue +1 -1
  116. package/composables/useAccessTokenCookie.ts +25 -0
  117. package/composables/useActiveNavigation.ts +84 -0
  118. package/composables/useChipOverflow.ts +92 -0
  119. package/composables/useHeaderNavigationLinks.ts +140 -98
  120. package/icons/archive.vue +16 -0
  121. package/icons/arrow/arrow-left-right.vue +8 -0
  122. package/icons/arrow/arrow-right.vue +14 -0
  123. package/icons/arrow/arrows-in.vue +11 -0
  124. package/icons/arrow/arrows-out.vue +11 -0
  125. package/icons/arrow/arrows-resize-in.vue +38 -0
  126. package/icons/arrow/arrows-resize-out.vue +17 -0
  127. package/icons/bill.vue +3 -0
  128. package/icons/bot.vue +52 -0
  129. package/icons/briefcase.vue +14 -0
  130. package/icons/building.vue +9 -0
  131. package/icons/car-side.vue +16 -0
  132. package/icons/cart.vue +15 -0
  133. package/icons/chart-no-axes-column.vue +3 -0
  134. package/icons/clock-time.vue +24 -0
  135. package/icons/cloud/cloud-upload.vue +3 -0
  136. package/icons/code.vue +9 -0
  137. package/icons/compare-outlined.vue +8 -0
  138. package/icons/dots-horizontal.vue +31 -0
  139. package/icons/download-line.vue +13 -0
  140. package/icons/download-rounded.vue +15 -0
  141. package/icons/egov.vue +41 -0
  142. package/icons/elicense.vue +38 -0
  143. package/icons/favourite-bookmark-outline.vue +17 -0
  144. package/icons/file/file-check-corner.vue +3 -0
  145. package/icons/file/file-spreadsheet.vue +3 -0
  146. package/icons/file-pen.vue +31 -0
  147. package/icons/file-stack.vue +31 -0
  148. package/icons/file-types/excel.vue +131 -0
  149. package/icons/file-types/html.vue +13 -0
  150. package/icons/file-types/jpeg.vue +16 -0
  151. package/icons/file-types/pdf.vue +11 -0
  152. package/icons/file-types/png.vue +11 -0
  153. package/icons/file-types/rar.vue +11 -0
  154. package/icons/file-types/word.vue +50 -0
  155. package/icons/file-types/zip.vue +11 -0
  156. package/icons/gauge.vue +17 -0
  157. package/icons/git-compare.vue +38 -0
  158. package/icons/git-fork.vue +3 -0
  159. package/icons/graduation-cap.vue +31 -0
  160. package/icons/graph-loading.vue +34 -0
  161. package/icons/headset.vue +6 -0
  162. package/icons/help-question-circle.vue +26 -0
  163. package/icons/in-row-type.vue +46 -0
  164. package/icons/in-table-type.vue +29 -0
  165. package/icons/landmark.vue +3 -0
  166. package/icons/layers.vue +3 -0
  167. package/icons/letter/letter-a.vue +6 -0
  168. package/icons/letter/letter-b.vue +6 -0
  169. package/icons/letter/letter-c.vue +6 -0
  170. package/icons/letter/letter-d.vue +6 -0
  171. package/icons/letter/letter-e.vue +6 -0
  172. package/icons/link-url.vue +3 -0
  173. package/icons/list-tree.vue +3 -0
  174. package/icons/mail-question-mark.vue +8 -0
  175. package/icons/message/message-square.vue +3 -0
  176. package/icons/message-dotted.vue +30 -0
  177. package/icons/network.vue +3 -0
  178. package/icons/newspaper.vue +3 -0
  179. package/icons/notification-error.vue +17 -0
  180. package/icons/person.vue +22 -0
  181. package/icons/phone-call.vue +6 -0
  182. package/icons/phone-reversed.vue +3 -0
  183. package/icons/route.vue +7 -0
  184. package/icons/scan-search.vue +3 -0
  185. package/icons/settings.vue +26 -0
  186. package/icons/shield-alert.vue +31 -0
  187. package/icons/shield-verified.vue +14 -0
  188. package/icons/socials/facebook-color.vue +16 -0
  189. package/icons/socials/globe-color.vue +16 -0
  190. package/icons/socials/instagram-color.vue +46 -0
  191. package/icons/socials/linkedin-color.vue +14 -0
  192. package/icons/socials/telegram-color.vue +16 -0
  193. package/icons/socials/tiktok-color.vue +14 -0
  194. package/icons/socials/vk-color.vue +16 -0
  195. package/icons/socials/youtube-color.vue +16 -0
  196. package/icons/sparkles.vue +3 -0
  197. package/icons/sun.vue +13 -3
  198. package/icons/users-round.vue +3 -0
  199. package/icons/view-eye-open.vue +13 -0
  200. package/icons/workflow.vue +3 -0
  201. package/icons/zap.vue +3 -0
  202. package/lang/en.ts +49 -5
  203. package/lang/kk.ts +50 -6
  204. package/lang/ru.ts +51 -7
  205. package/package.json +1 -1
  206. package/plugins/toast.client.ts +38 -55
  207. package/shared/constans/pages.ts +8 -2
  208. package/tailwind.config.ts +147 -116
  209. package/utils/twMerge.ts +34 -0
  210. package/components/elements/tree-select/components/tree-select-nodes.vue +0 -91
  211. package/components/navigation/header/TopHeader.vue +0 -196
@@ -0,0 +1,217 @@
1
+ <script lang="ts" setup>
2
+ import type { TreeData, TreeViewNode } from '#adata-ui/components/elements/tree-select/types'
3
+ import { getSelectedItems } from '#adata-ui/components/elements/tree-select/utils'
4
+ import AIconSearch from '#adata-ui/icons/search.vue'
5
+
6
+ type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'container' | 'fullscreen'
7
+
8
+ interface Props {
9
+ options: TreeData[]
10
+ label?: string
11
+ title?: string
12
+ searchLabel?: string
13
+ type?: 'checkbox' | 'select'
14
+ onlyOne?: boolean
15
+ withoutId?: boolean
16
+ loading?: boolean
17
+ disabled?: boolean
18
+ asyncSearch?: (keyword: string) => TreeData[] | Promise<TreeData[]>
19
+ filteringLabel?: string
20
+ applyLabel?: string
21
+ resetLabel?: string
22
+ btnClass?: string
23
+ size?: ModalSize
24
+ width?: string
25
+ bodyHeight?: string
26
+ idKey?: string
27
+ nameKey?: string
28
+ childrenKey?: string
29
+ hasChildrenKey?: string
30
+ lazy?: boolean
31
+ loadChildren?: (node: TreeViewNode) => TreeData[] | Promise<TreeData[]>
32
+ }
33
+
34
+ const props = withDefaults(defineProps<Props>(), {
35
+ label: '',
36
+ title: '',
37
+ searchLabel: '',
38
+ type: 'checkbox',
39
+ onlyOne: false,
40
+ withoutId: false,
41
+ loading: false,
42
+ disabled: false,
43
+ filteringLabel: '',
44
+ applyLabel: 'Применить',
45
+ resetLabel: 'Сбросить',
46
+ btnClass: '',
47
+ size: 'lg',
48
+ bodyHeight: '50vh',
49
+ idKey: 'id',
50
+ nameKey: 'name',
51
+ childrenKey: 'children',
52
+ lazy: false,
53
+ })
54
+
55
+ const emit = defineEmits<{
56
+ (e: 'submit'): void
57
+ (e: 'cancel'): void
58
+ }>()
59
+
60
+ const selected = defineModel<(string | number)[]>({ default: () => [] })
61
+ const search = defineModel<string>('search', { default: '' })
62
+ const isOpen = defineModel<boolean>('open', { default: false })
63
+
64
+ const treeRef = ref<{ reset: () => void } | null>(null)
65
+ const isSearch = ref(false)
66
+
67
+ const count = computed(() => selected.value?.length ?? 0)
68
+
69
+ const dataForDisplay = computed(() =>
70
+ getSelectedItems(props.options, selected.value ?? [], {
71
+ idKey: props.idKey,
72
+ nameKey: props.nameKey,
73
+ childrenKey: props.childrenKey,
74
+ }),
75
+ )
76
+
77
+ const treeAreaStyle = computed(() =>
78
+ props.bodyHeight && props.bodyHeight !== 'auto' ? { height: props.bodyHeight } : undefined,
79
+ )
80
+
81
+ function openModal() {
82
+ if (props.disabled) return
83
+ isOpen.value = true
84
+ }
85
+
86
+ function onApply() {
87
+ isOpen.value = false
88
+ search.value = ''
89
+ nextTick(() => emit('submit'))
90
+ }
91
+
92
+ function onReset() {
93
+ treeRef.value?.reset()
94
+ selected.value = []
95
+ search.value = ''
96
+ emit('cancel')
97
+ }
98
+
99
+ watch(
100
+ () => props.loading,
101
+ (loading) => {
102
+ if (!loading) isSearch.value = !!search.value.length
103
+ },
104
+ )
105
+
106
+ defineExpose({ open: openModal, close: () => (isOpen.value = false) })
107
+ </script>
108
+
109
+ <template>
110
+ <div class="tree-select-modal w-full">
111
+ <slot
112
+ name="trigger"
113
+ :open="openModal"
114
+ :selected="dataForDisplay"
115
+ :count="count"
116
+ >
117
+ <a-select-row-v2
118
+ v-model:is-open="isOpen"
119
+ :label="label"
120
+ :selected-items="dataForDisplay"
121
+ :disabled="disabled"
122
+ :btn-class="btnClass"
123
+ :clearable="false"
124
+ :deletable="false"
125
+ />
126
+ </slot>
127
+
128
+ <a-modal-v2
129
+ v-model="isOpen"
130
+ :title="title"
131
+ :size="size"
132
+ :width="width"
133
+ >
134
+ <template #header>
135
+ <slot name="header" :title="title">
136
+ <h2 class="text-h5 text-gray-900 dark:text-gray-100">
137
+ {{ title }}
138
+ </h2>
139
+ </slot>
140
+ </template>
141
+
142
+ <div class="flex flex-col">
143
+ <div class="sticky top-0 z-[1] flex flex-col gap-4 bg-white px-5 pb-4 pt-4 dark:bg-gray-900">
144
+ <a-input-v2
145
+ v-model="search"
146
+ type="search"
147
+ :label="searchLabel"
148
+ :start-icon="AIconSearch"
149
+ clearable
150
+ />
151
+
152
+ <p v-if="filteringLabel" class="text-body-sm text-gray-500 dark:text-gray-400">
153
+ {{ filteringLabel }}
154
+ </p>
155
+ </div>
156
+
157
+ <div
158
+ class="tree-select-modal__body px-5 pb-4"
159
+ :class="{ 'overflow-auto': treeAreaStyle }"
160
+ :style="treeAreaStyle"
161
+ >
162
+ <a-tree-select
163
+ v-if="!loading"
164
+ ref="treeRef"
165
+ v-model="selected"
166
+ :options="options"
167
+ :type="type"
168
+ :only-one="onlyOne"
169
+ :is-search="isSearch"
170
+ :search-text="search"
171
+ :async-search="asyncSearch"
172
+ :without-id="withoutId"
173
+ :id-key="idKey"
174
+ :name-key="nameKey"
175
+ :children-key="childrenKey"
176
+ :has-children-key="hasChildrenKey"
177
+ :lazy="lazy"
178
+ :load-children="loadChildren"
179
+ >
180
+ <template v-if="$slots.label" #label="slotProps">
181
+ <slot name="label" v-bind="slotProps" />
182
+ </template>
183
+ </a-tree-select>
184
+
185
+ <div v-else class="flex items-center justify-center py-10 text-gray-400 dark:text-gray-500">
186
+ <a-icon-loader-circle class="size-6 animate-spin" />
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ <template #footer="{ close }">
192
+ <slot
193
+ name="footer"
194
+ :reset="onReset"
195
+ :apply="onApply"
196
+ :close="close"
197
+ :count="count"
198
+ >
199
+ <div class="flex justify-end gap-2">
200
+ <a-button-v2
201
+ view="outline"
202
+ color="gray"
203
+ @click="onReset"
204
+ >
205
+ {{ resetLabel }}
206
+ </a-button-v2>
207
+ <a-button-v2 @click="onApply">
208
+ {{ applyLabel }}<template v-if="count">
209
+ ({{ count }})
210
+ </template>
211
+ </a-button-v2>
212
+ </div>
213
+ </slot>
214
+ </template>
215
+ </a-modal-v2>
216
+ </div>
217
+ </template>
@@ -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-lg 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,140 @@
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
+ const transformOrigin = computed(() => {
28
+ const [side, align] = resolvedPlacement.value.split('-')
29
+ const vertical = side === 'top' ? 'bottom' : 'top'
30
+ const horizontal = align === 'start' ? 'left' : align === 'end' ? 'right' : 'center'
31
+ return `${vertical} ${horizontal}`
32
+ })
33
+
34
+ let cleanup: (() => void) | null = null
35
+ watch(isOpen, (open) => {
36
+ if (open && reference.value && floating.value) {
37
+ cleanup = autoUpdate(reference.value, floating.value, update)
38
+ }
39
+ else {
40
+ cleanup?.()
41
+ cleanup = null
42
+ }
43
+ })
44
+ onUnmounted(() => cleanup?.())
45
+
46
+ function toggle() {
47
+ isOpen.value = !isOpen.value
48
+ }
49
+ function close() {
50
+ isOpen.value = false
51
+ }
52
+
53
+ onClickOutside(wrapper, () => {
54
+ if (isOpen.value) close()
55
+ })
56
+ onKeyStroke('Escape', () => {
57
+ if (isOpen.value) close()
58
+ })
59
+ </script>
60
+
61
+ <template>
62
+ <div
63
+ ref="wrapper"
64
+ class="relative"
65
+ >
66
+ <div
67
+ ref="reference"
68
+ class="inline-flex"
69
+ >
70
+ <slot
71
+ :toggle="toggle"
72
+ :is-open="isOpen"
73
+ />
74
+ </div>
75
+
76
+ <transition
77
+ enter-active-class="a-dropdown-v2__enter-active"
78
+ enter-from-class="a-dropdown-v2__enter-from"
79
+ enter-to-class="a-dropdown-v2__enter-to"
80
+ leave-active-class="a-dropdown-v2__leave-active"
81
+ leave-from-class="a-dropdown-v2__leave-from"
82
+ leave-to-class="a-dropdown-v2__leave-to"
83
+ >
84
+ <div
85
+ v-if="isOpen"
86
+ ref="floating"
87
+ class="z-[60] overflow-hidden rounded-lg shadow-lg shadow-gray-900/10 dark:shadow-black/30"
88
+ :style="{
89
+ position: strategy,
90
+ left: x != null ? `${x}px` : '',
91
+ top: y != null ? `${y}px` : '',
92
+ transformOrigin,
93
+ }"
94
+ >
95
+ <slot
96
+ name="content"
97
+ :close="close"
98
+ />
99
+ </div>
100
+ </transition>
101
+ </div>
102
+ </template>
103
+
104
+ <style scoped>
105
+ .a-dropdown-v2__enter-active {
106
+ transition:
107
+ opacity 200ms ease,
108
+ transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
109
+ }
110
+
111
+ .a-dropdown-v2__leave-active {
112
+ transition:
113
+ opacity 140ms ease,
114
+ transform 160ms cubic-bezier(0.4, 0, 1, 1);
115
+ }
116
+
117
+ .a-dropdown-v2__enter-from,
118
+ .a-dropdown-v2__leave-to {
119
+ opacity: 0;
120
+ transform: translateY(-8px) scale(0.96);
121
+ }
122
+
123
+ .a-dropdown-v2__enter-to,
124
+ .a-dropdown-v2__leave-from {
125
+ opacity: 1;
126
+ transform: translateY(0) scale(1);
127
+ }
128
+
129
+ @media (prefers-reduced-motion: reduce) {
130
+ .a-dropdown-v2__enter-active,
131
+ .a-dropdown-v2__leave-active {
132
+ transition: opacity 120ms ease;
133
+ }
134
+
135
+ .a-dropdown-v2__enter-from,
136
+ .a-dropdown-v2__leave-to {
137
+ transform: none;
138
+ }
139
+ }
140
+ </style>
@@ -1,73 +1,158 @@
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-lg text-body-sm 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-lg 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
103
+ v-for="loc in locales"
104
+ :key="localeCode(loc)"
105
+ role="option"
106
+ :aria-selected="localeCode(loc) === locale"
107
+ >
108
+ <nuxt-link
109
+ :data-test-id="`header-switch-${localeCode(loc)}-language-button${testIdSuffix}`"
110
+ :to="switchLocalePath(localeCode(loc))"
111
+ class="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-body-md transition-colors duration-100"
112
+ :class="[
113
+ localeCode(loc) === locale
114
+ ? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
115
+ : 'text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800/60',
116
+ ]"
117
+ @click.prevent="onSelect(loc)"
118
+ >
119
+ <span>{{ t(`lang.${localeCode(loc)}.long`) }}</span>
120
+ <check
121
+ v-if="localeCode(loc) === locale"
122
+ class="size-4 shrink-0 text-blue-600 dark:text-blue-400"
123
+ />
124
+ </nuxt-link>
125
+ </li>
126
+ </ul>
127
+ </div>
128
+ </transition>
66
129
  </div>
67
130
  </template>
68
131
 
69
- <style lang="scss" scoped>
70
- .shadow {
71
- box-shadow: 0 1px 8px 0 #8B929C4D;
132
+ <style scoped>
133
+ .lang-switcher__dropdown-enter-active,
134
+ .lang-switcher__dropdown-leave-active {
135
+ transition:
136
+ opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
137
+ transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
138
+ }
139
+
140
+ .lang-switcher__dropdown-enter-from,
141
+ .lang-switcher__dropdown-leave-to {
142
+ opacity: 0;
143
+ transform: translateY(-4px);
144
+ }
145
+
146
+ .lang-switcher__dropdown-enter-to,
147
+ .lang-switcher__dropdown-leave-from {
148
+ opacity: 1;
149
+ transform: translateY(0);
150
+ }
151
+
152
+ @media (prefers-reduced-motion: reduce) {
153
+ .lang-switcher__dropdown-enter-active,
154
+ .lang-switcher__dropdown-leave-active {
155
+ transition: none;
156
+ }
72
157
  }
73
158
  </style>