adata-ui 2.1.40-beta.11 → 2.1.40-beta.12

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 (133) hide show
  1. package/assets/styles/index.scss +67 -5
  2. package/components/elements/a-select-row/ASelectRowV2.vue +8 -8
  3. package/components/elements/a-select-row/index.vue +3 -5
  4. package/components/elements/accordion/AAccordion.vue +5 -3
  5. package/components/elements/accordion/AAccordionGroup/AAccordionGroup.vue +3 -3
  6. package/components/elements/alert/AAlert.vue +34 -47
  7. package/components/elements/bg-block/ABgBlock.vue +1 -1
  8. package/components/elements/button/AButton.vue +3 -3
  9. package/components/elements/button/AButtonV2.vue +7 -5
  10. package/components/elements/button-login/index.vue +1 -1
  11. package/components/elements/chip-wrapper/AChipWrapper.vue +1 -1
  12. package/components/elements/chips/AChips.vue +2 -2
  13. package/components/elements/digit-badge/ADigitBadge.vue +21 -19
  14. package/components/elements/feature-description/AFeatureDescription.vue +1 -1
  15. package/components/elements/leave-note/ALeaveNote.vue +1 -1
  16. package/components/elements/pagination/APagination.vue +7 -7
  17. package/components/elements/segmented/ASegmentedV2.vue +103 -10
  18. package/components/elements/select/ASelect.vue +4 -4
  19. package/components/elements/select/ASelectMobile.vue +2 -2
  20. package/components/elements/select/ASelectV2.vue +10 -11
  21. package/components/elements/select/ui/select-button.vue +1 -1
  22. package/components/elements/status-badge/AStatusBadge.vue +22 -19
  23. package/components/elements/status-badge-v2/StatusBadgeV2.vue +1 -3
  24. package/components/elements/table/ATable.vue +1 -1
  25. package/components/elements/table/table.config.ts +2 -2
  26. package/components/elements/tree-select/ATreeSelect.vue +97 -113
  27. package/components/elements/tree-select/TreeService.ts +142 -207
  28. package/components/elements/tree-select/components/ATreeSelectNode.vue +104 -0
  29. package/components/elements/tree-select/types.ts +40 -11
  30. package/components/elements/tree-select/utils.ts +30 -0
  31. package/components/elements/tree-select-modal/ATreeSelectModal.vue +217 -0
  32. package/components/features/color-mode/AColorMode.client.vue +1 -1
  33. package/components/features/dropdown/ADropdownV2.vue +1 -2
  34. package/components/features/lang-switcher/lang-switcher.vue +9 -4
  35. package/components/features/payment/banner/BasicPlusLimitBanner.vue +3 -3
  36. package/components/features/payment/banner/PaymentBanner.vue +3 -3
  37. package/components/features/payment/banner/UpSellBanner.vue +3 -3
  38. package/components/features/payment/process/PaymentKaspiQrSidePanel.vue +3 -3
  39. package/components/features/payment/process/PaymentKaspiRedirectSidePanel.vue +1 -1
  40. package/components/features/payment/process/PaymentMethodSidePanel.vue +3 -3
  41. package/components/features/payment/process/PaymentTopUpSidePanel.vue +2 -2
  42. package/components/features/pk-mobile-services/APkMobileServices.vue +1 -1
  43. package/components/features//321/201hange-version/AChangeVersion.vue +2 -2
  44. package/components/forms/checkbox/ACheckbox.vue +1 -1
  45. package/components/forms/checkbox/ACheckboxV2.vue +5 -5
  46. package/components/forms/input/AInputV2.vue +4 -4
  47. package/components/forms/input/date/AInputDate.vue +2 -2
  48. package/components/forms/input/password/AInputPassword.vue +8 -2
  49. package/components/forms/input/standard/AInputStandard.vue +3 -3
  50. package/components/forms/input/textarea/ATextarea.vue +2 -2
  51. package/components/forms/toggle/AToggle.vue +2 -3
  52. package/components/forms/toggle/AToggleV2.vue +3 -3
  53. package/components/modals/AnotherDeviceModal.vue +1 -1
  54. package/components/modals/ContactsMobileModel.vue +1 -1
  55. package/components/modals/ContentNavigationModal.vue +3 -3
  56. package/components/modals/Insufficient-funds-modal.vue +1 -1
  57. package/components/modals/PaymentMethodModal.vue +1 -1
  58. package/components/modals/id/IdBanner.vue +3 -3
  59. package/components/modals/id/IdRecoveryModal.vue +1 -1
  60. package/components/navigation/breadcrumbs/ABreadcrumbs.vue +106 -45
  61. package/components/navigation/breadcrumbs/types.ts +3 -0
  62. package/components/navigation/footer/AFooter.vue +39 -40
  63. package/components/navigation/footer/ui/a-footer-accordion.vue +9 -9
  64. package/components/navigation/header/AHeader.vue +2 -4
  65. package/components/navigation/header/AlmatyContacts.vue +78 -78
  66. package/components/navigation/header/CardGallery.vue +4 -4
  67. package/components/navigation/header/HeaderUsage.vue +4 -6
  68. package/components/navigation/header/ListItem.vue +2 -2
  69. package/components/navigation/header/NavCard.vue +1 -1
  70. package/components/navigation/header/NavList.vue +2 -2
  71. package/components/navigation/header/NotificationsMenu.vue +2 -3
  72. package/components/navigation/header/ProductMenu.vue +1 -1
  73. package/components/navigation/header/ProfileMenu.vue +26 -35
  74. package/components/navigation/header/SystemNotification.vue +1 -3
  75. package/components/navigation/pill-tabs/APillTabs.vue +2 -2
  76. package/components/navigation/pill-tabs/APillTabsV2.vue +5 -5
  77. package/components/overlays/drawer/ADrawerV2.vue +335 -0
  78. package/components/overlays/modal/AModal.vue +4 -4
  79. package/components/overlays/modal/AModalV2.vue +4 -4
  80. package/components/overlays/side-panel/ASidePanel.vue +6 -6
  81. package/components/overlays/sideover/ASlideover.vue +6 -6
  82. package/components/overlays/tooltip/ATooltip.vue +1 -1
  83. package/components/overlays/tooltip/ATooltipV2.vue +2 -12
  84. package/components/transitions/ATransitionHeight.vue +1 -1
  85. package/composables/useHeaderNavigationLinks.ts +100 -94
  86. package/icons/archive.vue +16 -0
  87. package/icons/arrow/arrows-in.vue +11 -0
  88. package/icons/arrow/arrows-out.vue +11 -0
  89. package/icons/arrow/arrows-resize-in.vue +38 -0
  90. package/icons/arrow/arrows-resize-out.vue +17 -0
  91. package/icons/bot.vue +52 -0
  92. package/icons/clock-time.vue +24 -0
  93. package/icons/compare-outlined.vue +8 -0
  94. package/icons/dots-horizontal.vue +31 -0
  95. package/icons/download-line.vue +13 -0
  96. package/icons/egov.vue +41 -0
  97. package/icons/elicense.vue +38 -0
  98. package/icons/favourite-bookmark-outline.vue +17 -0
  99. package/icons/file-pen.vue +31 -0
  100. package/icons/file-stack.vue +31 -0
  101. package/icons/file-types/excel.vue +131 -0
  102. package/icons/file-types/html.vue +13 -0
  103. package/icons/file-types/jpeg.vue +16 -0
  104. package/icons/file-types/pdf.vue +11 -0
  105. package/icons/file-types/png.vue +11 -0
  106. package/icons/file-types/rar.vue +11 -0
  107. package/icons/file-types/word.vue +50 -0
  108. package/icons/file-types/zip.vue +11 -0
  109. package/icons/git-compare.vue +38 -0
  110. package/icons/graduation-cap.vue +31 -0
  111. package/icons/graph-loading.vue +34 -0
  112. package/icons/help-question-circle.vue +26 -0
  113. package/icons/in-row-type.vue +46 -0
  114. package/icons/in-table-type.vue +29 -0
  115. package/icons/message-dotted.vue +30 -0
  116. package/icons/notification-error.vue +17 -0
  117. package/icons/person.vue +22 -0
  118. package/icons/settings.vue +26 -0
  119. package/icons/shield-alert.vue +31 -0
  120. package/icons/socials/facebook-color.vue +16 -0
  121. package/icons/socials/globe-color.vue +16 -0
  122. package/icons/socials/instagram-color.vue +46 -0
  123. package/icons/socials/linkedin-color.vue +14 -0
  124. package/icons/socials/telegram-color.vue +16 -0
  125. package/icons/socials/tiktok-color.vue +14 -0
  126. package/icons/socials/vk-color.vue +16 -0
  127. package/icons/socials/youtube-color.vue +16 -0
  128. package/icons/view-eye-open.vue +13 -0
  129. package/package.json +1 -1
  130. package/plugins/toast.client.ts +38 -55
  131. package/tailwind.config.ts +147 -116
  132. package/utils/twMerge.ts +34 -0
  133. package/components/elements/tree-select/components/tree-select-nodes.vue +0 -91
@@ -62,7 +62,7 @@ function handleExportClick() {
62
62
  v-if="module.key === 'edo'"
63
63
  #bottom
64
64
  >
65
- <div class="mt-4 flex flex-col gap-4 rounded-xl bg-blue-700 p-4 normal-case text-white dark:bg-blue-500">
65
+ <div class="mt-4 flex flex-col gap-4 rounded-lg bg-blue-700 p-4 normal-case text-white dark:bg-blue-500">
66
66
  <div class="flex items-center gap-3">
67
67
  <span class="rounded-[10px] bg-white p-[10px] dark:bg-gray-950">
68
68
  <a-icon-download-rounded class="size-5 text-blue-700 dark:text-blue-500" />
@@ -39,23 +39,20 @@ function onReplenish() {
39
39
  </script>
40
40
 
41
41
  <template>
42
- <div class="w-full overflow-hidden bg-white text-deepblue-900 dark:bg-gray-900 dark:text-gray-200 lg:w-[320px] lg:rounded-xl lg:border lg:border-gray-200 lg:dark:border-gray-800">
43
- <!-- Subscription & balance card -->
42
+ <div class="w-full overflow-hidden bg-white text-deepblue-900 dark:bg-gray-900 dark:text-gray-200 lg:w-[320px] lg:rounded-lg lg:border lg:border-gray-200 lg:dark:border-gray-800">
44
43
  <div class="p-2">
45
- <div class="relative overflow-hidden rounded-xl bg-gradient-to-br from-blue-700/95 to-blue-500/85 p-3 text-white">
46
- <!-- decorative glow for a "living" feel -->
44
+ <div class="relative overflow-hidden rounded-lg bg-gradient-to-br from-blue-700/95 to-blue-500/85 p-3 text-white">
47
45
  <div class="pointer-events-none absolute -right-8 -top-10 size-28 rounded-full bg-white/20 blur-2xl" aria-hidden="true" />
48
46
 
49
47
  <div class="relative">
50
- <!-- Tariff -->
51
- <p class="text-[11px] font-medium uppercase tracking-wide text-white/70">
48
+ <p class="text-overline-medium uppercase text-white/70">
52
49
  {{ t('header.profile.tariff') }}
53
50
  </p>
54
51
  <div class="mt-0.5 flex items-center justify-between gap-2">
55
- <p class="truncate text-lg font-bold leading-tight">
52
+ <p class="truncate text-h4">
56
53
  {{ rate }}
57
54
  </p>
58
- <span class="inline-flex shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium text-white ring-1 ring-inset ring-white/20">
55
+ <span class="inline-flex shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-body-sm-medium text-white ring-1 ring-inset ring-white/20">
59
56
  <span class="relative flex size-1.5 items-center justify-center">
60
57
  <span class="absolute inline-flex size-full animate-ping rounded-full bg-green-400/80" />
61
58
  <span class="relative inline-flex size-1.5 rounded-full bg-green-400" />
@@ -64,10 +61,9 @@ function onReplenish() {
64
61
  </span>
65
62
  </div>
66
63
 
67
- <!-- Email (mobile only — desktop shows it in the header trigger) -->
68
64
  <p
69
65
  v-if="email"
70
- class="mt-1 truncate text-xs text-white/75 lg:hidden"
66
+ class="mt-1 truncate text-body-sm text-white/75 lg:hidden"
71
67
  data-test-id="profile-menu-email"
72
68
  >
73
69
  {{ email }}
@@ -75,42 +71,39 @@ function onReplenish() {
75
71
 
76
72
  <div class="my-2 h-px bg-white/15" />
77
73
 
78
- <!-- Balance -->
79
74
  <div class="flex items-center justify-between gap-2">
80
- <span class="text-xs text-white/80">{{ t('header.profile.currentBalance') }}</span>
81
- <span class="text-base font-bold tabular-nums">{{ formattedBalance }} ₸</span>
75
+ <span class="text-body-sm text-white/80">{{ t('header.profile.currentBalance') }}</span>
76
+ <span class="text-body-lg font-bold tabular-nums">{{ formattedBalance }} ₸</span>
82
77
  </div>
83
78
 
84
- <!-- Usage stats (mobile only — desktop shows them in the header bar) -->
85
79
  <div class="mt-2 grid grid-cols-2 gap-2 lg:hidden">
86
- <div class="flex items-center gap-2 rounded-lg px-2 py-1.5 ring-1 ring-inset ring-white/15">
80
+ <div class="flex items-center gap-2 rounded-md px-2 py-1.5 ring-1 ring-inset ring-white/15">
87
81
  <gauge class="size-4 shrink-0 text-white/80" />
88
82
  <div class="min-w-0">
89
- <p class="truncate text-[10px] uppercase leading-none text-white/70">
83
+ <p class="truncate text-body-sm uppercase leading-none text-white/70">
90
84
  {{ t('header.profile.requests') }}
91
85
  </p>
92
- <p class="mt-1 text-sm font-bold tabular-nums leading-none">
86
+ <p class="mt-1 text-body-md font-bold tabular-nums leading-none">
93
87
  {{ limitRemaining }}
94
88
  </p>
95
89
  </div>
96
90
  </div>
97
- <div class="flex items-center gap-2 rounded-lg px-2 py-1.5 ring-1 ring-inset ring-white/15">
91
+ <div class="flex items-center gap-2 rounded-md px-2 py-1.5 ring-1 ring-inset ring-white/15">
98
92
  <calendar class="size-4 shrink-0 text-white/80" />
99
93
  <div class="min-w-0">
100
- <p class="truncate text-[10px] uppercase leading-none text-white/70">
94
+ <p class="truncate text-body-sm uppercase leading-none text-white/70">
101
95
  {{ t('header.profile.daysLeft') }}
102
96
  </p>
103
- <p class="mt-1 text-sm font-bold tabular-nums leading-none">
97
+ <p class="mt-1 text-body-md font-bold tabular-nums leading-none">
104
98
  {{ daysRemaining }}
105
99
  </p>
106
100
  </div>
107
101
  </div>
108
102
  </div>
109
103
 
110
- <!-- Top up -->
111
104
  <button
112
105
  type="button"
113
- class="mt-2.5 inline-flex h-8 w-full cursor-pointer items-center justify-center gap-1.5 rounded-full bg-white px-3 text-sm font-semibold text-blue-700 shadow-sm transition-all duration-150 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2 focus-visible:ring-offset-blue-600 active:scale-[0.98]"
106
+ class="mt-2.5 inline-flex h-8 w-full cursor-pointer items-center justify-center gap-1.5 rounded-full bg-white px-3 text-body-md-strong text-blue-700 shadow-sm transition-all duration-150 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2 focus-visible:ring-offset-blue-600 active:scale-[0.98]"
114
107
  @click="onReplenish"
115
108
  >
116
109
  <a-icon-plus-circle class="size-4" />
@@ -122,31 +115,29 @@ function onReplenish() {
122
115
  </div>
123
116
  </div>
124
117
 
125
- <!-- Menu links -->
126
118
  <nav class="flex flex-col px-2 pb-2">
127
119
  <nuxt-link-locale
128
120
  v-for="item in items"
129
121
  :key="item.title"
130
- class="group flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/60"
122
+ class="group flex items-center gap-3 rounded-md px-2.5 py-2 text-body-md transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/60"
131
123
  :to="item.to"
132
124
  :data-test-id="item?.dataTestId"
133
125
  target="_blank"
134
126
  >
135
- <!-- <span-->
136
- <!-- class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-blue-100 bg-blue-50 text-blue-700 dark:border-blue-900/40 dark:bg-blue-900/30 dark:text-blue-400"-->
137
- <!-- >-->
138
- <!-- <component-->
139
- <!-- :is="item.icon"-->
140
- <!-- class="size-4"-->
141
- <!-- />-->
142
- <!-- </span>-->
127
+ <!-- <span -->
128
+ <!-- class="flex size-8 shrink-0 items-center justify-center rounded-md border border-blue-100 bg-blue-50 text-blue-700 dark:border-blue-900/40 dark:bg-blue-900/30 dark:text-blue-400" -->
129
+ <!-- > -->
130
+ <!-- <component -->
131
+ <!-- :is="item.icon" -->
132
+ <!-- class="size-4" -->
133
+ <!-- /> -->
134
+ <!-- </span> -->
143
135
  <span class="truncate text-deepblue-900 group-hover:text-blue-700 dark:text-gray-200 dark:group-hover:text-blue-400">
144
136
  {{ item.title }}
145
137
  </span>
146
138
  </nuxt-link-locale>
147
139
  </nav>
148
140
 
149
- <!-- Footer: old version, theme (mobile), logout -->
150
141
  <div class="border-t border-gray-100 p-2 dark:border-gray-800">
151
142
  <a-change-version
152
143
  v-if="oldVersion"
@@ -156,7 +147,7 @@ function onReplenish() {
156
147
 
157
148
  <div
158
149
  v-if="!isLargeScreen"
159
- class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm text-deepblue-900 dark:text-gray-200"
150
+ class="flex items-center justify-between rounded-md px-2.5 py-2 text-body-md text-deepblue-900 dark:text-gray-200"
160
151
  >
161
152
  <span>{{ t('header.profile.colorScheme') }}</span>
162
153
  <a-color-mode />
@@ -164,7 +155,7 @@ function onReplenish() {
164
155
 
165
156
  <button
166
157
  type="button"
167
- class="flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
158
+ class="flex w-full cursor-pointer items-center gap-3 rounded-md px-2.5 py-2 text-body-md-medium text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
168
159
  data-test-id="logout-button"
169
160
  @click="$emit('logout')"
170
161
  >
@@ -19,7 +19,6 @@ const message = useCookie('message')
19
19
  const systemMessage = ref<string | null>(null)
20
20
  const isOpen = ref(false)
21
21
 
22
- // API expects the legacy "counterparty" code instead of "pk".
23
22
  const moduleName = computed(() => (currentModule.value === 'pk' ? 'counterparty' : currentModule.value))
24
23
 
25
24
  function localizedMessage(detail: SystemMessageDetail): string {
@@ -34,7 +33,6 @@ function localizedMessage(detail: SystemMessageDetail): string {
34
33
  }
35
34
 
36
35
  async function fetchNotification() {
37
- // The cookie is set once the user dismisses the banner — skip the request afterwards.
38
36
  if (message.value !== undefined) return
39
37
 
40
38
  try {
@@ -74,7 +72,7 @@ onMounted(fetchNotification)
74
72
  <a-icon-info-circle
75
73
  class="max-h-[16px] w-full max-w-[16px] shrink-0 stroke-orange-500"
76
74
  />
77
- <span class="text-sm font-semibold text-orange-500">{{ systemMessage }}</span>
75
+ <span class="text-body-md-strong text-orange-500">{{ systemMessage }}</span>
78
76
  </div>
79
77
  <a-icon-x-mark
80
78
  class="max-h-[16px] w-full max-w-[16px] shrink-0 cursor-pointer stroke-orange-500"
@@ -115,14 +115,14 @@ const viewTypeValues: ViewType = {
115
115
  }
116
116
 
117
117
  const defaultClasses = (isDisabled: boolean) => [
118
- 'text-sm rounded-md outline-none select-none focus-visible:shadow-[0_0_0_4px#1B64B3]',
118
+ 'text-sm rounded-sm outline-none select-none focus-visible:shadow-[0_0_0_4px#1B64B3]',
119
119
  isDisabled ? 'bg-deepblue-900/5 dark:bg-gray-200/5 text-gray-500 dark:text-gray-500' : '',
120
120
  props.view === 'transparent' && isDisabled ? 'bg-transparent' : '',
121
121
  viewTypeValues[props.view]['default'],
122
122
  props.block ? 'w-full' : ''
123
123
  ]
124
124
  const selectedClasses = () => [
125
- 'text-sm rounded-md select-none focus-visible:shadow-[0_0_0_4px#1B64B3]',
125
+ 'text-sm rounded-sm select-none focus-visible:shadow-[0_0_0_4px#1B64B3]',
126
126
  viewTypeValues[props.view]['selected'],
127
127
  props.block ? 'w-full' : ''
128
128
  ]
@@ -32,8 +32,8 @@ const emit = defineEmits<{ (e: 'update:modelValue', value: string): void }>()
32
32
  const listRef = ref<HTMLElement | null>(null)
33
33
 
34
34
  const SIZE_CLASS: Record<'sm' | 'md', string> = {
35
- sm: 'text-xs',
36
- md: 'text-sm',
35
+ sm: 'text-body-sm',
36
+ md: 'text-body-md',
37
37
  }
38
38
 
39
39
  const COUNT_COLOR_CLASS: Record<CountColor, string> = {
@@ -74,7 +74,7 @@ onMounted(scrollActiveIntoView)
74
74
 
75
75
  <template>
76
76
  <div
77
- class="pill-tabs-v2 min-w-0 max-w-full rounded-xl border border-gray-200 bg-gray-50 p-1 dark:border-white/10 dark:bg-white/[0.03]"
77
+ class="pill-tabs-v2 min-w-0 max-w-full rounded-lg border border-gray-200 bg-gray-50 p-1 dark:border-white/10 dark:bg-white/[0.03]"
78
78
  :class="[
79
79
  block ? 'w-full' : 'w-fit',
80
80
  disabled ? 'pill-tabs-v2--disabled opacity-60' : '',
@@ -96,7 +96,7 @@ onMounted(scrollActiveIntoView)
96
96
  :aria-disabled="isDisabled(option) || undefined"
97
97
  :disabled="isDisabled(option)"
98
98
  :tabindex="modelValue === option.key ? 0 : -1"
99
- class="inline-flex items-center gap-1.5 whitespace-nowrap rounded-lg px-3 py-1.5 font-medium transition-colors duration-150"
99
+ class="inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-3 py-1.5 font-medium transition-colors duration-150"
100
100
  :class="[
101
101
  SIZE_CLASS[size],
102
102
  isDisabled(option)
@@ -113,7 +113,7 @@ onMounted(scrollActiveIntoView)
113
113
 
114
114
  <span
115
115
  v-if="showCount && !option.hideCount"
116
- class="inline-flex min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-semibold leading-5"
116
+ class="inline-flex min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-body-sm font-semibold leading-5"
117
117
  :class="COUNT_COLOR_CLASS[option.countColor ?? 'gray']"
118
118
  >
119
119
  {{ option.count }}
@@ -0,0 +1,335 @@
1
+ <script setup lang="ts">
2
+ import { onKeyStroke, useScrollLock } from '@vueuse/core'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ title?: string
6
+ closeOnOverlay?: boolean
7
+ closeOnEsc?: boolean
8
+ lockScroll?: boolean
9
+ persistent?: boolean
10
+ transition?: boolean
11
+ contentClass?: string
12
+ hideHeader?: boolean
13
+ blur?: boolean | 'sm' | 'md' | 'lg' | 'xl'
14
+ swipeToClose?: boolean
15
+ swipeThreshold?: number
16
+ showDragHandle?: boolean
17
+ maxHeight?: string
18
+ }>(), {
19
+ closeOnOverlay: true,
20
+ closeOnEsc: true,
21
+ lockScroll: true,
22
+ persistent: false,
23
+ transition: true,
24
+ hideHeader: false,
25
+ blur: false,
26
+ swipeToClose: true,
27
+ swipeThreshold: 65,
28
+ showDragHandle: true,
29
+ maxHeight: '100dvh',
30
+ })
31
+
32
+ const emit = defineEmits<{
33
+ (e: 'close'): void
34
+ (e: 'open'): void
35
+ }>()
36
+
37
+ const open = defineModel<boolean>({ required: true })
38
+
39
+ const contentRef = ref<HTMLElement | null>(null)
40
+ const overlayRef = ref<HTMLElement | null>(null)
41
+ const swipeClosing = ref(false)
42
+ const bodyScrollLocked = useScrollLock(import.meta.client ? document.body : null)
43
+
44
+ const blurClass = computed(() => {
45
+ if (!props.blur) return ''
46
+
47
+ switch (props.blur) {
48
+ case 'sm': return 'backdrop-blur-sm'
49
+ case 'md': return 'backdrop-blur-md'
50
+ case 'lg': return 'backdrop-blur-lg'
51
+ case 'xl': return 'backdrop-blur-xl'
52
+ default: return 'backdrop-blur'
53
+ }
54
+ })
55
+
56
+ const overlayClass = computed(() => {
57
+ const base = ['drawer-v2-overlay fixed inset-0 z-[10000] flex items-end justify-stretch bg-black/40 dark:bg-black/60']
58
+ if (blurClass.value) base.push(blurClass.value)
59
+ return base.join(' ')
60
+ })
61
+
62
+ const contentStyle = computed(() => ({ maxHeight: props.maxHeight }))
63
+
64
+ const overlayTransitionName = computed(() => {
65
+ if (swipeClosing.value) return ''
66
+ return props.transition ? 'drawer-v2-overlay' : ''
67
+ })
68
+ const contentTransitionName = computed(() => {
69
+ if (swipeClosing.value || !props.transition) return ''
70
+ return 'drawer-v2-sheet'
71
+ })
72
+
73
+ function closeIfAllowed() {
74
+ if (props.persistent) return
75
+ open.value = false
76
+ }
77
+
78
+ function onOverlayClick(event: MouseEvent) {
79
+ if (!props.closeOnOverlay || props.persistent) return
80
+ if (event.target === event.currentTarget) {
81
+ open.value = false
82
+ }
83
+ }
84
+
85
+ onKeyStroke('Escape', () => {
86
+ if (!open.value) return
87
+ if (!props.closeOnEsc || props.persistent) return
88
+ open.value = false
89
+ })
90
+
91
+ function setScrollbarCompensation(active: boolean) {
92
+ if (!import.meta.client) return
93
+
94
+ if (active) {
95
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
96
+ document.body.style.paddingRight = scrollbarWidth > 0 ? `${scrollbarWidth}px` : ''
97
+ }
98
+ else {
99
+ document.body.style.paddingRight = ''
100
+ }
101
+ }
102
+
103
+ watch(open, (value) => {
104
+ if (props.lockScroll) {
105
+ if (value) setScrollbarCompensation(true)
106
+ bodyScrollLocked.value = value
107
+ if (!value) setScrollbarCompensation(false)
108
+ }
109
+
110
+ if (value) emit('open')
111
+ else emit('close')
112
+ })
113
+
114
+ onBeforeUnmount(() => {
115
+ if (bodyScrollLocked.value) {
116
+ bodyScrollLocked.value = false
117
+ }
118
+ setScrollbarCompensation(false)
119
+ })
120
+
121
+ const dragging = ref(false)
122
+ const dragStartY = ref(0)
123
+ const dragDelta = ref(0)
124
+
125
+ function canSwipe() {
126
+ return props.swipeToClose && !props.persistent
127
+ }
128
+
129
+ function setTransform(value: string) {
130
+ if (contentRef.value) {
131
+ contentRef.value.style.transform = value
132
+ }
133
+ }
134
+
135
+ function onTouchStart(event: TouchEvent) {
136
+ if (!canSwipe()) return
137
+ dragging.value = true
138
+ dragStartY.value = event.touches[0].clientY
139
+ dragDelta.value = 0
140
+
141
+ if (contentRef.value) {
142
+ contentRef.value.style.transition = 'none'
143
+ }
144
+ }
145
+
146
+ function onTouchMove(event: TouchEvent) {
147
+ if (!dragging.value || !canSwipe()) return
148
+
149
+ const delta = event.touches[0].clientY - dragStartY.value
150
+ if (delta < 0) {
151
+ dragDelta.value = 0
152
+ setTransform('')
153
+ return
154
+ }
155
+
156
+ dragDelta.value = delta
157
+ setTransform(`translateY(${delta}px)`)
158
+ }
159
+
160
+ function prefersReducedMotion() {
161
+ return import.meta.client && window.matchMedia('(prefers-reduced-motion: reduce)').matches
162
+ }
163
+
164
+ function animateClose() {
165
+ const content = contentRef.value
166
+
167
+ if (!content || prefersReducedMotion()) {
168
+ open.value = false
169
+ return
170
+ }
171
+
172
+ swipeClosing.value = true
173
+
174
+ let done = false
175
+ const finish = () => {
176
+ if (done) return
177
+ done = true
178
+ content.removeEventListener('transitionend', onTransitionEnd)
179
+ open.value = false
180
+ swipeClosing.value = false
181
+ }
182
+
183
+ function onTransitionEnd(event: TransitionEvent) {
184
+ if (event.target === content && event.propertyName === 'transform') {
185
+ finish()
186
+ }
187
+ }
188
+
189
+ content.addEventListener('transitionend', onTransitionEnd)
190
+ setTimeout(finish, 320)
191
+
192
+ requestAnimationFrame(() => {
193
+ content.style.transition = 'transform 260ms cubic-bezier(0.22, 1, 0.36, 1)'
194
+ content.style.transform = 'translateY(100%)'
195
+
196
+ if (overlayRef.value) {
197
+ overlayRef.value.style.transition = 'opacity 260ms ease'
198
+ overlayRef.value.style.opacity = '0'
199
+ }
200
+ })
201
+ }
202
+
203
+ function onTouchEnd() {
204
+ if (!dragging.value) return
205
+ dragging.value = false
206
+
207
+ const shouldClose = dragDelta.value > props.swipeThreshold && canSwipe()
208
+
209
+ if (shouldClose) {
210
+ animateClose()
211
+ }
212
+ else if (contentRef.value) {
213
+ contentRef.value.style.transition = 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)'
214
+ contentRef.value.style.transform = ''
215
+ }
216
+
217
+ dragDelta.value = 0
218
+ }
219
+ </script>
220
+
221
+ <template>
222
+ <client-only>
223
+ <teleport to="body">
224
+ <transition :name="overlayTransitionName">
225
+ <div
226
+ v-if="open"
227
+ ref="overlayRef"
228
+ :class="overlayClass"
229
+ @mousedown="onOverlayClick"
230
+ >
231
+ <transition :name="contentTransitionName" appear>
232
+ <div
233
+ v-if="open"
234
+ ref="contentRef"
235
+ class="drawer-v2-content flex w-full max-w-none flex-col overflow-hidden rounded-t-xl border-t border-gray-200 bg-white shadow-2xl dark:border-gray-800 dark:bg-gray-900"
236
+ :style="contentStyle"
237
+ @mousedown.stop
238
+ >
239
+ <div
240
+ v-if="showDragHandle"
241
+ class="flex shrink-0 cursor-grab touch-none justify-center py-2"
242
+ @touchstart.passive="onTouchStart"
243
+ @touchmove="onTouchMove"
244
+ @touchend="onTouchEnd"
245
+ @touchcancel="onTouchEnd"
246
+ >
247
+ <div class="h-1 w-10 rounded-full bg-gray-300 dark:bg-gray-600" />
248
+ </div>
249
+
250
+ <header
251
+ v-if="!hideHeader && (title || $slots.header)"
252
+ class="flex shrink-0 items-center justify-between gap-3 border-b border-gray-200 px-5 py-4 dark:border-gray-800"
253
+ @touchstart.passive="onTouchStart"
254
+ @touchmove="onTouchMove"
255
+ @touchend="onTouchEnd"
256
+ @touchcancel="onTouchEnd"
257
+ >
258
+ <slot name="header">
259
+ <h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">
260
+ {{ title }}
261
+ </h2>
262
+ <button
263
+ type="button"
264
+ class="rounded-sm p-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200"
265
+ @click="closeIfAllowed"
266
+ >
267
+ <a-icon-x-mark class="size-4" />
268
+ </button>
269
+ </slot>
270
+ </header>
271
+
272
+ <div class="min-h-0 flex-1 overflow-auto" :class="contentClass">
273
+ <slot :close="closeIfAllowed" />
274
+ </div>
275
+
276
+ <footer
277
+ v-if="$slots.footer"
278
+ class="shrink-0 border-t border-gray-200 px-5 py-4 dark:border-gray-800"
279
+ >
280
+ <slot name="footer" :close="closeIfAllowed" />
281
+ </footer>
282
+ </div>
283
+ </transition>
284
+ </div>
285
+ </transition>
286
+ </teleport>
287
+ </client-only>
288
+ </template>
289
+
290
+ <style scoped lang="scss">
291
+ .drawer-v2-overlay {
292
+ will-change: opacity;
293
+ }
294
+
295
+ .drawer-v2-content {
296
+ will-change: transform;
297
+ transform: translateZ(0);
298
+ backface-visibility: hidden;
299
+ }
300
+
301
+ .drawer-v2-overlay-enter-active {
302
+ transition: opacity 240ms ease-out;
303
+ }
304
+
305
+ .drawer-v2-overlay-leave-active {
306
+ transition: opacity 220ms ease-in;
307
+ }
308
+
309
+ .drawer-v2-overlay-enter-from,
310
+ .drawer-v2-overlay-leave-to {
311
+ opacity: 0;
312
+ }
313
+
314
+ .drawer-v2-sheet-enter-active {
315
+ transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
316
+ }
317
+
318
+ .drawer-v2-sheet-leave-active {
319
+ transition: transform 240ms cubic-bezier(0.4, 0, 1, 1);
320
+ }
321
+
322
+ .drawer-v2-sheet-enter-from,
323
+ .drawer-v2-sheet-leave-to {
324
+ transform: translate3d(0, 100%, 0);
325
+ }
326
+
327
+ @media (prefers-reduced-motion: reduce) {
328
+ .drawer-v2-overlay-enter-active,
329
+ .drawer-v2-overlay-leave-active,
330
+ .drawer-v2-sheet-enter-active,
331
+ .drawer-v2-sheet-leave-active {
332
+ transition: none;
333
+ }
334
+ }
335
+ </style>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
 
3
- import { twJoin, twMerge } from 'tailwind-merge'
3
+ import { twJoin, twMerge } from '#adata-ui/utils/twMerge'
4
4
  import { onClickOutside, useWindowSize } from '@vueuse/core'
5
5
  import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'
6
6
 
@@ -22,13 +22,13 @@ const uiConfig = {
22
22
  position: 'top-1/2 left-1/2 translate-y-[-50%] translate-x-[-50%]',
23
23
  padding: "p-8",
24
24
  background: "bg-white dark:bg-gray-900",
25
- rounded: "rounded-lg",
25
+ rounded: "rounded-md",
26
26
  fullscreen: "w-screen h-screen rounded-none",
27
27
  mobile: {
28
28
  width: "w-screen",
29
29
  position: "bottom-0 left-0 transition-transform",
30
30
  padding: 'px-4 sm:pb-8 pb-4 pt-3',
31
- rounded: "rounded-t-2xl"
31
+ rounded: "rounded-t-xl"
32
32
  },
33
33
  transition: {
34
34
  desktop: {
@@ -297,7 +297,7 @@ watch(modelValue, async (newValue) => {
297
297
  >
298
298
  <h4
299
299
  v-if="title"
300
- class="heading-02 text-center"
300
+ class="text-h2 text-center"
301
301
  @touchmove="touchmove"
302
302
  @touchstart="touchstart"
303
303
  @touchend="touchend"
@@ -98,10 +98,10 @@ const contentBaseClass = computed(() => {
98
98
  const base = ['modal-v2-content flex flex-col overflow-hidden border bg-white shadow-2xl dark:bg-gray-900']
99
99
 
100
100
  if (isSheet.value) {
101
- base.push('w-full max-w-none max-h-[100dvh] rounded-t-2xl rounded-b-none border-x-0 border-b-0 border-t border-gray-200 dark:border-gray-800')
101
+ base.push('w-full max-w-none max-h-[100dvh] rounded-t-xl rounded-b-none border-x-0 border-b-0 border-t border-gray-200 dark:border-gray-800')
102
102
  }
103
103
  else {
104
- const radius = props.size === 'fullscreen' ? 'rounded-none' : 'rounded-2xl'
104
+ const radius = props.size === 'fullscreen' ? 'rounded-none' : 'rounded-xl'
105
105
  const widthCls = props.size === 'container'
106
106
  ? ' w-full max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg xl:max-w-screen-xl 2xl:max-w-screen-2xl'
107
107
  : ''
@@ -308,14 +308,14 @@ function onTouchEnd() {
308
308
  @touchcancel="onTouchEnd"
309
309
  >
310
310
  <slot name="header">
311
- <h2 v-if="title" class="text-base font-semibold text-gray-900 dark:text-gray-100">
311
+ <h2 v-if="title" class="text-h5 text-gray-900 dark:text-gray-100">
312
312
  {{ title }}
313
313
  </h2>
314
314
  </slot>
315
315
  <button
316
316
  v-if="!hideClose"
317
317
  type="button"
318
- class="ml-auto rounded-md p-1 transition hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-gray-200"
318
+ class="ml-auto rounded-sm p-1 transition hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-gray-200"
319
319
  @click="closeIfAllowed"
320
320
  >
321
321
  <a-icon-x-mark class="size-4" />
@@ -128,10 +128,10 @@ watch(() => [modelValue.value, panel.value], (newP, oldP) => {
128
128
 
129
129
  const roundedClass = computed(() => {
130
130
  const classes = {
131
- left: "rounded-r-xl",
132
- right: "rounded-l-xl",
133
- top: "rounded-b-xl",
134
- bottom: "rounded-t-xl",
131
+ left: "rounded-r-lg",
132
+ right: "rounded-l-lg",
133
+ top: "rounded-b-lg",
134
+ bottom: "rounded-t-lg",
135
135
  }
136
136
 
137
137
  return classes[panelSide.value]
@@ -170,7 +170,7 @@ const modalHeight = ref(0)
170
170
  const touchstart = (event: TouchEvent) => {
171
171
  isClose.value = false
172
172
  positionStart.value = event.touches[0].clientY
173
- modalHeight.value = panel.value?.offsetHeight || 0 // Запоминаем текущую высоту модалки
173
+ modalHeight.value = panel.value?.offsetHeight || 0
174
174
  }
175
175
 
176
176
  const touchmove = (event: TouchEvent) => {
@@ -179,7 +179,7 @@ const touchmove = (event: TouchEvent) => {
179
179
  const touchPosition = event.touches[0].clientY
180
180
  const step = touchPosition - positionStart.value
181
181
 
182
- if (step < 0) return // предотвратить движение вверх
182
+ if (step < 0) return
183
183
 
184
184
  const newHeight = modalHeight.value - step
185
185
  panel.value.style.height = `${newHeight}px`