adata-ui 2.1.41-beta.10 → 2.1.41-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 (594) hide show
  1. package/-error.vue +170 -170
  2. package/.claude/settings.local.json +19 -0
  3. package/.editorconfig +12 -12
  4. package/.gitlab-ci.yml +16 -16
  5. package/.nuxtrc +1 -1
  6. package/.nvmrc +1 -1
  7. package/.playground/app.config.ts +5 -5
  8. package/.playground/app.vue +102 -102
  9. package/.playground/nuxt.config.ts +4 -4
  10. package/CLAUDE.md +63 -63
  11. package/README.md +75 -75
  12. package/app.config.ts +28 -28
  13. package/app.vue +8 -8
  14. package/assets/styles/main.css +12 -12
  15. package/assets/styles/theme.css +590 -590
  16. package/assets/styles/ui.css +247 -247
  17. package/components/elements/README.md +1 -1
  18. package/components/elements/a-select-row/ASelectRowV2.vue +210 -210
  19. package/components/elements/a-select-row/index.vue +139 -139
  20. package/components/elements/accordion/AAccordion.vue +199 -199
  21. package/components/elements/accordion/AAccordionGroup/AAccordionGroup.vue +145 -145
  22. package/components/elements/accordion/AAccordionGroup/types.ts +20 -20
  23. package/components/elements/accordion/AAccordionGroup/ui.config.ts +22 -22
  24. package/components/elements/accordion/types.ts +14 -14
  25. package/components/elements/accordion/ui.config.ts +22 -22
  26. package/components/elements/alert/AAlert.vue +52 -52
  27. package/components/elements/banner/ABanner.vue +67 -49
  28. package/components/elements/bg-block/ABgBlock.vue +11 -11
  29. package/components/elements/button/AButton.vue +191 -191
  30. package/components/elements/button/AButtonV2.vue +179 -179
  31. package/components/elements/button/types.ts +24 -24
  32. package/components/elements/button-login/index.vue +19 -19
  33. package/components/elements/calendar/ACalendar.vue +83 -83
  34. package/components/elements/carousel/ACarousel.vue +159 -159
  35. package/components/elements/carousel/config.ui.ts +15 -15
  36. package/components/elements/carousel/types.ts +18 -18
  37. package/components/elements/chip-wrapper/AChipWrapper.vue +81 -81
  38. package/components/elements/chip-wrapper/types.ts +12 -12
  39. package/components/elements/chips/AChips.vue +33 -33
  40. package/components/elements/companies/AOurClients.vue +47 -47
  41. package/components/elements/count-up/ACountUp.vue +54 -54
  42. package/components/elements/curve-block/ACurveBlock.vue +259 -259
  43. package/components/elements/curve-block-v2/ACurveBlockV2.vue +122 -122
  44. package/components/elements/digit-badge/ADigitBadge.vue +86 -86
  45. package/components/elements/empty-box/empty-box-V2.vue +92 -92
  46. package/components/elements/error-template/index.vue +121 -121
  47. package/components/elements/feature-description/AFeatureDescription.vue +112 -112
  48. package/components/elements/illustrations/noAccess.vue +71 -71
  49. package/components/elements/infinite-carousel/AInfiniteCarousel.vue +85 -85
  50. package/components/elements/infinite-scroll/AInfiniteScroll.vue +33 -33
  51. package/components/elements/leave-note/ALeaveNote.vue +102 -102
  52. package/components/elements/pagination/APagination.vue +110 -110
  53. package/components/elements/photos-animation/APhotosAnimation.vue +85 -85
  54. package/components/elements/section-heading/ASectionHeading.vue +35 -35
  55. package/components/elements/segmented/ASegmentedV2.vue +154 -154
  56. package/components/elements/select/ACascadeSelectV2.vue +710 -710
  57. package/components/elements/select/ASelect.vue +448 -448
  58. package/components/elements/select/ASelectMobile.vue +186 -186
  59. package/components/elements/select/ASelectV2.vue +513 -507
  60. package/components/elements/select/cascade-utils.ts +115 -115
  61. package/components/elements/select/types.ts +27 -27
  62. package/components/elements/select/ui/cascade-drawer.vue +146 -146
  63. package/components/elements/select/ui/cascade-panel.vue +237 -237
  64. package/components/elements/select/ui/select-button.vue +112 -112
  65. package/components/elements/select/ui/select-list.vue +99 -99
  66. package/components/elements/select/ui/select-trigger.vue +167 -167
  67. package/components/elements/show-more/AShowMore.vue +35 -35
  68. package/components/elements/show-more/AShowMoreV2.vue +26 -26
  69. package/components/elements/skeleton/ASkeleton.vue +44 -44
  70. package/components/elements/slider/ASlider.vue +175 -175
  71. package/components/elements/slider/ASliderItem.vue +31 -31
  72. package/components/elements/slider/ASliderV2.vue +182 -182
  73. package/components/elements/slider/types.ts +21 -21
  74. package/components/elements/star-rating/AStarRating.vue +189 -189
  75. package/components/elements/star-rating/types.ts +7 -7
  76. package/components/elements/star-rating/ui/Star.vue +74 -74
  77. package/components/elements/status-badge/AStatusBadge.vue +125 -125
  78. package/components/elements/step-slider/AStepSliderColCard.vue +266 -266
  79. package/components/elements/step-slider/AStepSliderColNumber.vue +199 -199
  80. package/components/elements/table/ATable.vue +93 -93
  81. package/components/elements/table/table.config.ts +32 -32
  82. package/components/elements/tree-select/ATreeSelect.vue +157 -157
  83. package/components/elements/tree-select/TreeService.ts +184 -184
  84. package/components/elements/tree-select/components/ATreeSelectNode.vue +104 -104
  85. package/components/elements/tree-select/types.ts +63 -63
  86. package/components/elements/tree-select/utils.ts +30 -30
  87. package/components/elements/tree-select-mobile/ATreeSelectMobile.vue +79 -79
  88. package/components/elements/tree-select-mobile/components/ATreeSelectNodesMobile.vue +175 -175
  89. package/components/elements/tree-select-modal/ATreeSelectModal.vue +217 -217
  90. package/components/features/color-mode/AColorMode.client.vue +74 -74
  91. package/components/features/dropdown/ADropdown.vue +127 -127
  92. package/components/features/dropdown/ADropdownV2.vue +140 -140
  93. package/components/features/go-top/GoTop.vue +67 -67
  94. package/components/features/lang-switcher/lang-switcher.vue +158 -158
  95. package/components/features/payment/banner/BasicPlusLimitBanner.vue +84 -84
  96. package/components/features/payment/banner/PaymentBanner.vue +88 -88
  97. package/components/features/payment/banner/UpSellBanner.vue +105 -105
  98. package/components/features/payment/process/PaymentKaspiQrSidePanel.vue +180 -180
  99. package/components/features/payment/process/PaymentKaspiRedirectSidePanel.vue +123 -123
  100. package/components/features/payment/process/PaymentMethodSidePanel.vue +138 -138
  101. package/components/features/payment/process/PaymentProcess.vue +158 -158
  102. package/components/features/payment/process/PaymentTopUpSidePanel.vue +119 -119
  103. package/components/features/pk-mobile-services/APkMobileServices.vue +107 -107
  104. package/components/features//321/201hange-version/AChangeVersion.vue +20 -20
  105. package/components/forms/README.md +1 -1
  106. package/components/forms/checkbox/ACheckbox.vue +149 -149
  107. package/components/forms/checkbox/ACheckboxV2.vue +229 -229
  108. package/components/forms/feedback-form/FeedbackForm.vue +177 -177
  109. package/components/forms/feedback-form/FeedbackFormV2.vue +183 -183
  110. package/components/forms/input/AInputV2.vue +560 -560
  111. package/components/forms/input/button/AInputButton.vue +53 -53
  112. package/components/forms/input/date/AInputDate.vue +217 -217
  113. package/components/forms/input/password/AInputPassword.vue +59 -59
  114. package/components/forms/input/standard/AInputStandard.vue +424 -424
  115. package/components/forms/input/textarea/ATextarea.vue +207 -207
  116. package/components/forms/input/textarea/ATextareaV2.vue +253 -253
  117. package/components/forms/radio-button/ARadioButton.vue +95 -95
  118. package/components/forms/request-demo/ARequestDemo.vue +108 -108
  119. package/components/forms/toggle/AToggle.vue +157 -157
  120. package/components/forms/toggle/AToggleV2.vue +72 -72
  121. package/components/forms/toggle/types.ts +16 -16
  122. package/components/modals/AConfirmationEmail.vue +40 -40
  123. package/components/modals/AboutTariffModal.vue +115 -115
  124. package/components/modals/AnotherDeviceModal.vue +65 -65
  125. package/components/modals/ApplicationAcceptedModal.vue +45 -45
  126. package/components/modals/ConnectingTariffModal.vue +60 -60
  127. package/components/modals/ContactsMobileModel.vue +182 -182
  128. package/components/modals/ContentNavigationModal.vue +100 -100
  129. package/components/modals/Insufficient-funds-modal.vue +62 -62
  130. package/components/modals/LimitReachedModal.vue +44 -44
  131. package/components/modals/NoAccessModal.vue +39 -39
  132. package/components/modals/PaymentMethodModal.vue +101 -101
  133. package/components/modals/RedirectsBanksModal.vue +69 -69
  134. package/components/modals/ReplenishModal.vue +132 -132
  135. package/components/modals/ReportBugConfirmModal.vue +32 -32
  136. package/components/modals/ReportBugModal.vue +172 -172
  137. package/components/modals/Resend.vue +81 -81
  138. package/components/modals/SubmitApplicationModal.vue +126 -126
  139. package/components/modals/id/AuthModal.vue +72 -72
  140. package/components/modals/id/IdAutoLogoutModal.vue +44 -44
  141. package/components/modals/id/IdBanner.vue +41 -41
  142. package/components/modals/id/IdConfirmAccountOtpModal.vue +182 -182
  143. package/components/modals/id/IdConfirmSuccessfulModal.vue +41 -41
  144. package/components/modals/id/IdLoginModal.vue +336 -336
  145. package/components/modals/id/IdModals.vue +129 -129
  146. package/components/modals/id/IdNewPasswordModal.vue +129 -129
  147. package/components/modals/id/IdOtpInput.vue +156 -156
  148. package/components/modals/id/IdPasswordSuccessfulModal.vue +25 -25
  149. package/components/modals/id/IdRecoveryModal.vue +117 -117
  150. package/components/modals/id/IdRegistrationModal.vue +215 -215
  151. package/components/modals/id/IdResetPasswordOtpModal.vue +159 -159
  152. package/components/modals/id/IdTwoFactorModal.vue +126 -126
  153. package/components/navigation/README.md +1 -1
  154. package/components/navigation/bottom-navigation/ABottomNavigation.vue +106 -106
  155. package/components/navigation/breadcrumbs/ABreadcrumbs.vue +128 -128
  156. package/components/navigation/breadcrumbs/types.ts +8 -8
  157. package/components/navigation/footer/AFooter.vue +283 -283
  158. package/components/navigation/footer/ui/a-footer-accordion.vue +118 -118
  159. package/components/navigation/header/AHeader.vue +241 -241
  160. package/components/navigation/header/AlmatyContacts.vue +145 -145
  161. package/components/navigation/header/AstanaContacts.vue +67 -67
  162. package/components/navigation/header/CardGallery.vue +252 -252
  163. package/components/navigation/header/ContactMenu.vue +48 -48
  164. package/components/navigation/header/HeaderLink.vue +250 -250
  165. package/components/navigation/header/HeaderUsage.vue +139 -139
  166. package/components/navigation/header/ListItem.vue +32 -32
  167. package/components/navigation/header/NavCard.vue +42 -42
  168. package/components/navigation/header/NavList.vue +73 -73
  169. package/components/navigation/header/NotificationsMenu.vue +50 -50
  170. package/components/navigation/header/ProductMenu.vue +53 -101
  171. package/components/navigation/header/ProfileMenu.vue +168 -168
  172. package/components/navigation/header/SystemNotification.vue +108 -108
  173. package/components/navigation/header/types.ts +28 -28
  174. package/components/navigation/line-tabs/ALineTabs.vue +112 -112
  175. package/components/navigation/line-tabs/types.ts +21 -21
  176. package/components/navigation/mobile-navigation/AMobileNavigation.vue +112 -112
  177. package/components/navigation/pill-tabs/APillTabs.vue +179 -179
  178. package/components/navigation/pill-tabs/APillTabsV2.vue +241 -241
  179. package/components/navigation/pill-tabs/types.ts +24 -24
  180. package/components/navigation/side-navigation/ASideNavigation.vue +406 -406
  181. package/components/overlays/README.md +1 -1
  182. package/components/overlays/drawer/ADrawerV2.vue +342 -342
  183. package/components/overlays/modal/AModal.vue +335 -335
  184. package/components/overlays/modal/AModalV2.vue +401 -401
  185. package/components/overlays/side-panel/ASidePanel.vue +445 -445
  186. package/components/overlays/sideover/ASlideover.vue +351 -351
  187. package/components/overlays/tooltip/ATooltip.vue +238 -238
  188. package/components/overlays/tooltip/ATooltipV2.vue +226 -226
  189. package/components/overlays/tooltip/types.ts +26 -26
  190. package/components/overlays/tooltip/useTooltipTrigger.ts +101 -101
  191. package/components/transitions/ATransitionHeight.vue +67 -67
  192. package/components/utils/index.ts +19 -19
  193. package/components/utils/removeTrailingSlash.ts +7 -7
  194. package/composables/highlight.ts +19 -19
  195. package/composables/modalsState.ts +8 -8
  196. package/composables/projectState.ts +2 -2
  197. package/composables/useAccessTokenCookie.ts +25 -25
  198. package/composables/useActiveNavigation.ts +84 -84
  199. package/composables/useAdaptive.ts +46 -46
  200. package/composables/useBuyTariffs.ts +91 -91
  201. package/composables/useCarouselScroll.ts +49 -49
  202. package/composables/useChipOverflow.ts +92 -92
  203. package/composables/useHeaderNavigationLinks.ts +535 -519
  204. package/composables/useIdModals.ts +36 -36
  205. package/composables/usePayment.ts +78 -78
  206. package/composables/useUIValidation.ts +16 -16
  207. package/composables/useUrls.ts +23 -23
  208. package/eslint.config.mjs +58 -58
  209. package/i18n/i18n.config.ts +21 -21
  210. package/i18n.config.ts +21 -21
  211. package/icons/adata-logo.vue +17 -17
  212. package/icons/ai/ai-diamond-unfilled.vue +26 -26
  213. package/icons/ai/ai-diamond.vue +11 -11
  214. package/icons/ai-assistant.vue +13 -13
  215. package/icons/akimat.vue +20 -20
  216. package/icons/archive.vue +16 -16
  217. package/icons/arrow/arrow-bottom-left-on-square.vue +5 -5
  218. package/icons/arrow/arrow-circle-down.vue +16 -16
  219. package/icons/arrow/arrow-circle-reset.vue +16 -16
  220. package/icons/arrow/arrow-corner.vue +9 -9
  221. package/icons/arrow/arrow-down-left.vue +6 -6
  222. package/icons/arrow/arrow-graph-down.vue +13 -13
  223. package/icons/arrow/arrow-graph-up.vue +14 -14
  224. package/icons/arrow/arrow-left-right.vue +8 -8
  225. package/icons/arrow/arrow-left.vue +14 -14
  226. package/icons/arrow/arrow-right.vue +14 -14
  227. package/icons/arrow/arrow-square-down.vue +15 -15
  228. package/icons/arrow/arrow-top-right-on-square.vue +6 -6
  229. package/icons/arrow/arrow-up-right.vue +6 -6
  230. package/icons/arrow/arrows-in.vue +11 -11
  231. package/icons/arrow/arrows-out.vue +11 -11
  232. package/icons/arrow/arrows-resize-in.vue +38 -38
  233. package/icons/arrow/arrows-resize-out.vue +17 -17
  234. package/icons/arrow-currency-gray.vue +5 -5
  235. package/icons/arrow-currency-green.vue +5 -5
  236. package/icons/arrow-currency-red.vue +5 -5
  237. package/icons/arrow-side-up.vue +6 -6
  238. package/icons/avatar.vue +12 -12
  239. package/icons/bank.vue +5 -5
  240. package/icons/bar-chart-3.vue +3 -3
  241. package/icons/bill.vue +3 -3
  242. package/icons/block.vue +16 -16
  243. package/icons/book-open.vue +3 -3
  244. package/icons/bookmark/bookmark-filled.vue +19 -19
  245. package/icons/bookmark/bookmark.vue +5 -5
  246. package/icons/bot.vue +52 -52
  247. package/icons/braces.vue +3 -3
  248. package/icons/briefcase.vue +14 -14
  249. package/icons/browsers/browser-duck.vue +65 -65
  250. package/icons/browsers/browser-google.vue +30 -30
  251. package/icons/browsers/browser-yandex.vue +13 -13
  252. package/icons/building-vector.vue +7 -7
  253. package/icons/building.vue +9 -9
  254. package/icons/calculator.vue +7 -7
  255. package/icons/calendar.vue +24 -24
  256. package/icons/car-side.vue +16 -16
  257. package/icons/car.vue +7 -7
  258. package/icons/cart.vue +15 -15
  259. package/icons/chart-bar.vue +5 -5
  260. package/icons/chart-no-axes-column.vue +3 -3
  261. package/icons/chart-pie.vue +16 -16
  262. package/icons/check/check-circle.vue +6 -6
  263. package/icons/check/check-outline.vue +3 -3
  264. package/icons/check/check-shield.vue +14 -14
  265. package/icons/check/check.vue +17 -17
  266. package/icons/check/checkmark-circle.vue +6 -6
  267. package/icons/check-sb.vue +7 -7
  268. package/icons/checkbox/checkbox-active.vue +17 -17
  269. package/icons/checkbox/checkbox-empty.vue +10 -10
  270. package/icons/checkbox/checkbox-intermediate.vue +7 -7
  271. package/icons/chevron/chevron-down-outline.vue +3 -3
  272. package/icons/chevron/chevron-down.vue +5 -5
  273. package/icons/chevron/chevron-left.vue +5 -5
  274. package/icons/chevron/chevron-right.vue +5 -5
  275. package/icons/chevron/chevron-up.vue +5 -5
  276. package/icons/chevron/double-chevron-right.vue +12 -12
  277. package/icons/clipboard-list.vue +3 -3
  278. package/icons/clipboard-text.vue +7 -7
  279. package/icons/clock-outline.vue +3 -3
  280. package/icons/clock-rotate-ccw.vue +7 -7
  281. package/icons/clock-time.vue +24 -24
  282. package/icons/clock.vue +25 -25
  283. package/icons/close.vue +16 -16
  284. package/icons/cloud/cloud-upload.vue +3 -3
  285. package/icons/code.vue +9 -9
  286. package/icons/company/company-bazis.vue +21 -21
  287. package/icons/company/company-bereke.vue +25 -25
  288. package/icons/company/company-bigroup.vue +16 -16
  289. package/icons/company/company-erg.vue +17 -17
  290. package/icons/company/company-forte.vue +21 -21
  291. package/icons/company/company-halyk.vue +15 -15
  292. package/icons/company/company-jusan.vue +31 -31
  293. package/icons/company/company-kaspi.vue +14 -14
  294. package/icons/company/company-kazzinc.vue +22 -22
  295. package/icons/company/company-mycar.vue +13 -13
  296. package/icons/company/company-samruk.vue +21 -21
  297. package/icons/company-egov-small.vue +15 -15
  298. package/icons/company.vue +8 -8
  299. package/icons/compare-outlined.vue +8 -8
  300. package/icons/copy.vue +7 -7
  301. package/icons/crown.vue +6 -6
  302. package/icons/currency/currency-dollar.vue +16 -16
  303. package/icons/currency/currency-down.vue +22 -22
  304. package/icons/currency/currency-eur.vue +57 -57
  305. package/icons/currency/currency-rub.vue +7 -7
  306. package/icons/currency/currency-tenge.vue +9 -9
  307. package/icons/currency/currency-usd.vue +221 -221
  308. package/icons/currency/currency-yuan.vue +25 -25
  309. package/icons/database.vue +3 -3
  310. package/icons/default-avatar.vue +20 -20
  311. package/icons/delete.vue +7 -7
  312. package/icons/document.vue +5 -5
  313. package/icons/dots-horizontal.vue +31 -31
  314. package/icons/download-line.vue +13 -13
  315. package/icons/download-outline.vue +3 -3
  316. package/icons/download-rounded.vue +15 -15
  317. package/icons/download.vue +11 -11
  318. package/icons/edit.vue +7 -7
  319. package/icons/education.vue +16 -16
  320. package/icons/egov-small.vue +7 -7
  321. package/icons/egov.vue +41 -41
  322. package/icons/elicense.vue +38 -38
  323. package/icons/excel-icon.vue +14 -14
  324. package/icons/expand-window.vue +7 -7
  325. package/icons/eye-closed.vue +26 -26
  326. package/icons/eye-open.vue +7 -7
  327. package/icons/eye-opened.vue +23 -23
  328. package/icons/favourite-bookmark-outline.vue +17 -17
  329. package/icons/file/file-check-corner.vue +3 -3
  330. package/icons/file/file-scan.vue +5 -0
  331. package/icons/file/file-spreadsheet.vue +3 -3
  332. package/icons/file/file.vue +16 -16
  333. package/icons/file/files.vue +16 -16
  334. package/icons/file-json.vue +3 -3
  335. package/icons/file-pen.vue +31 -31
  336. package/icons/file-search.vue +3 -3
  337. package/icons/file-stack.vue +31 -31
  338. package/icons/file-text.vue +3 -3
  339. package/icons/file-types/excel.vue +131 -131
  340. package/icons/file-types/html.vue +13 -13
  341. package/icons/file-types/jpeg.vue +16 -16
  342. package/icons/file-types/pdf.vue +11 -11
  343. package/icons/file-types/png.vue +11 -11
  344. package/icons/file-types/rar.vue +11 -11
  345. package/icons/file-types/word.vue +50 -50
  346. package/icons/file-types/zip.vue +11 -11
  347. package/icons/filter.vue +5 -5
  348. package/icons/flag.vue +7 -7
  349. package/icons/flask-conical.vue +3 -3
  350. package/icons/folder-open.vue +3 -3
  351. package/icons/gauge.vue +17 -17
  352. package/icons/gift-outline.vue +3 -3
  353. package/icons/gift.vue +8 -8
  354. package/icons/git-compare.vue +38 -38
  355. package/icons/git-fork.vue +3 -3
  356. package/icons/globe-2.vue +3 -3
  357. package/icons/globe.vue +16 -16
  358. package/icons/google.vue +41 -41
  359. package/icons/gradiend-icons/files-gradient.vue +22 -22
  360. package/icons/gradiend-icons/hammer-gradient.vue +62 -62
  361. package/icons/graduation-cap.vue +31 -31
  362. package/icons/graph-loading.vue +34 -34
  363. package/icons/graph-pie.vue +14 -14
  364. package/icons/hand/hand-thumb-up-filled.vue +5 -5
  365. package/icons/hand/hand-thumb-up.vue +5 -5
  366. package/icons/hand-with-phone/hand-with-phone-dark.vue +52 -52
  367. package/icons/hand-with-phone/hand-with-phone-light.vue +52 -52
  368. package/icons/handshake.vue +5 -5
  369. package/icons/hcheck.vue +14 -14
  370. package/icons/hdocument.vue +7 -7
  371. package/icons/headset.vue +6 -6
  372. package/icons/help-question-circle.vue +26 -26
  373. package/icons/history.vue +5 -5
  374. package/icons/home.vue +14 -14
  375. package/icons/horizontal-more.vue +13 -13
  376. package/icons/hot-line.vue +6 -6
  377. package/icons/hummer.vue +16 -16
  378. package/icons/id.vue +7 -7
  379. package/icons/in-row-type.vue +46 -46
  380. package/icons/in-table-type.vue +29 -29
  381. package/icons/info/info-circle.vue +29 -29
  382. package/icons/invoice.vue +8 -8
  383. package/icons/judge.vue +14 -14
  384. package/icons/kaspi-qr.vue +13 -13
  385. package/icons/key-round.vue +3 -3
  386. package/icons/landmark.vue +3 -3
  387. package/icons/layers.vue +3 -3
  388. package/icons/letter/letter-a.vue +8 -8
  389. package/icons/letter/letter-b.vue +6 -6
  390. package/icons/letter/letter-c.vue +6 -6
  391. package/icons/letter/letter-d.vue +6 -6
  392. package/icons/letter/letter-e.vue +6 -6
  393. package/icons/link-chain.vue +28 -28
  394. package/icons/link-url.vue +3 -3
  395. package/icons/link.vue +7 -7
  396. package/icons/linkedin.vue +24 -24
  397. package/icons/list-checks.vue +3 -3
  398. package/icons/list-tree.vue +3 -3
  399. package/icons/loader-circle.vue +27 -27
  400. package/icons/location.vue +8 -8
  401. package/icons/lock.vue +5 -5
  402. package/icons/log-out.vue +21 -21
  403. package/icons/logo.vue +15 -15
  404. package/icons/logout.vue +16 -16
  405. package/icons/magnify/magnifying-glass-minus.vue +8 -8
  406. package/icons/magnify/magnifying-glass-plus.vue +9 -9
  407. package/icons/magnify/magnifying-glass.vue +9 -9
  408. package/icons/mail-question-mark.vue +8 -8
  409. package/icons/mail.vue +7 -7
  410. package/icons/mailru.vue +34 -34
  411. package/icons/main-filter.vue +10 -10
  412. package/icons/map/map-pin-filled.vue +7 -7
  413. package/icons/map/map-pin-rect.vue +15 -15
  414. package/icons/map/map-pin.vue +7 -7
  415. package/icons/map-marker-start.vue +7 -7
  416. package/icons/map-paper.vue +5 -5
  417. package/icons/medal.vue +7 -7
  418. package/icons/menu-filled.vue +20 -20
  419. package/icons/menu.vue +8 -8
  420. package/icons/message/message-square.vue +3 -3
  421. package/icons/message/message.vue +12 -12
  422. package/icons/message-dotted.vue +30 -30
  423. package/icons/messages-square.vue +3 -3
  424. package/icons/minus/minus-circle.vue +10 -10
  425. package/icons/money.vue +5 -5
  426. package/icons/monitoring.vue +10 -10
  427. package/icons/moon.vue +8 -8
  428. package/icons/more.vue +29 -29
  429. package/icons/network.vue +3 -3
  430. package/icons/newspaper.vue +3 -3
  431. package/icons/note-pencil.vue +14 -14
  432. package/icons/note.vue +14 -14
  433. package/icons/notification-error.vue +17 -17
  434. package/icons/notification.vue +16 -16
  435. package/icons/paperclip.vue +7 -7
  436. package/icons/payment/payment-card.vue +6 -6
  437. package/icons/payment/payment-kaspi.vue +11 -11
  438. package/icons/person-vector.vue +7 -7
  439. package/icons/person-with-briefcase.vue +10 -10
  440. package/icons/person.vue +22 -22
  441. package/icons/phone-call.vue +6 -6
  442. package/icons/phone-filled.vue +5 -5
  443. package/icons/phone-reversed.vue +3 -3
  444. package/icons/phone.vue +7 -7
  445. package/icons/plus/plus-circle.vue +8 -8
  446. package/icons/plus/plus.vue +13 -13
  447. package/icons/profile.vue +5 -5
  448. package/icons/radio/radio-check.vue +8 -8
  449. package/icons/radio/radio-empty.vue +10 -10
  450. package/icons/receipt.vue +8 -8
  451. package/icons/refresh-cw.vue +8 -8
  452. package/icons/robot-outline.vue +3 -3
  453. package/icons/robot.vue +14 -14
  454. package/icons/route.vue +7 -7
  455. package/icons/sanctions.vue +8 -8
  456. package/icons/scales/scale-outline.vue +3 -3
  457. package/icons/scales/scale.vue +16 -16
  458. package/icons/scales/scales.vue +5 -5
  459. package/icons/scales/standing-scales.vue +15 -15
  460. package/icons/scan-search.vue +3 -3
  461. package/icons/search-outline.vue +3 -3
  462. package/icons/search.vue +27 -27
  463. package/icons/send.vue +3 -3
  464. package/icons/settings.vue +26 -26
  465. package/icons/share/share-alt.vue +5 -5
  466. package/icons/share/share.vue +14 -14
  467. package/icons/shield-alert.vue +31 -31
  468. package/icons/shield-check.vue +20 -20
  469. package/icons/shield-verified.vue +14 -14
  470. package/icons/ship.vue +8 -8
  471. package/icons/shuffle.vue +3 -3
  472. package/icons/socials/face-book.vue +15 -15
  473. package/icons/socials/facebook-color.vue +16 -16
  474. package/icons/socials/globe-color.vue +16 -16
  475. package/icons/socials/instagram-color.vue +46 -46
  476. package/icons/socials/instagram.vue +19 -19
  477. package/icons/socials/linkedin-color.vue +14 -14
  478. package/icons/socials/telegram-color.vue +16 -16
  479. package/icons/socials/telegram.vue +15 -15
  480. package/icons/socials/tik-tok.vue +15 -15
  481. package/icons/socials/tiktok-color.vue +14 -14
  482. package/icons/socials/vk-color.vue +16 -16
  483. package/icons/socials/youtube-color.vue +16 -16
  484. package/icons/socials/youtube.vue +16 -16
  485. package/icons/sort/sort-asc.vue +15 -15
  486. package/icons/sort/sort-desc.vue +15 -15
  487. package/icons/sparkles.vue +3 -3
  488. package/icons/splitting-arrows.vue +8 -8
  489. package/icons/star/star-filled.vue +41 -41
  490. package/icons/star/star-half-filled.vue +21 -21
  491. package/icons/star/star.vue +25 -25
  492. package/icons/sun.vue +25 -25
  493. package/icons/sviazi.vue +5 -5
  494. package/icons/tag.vue +30 -30
  495. package/icons/tasks.vue +10 -10
  496. package/icons/tender-search.vue +11 -11
  497. package/icons/terminal.vue +3 -3
  498. package/icons/toasts/check-circle-toast.vue +6 -6
  499. package/icons/toasts/warning-triangle-toast.vue +8 -8
  500. package/icons/trash.vue +13 -13
  501. package/icons/triangle-alert.vue +3 -3
  502. package/icons/triangle.vue +10 -10
  503. package/icons/truck.vue +7 -7
  504. package/icons/user-plus.vue +3 -3
  505. package/icons/user-square.vue +14 -14
  506. package/icons/user.vue +22 -22
  507. package/icons/users-focus.vue +8 -8
  508. package/icons/users-outline.vue +3 -3
  509. package/icons/users-round.vue +3 -3
  510. package/icons/users-three.vue +7 -7
  511. package/icons/users.vue +8 -8
  512. package/icons/view-eye-open.vue +13 -13
  513. package/icons/wallet.vue +6 -6
  514. package/icons/warning/warning-circle.vue +29 -29
  515. package/icons/warning/warning-triangle-filled.vue +12 -12
  516. package/icons/warning/warning-triangle.vue +28 -28
  517. package/icons/webhook.vue +3 -3
  518. package/icons/whatsapp.vue +8 -8
  519. package/icons/work-bag.vue +11 -11
  520. package/icons/work-case.vue +9 -9
  521. package/icons/work-search.vue +10 -10
  522. package/icons/work.vue +5 -5
  523. package/icons/workflow.vue +3 -3
  524. package/icons/x-mark.vue +15 -15
  525. package/icons/x-outline.vue +3 -3
  526. package/icons/yandex.vue +29 -29
  527. package/icons/zap.vue +3 -3
  528. package/illustrations/address-location.vue +38 -38
  529. package/illustrations/award-badge.vue +105 -105
  530. package/illustrations/ball-with-chain.vue +120 -120
  531. package/illustrations/bill.vue +133 -133
  532. package/illustrations/buildings.vue +82 -82
  533. package/illustrations/calendar.vue +156 -156
  534. package/illustrations/chains.vue +152 -152
  535. package/illustrations/coin-percent.vue +126 -126
  536. package/illustrations/coins-stack.vue +202 -202
  537. package/illustrations/delete-dark.vue +32 -32
  538. package/illustrations/delete.vue +33 -33
  539. package/illustrations/doc-with-stamp.vue +126 -126
  540. package/illustrations/document.vue +64 -64
  541. package/illustrations/door.vue +85 -85
  542. package/illustrations/empty-badge-check.vue +20 -20
  543. package/illustrations/empty-ban.vue +18 -18
  544. package/illustrations/empty-box.vue +77 -77
  545. package/illustrations/empty-filter.vue +20 -20
  546. package/illustrations/empty-inbox.vue +19 -19
  547. package/illustrations/empty-scale.vue +22 -22
  548. package/illustrations/empty-search-x.vue +21 -21
  549. package/illustrations/empty-shopping-cart.vue +21 -21
  550. package/illustrations/empty-users-round.vue +20 -20
  551. package/illustrations/empty-wallet.vue +127 -127
  552. package/illustrations/graph-in-coin.vue +119 -119
  553. package/illustrations/hammer.vue +156 -156
  554. package/illustrations/hand-cash.vue +108 -108
  555. package/illustrations/info.vue +52 -52
  556. package/illustrations/mail-active.vue +86 -86
  557. package/illustrations/mail.vue +68 -68
  558. package/illustrations/no-access.vue +92 -92
  559. package/illustrations/notes-search.vue +20 -20
  560. package/illustrations/notes-sticky.vue +20 -20
  561. package/illustrations/ok.vue +90 -90
  562. package/illustrations/people-group.vue +237 -237
  563. package/illustrations/person-with-phone.vue +187 -187
  564. package/illustrations/person.vue +159 -159
  565. package/illustrations/phone-check.vue +110 -110
  566. package/illustrations/phone-payment-method.vue +223 -223
  567. package/illustrations/scheme-notes.vue +21 -21
  568. package/illustrations/scheme-waypoints.vue +25 -25
  569. package/illustrations/search.vue +14 -14
  570. package/illustrations/shield-user.vue +94 -94
  571. package/illustrations/stop-hand.vue +84 -84
  572. package/illustrations/stop-sign.vue +34 -34
  573. package/illustrations/suit.vue +97 -97
  574. package/illustrations/suitcase.vue +105 -105
  575. package/illustrations/terminal.vue +100 -100
  576. package/illustrations/trash-can.vue +66 -66
  577. package/illustrations/turn-on-tariff.vue +38 -38
  578. package/illustrations/two-persons.vue +169 -169
  579. package/lang/en.ts +882 -878
  580. package/lang/kk.ts +884 -880
  581. package/lang/ru.ts +884 -880
  582. package/layouts/default.vue +13 -13
  583. package/nuxt.config.ts +95 -95
  584. package/package.json +64 -64
  585. package/plugins/maska.ts +4 -4
  586. package/plugins/toast.client.ts +41 -41
  587. package/public/kaspi/logo.svg +4 -4
  588. package/shared/constans/pages.ts +96 -96
  589. package/tsconfig.json +6 -6
  590. package/utils/getMaxZindex.ts +25 -25
  591. package/utils/localizedNavigation.ts +36 -36
  592. package/utils/toCssSize.ts +11 -11
  593. package/utils/twMerge.ts +34 -34
  594. package/vitest.config.ts +14 -14
@@ -1,710 +1,710 @@
1
- <script generic="T" lang="ts" setup>
2
- import type { CascadeEntry, CascadeKeys, CascadeMode } from './types'
3
- import { toCssSize } from '#adata-ui/utils/toCssSize'
4
- import { autoUpdate, flip, size as floatingSize, offset, shift, useFloating } from '@floating-ui/vue'
5
- import { onClickOutside, useMediaQuery, useToggle, useWindowSize } from '@vueuse/core'
6
- import { isEqual } from 'lodash-es'
7
- import { buildCascadeIndex, isDisabled, isLeaf, nodeKey, nodeLabel, pathLabel, resolvePanels } from './cascade-utils'
8
-
9
- interface Props {
10
- label: string
11
- options: T[]
12
- required?: boolean
13
- keyToShow?: string
14
- keyToSelect?: string
15
- childrenKey?: string
16
- disabledKey?: string
17
- /** `true` — в v-model лежит ключ листа, `false` — сам объект листа. */
18
- getOnlyKey?: boolean
19
- clearable?: boolean
20
- disabled?: boolean
21
- size?: 'sm' | 'md'
22
- error?: string | string[]
23
- startIcon?: object
24
- buttonBg?: string
25
- /** Куда растут подпанели. `auto` — вправо, а если не влезает, влево. */
26
- panelsSide?: 'auto' | 'right' | 'left'
27
- /** Принудительный режим раскрытия вместо авто-выбора по ширине экрана. */
28
- submenuTrigger?: 'auto' | 'hover' | 'click'
29
- hoverOpenDelay?: number
30
- hoverCloseDelay?: number
31
- /** Ширина колонки. По умолчанию — ширина триггера. */
32
- panelWidth?: number | string
33
- /** Нижняя граница ширины колонки — когда триггер узкий, а подписи длинные. */
34
- panelMinWidth?: number | string
35
- panelMaxHeight?: number | string
36
- /** Подпись первой колонки. По умолчанию — `label`. */
37
- panelTitle?: string
38
- /** Показывать в триггере весь путь («Кварталы / 1 квартал 2026»). */
39
- showPathInTrigger?: boolean
40
- drawerTitle?: string
41
- /** Фиксированная общая высота мобильного дровера («70dvh»). */
42
- drawerHeight?: number | string
43
- drawerMaxHeight?: number | string
44
- backLabel?: string
45
- emptyLabel?: string
46
- mobileBreakpoint?: number
47
- tabletBreakpoint?: number
48
- teleport?: boolean
49
- /** Слой выпадашки и мобильного дровера. */
50
- zIndex?: number | string
51
- }
52
-
53
- interface Emits {
54
- (e: 'onChange', option: T, path: T[]): void
55
- (e: 'onClear'): void
56
- }
57
-
58
- /**
59
- * Каскадный селект: опция-родитель раскрывает своих детей отдельной колонкой сбоку.
60
- *
61
- * Три режима, выбираются по ширине экрана (или принудительно через `submenuTrigger`):
62
- * • `columns-hover` (десктоп) — подпанель по наведению;
63
- * • `columns-click` (планшет) — подпанель по клику, у дочерней колонки есть «Назад»;
64
- * • `drawer` (мобильный) — `a-drawer-v2` с drill-down по одной колонке.
65
- *
66
- * Выбирается ТОЛЬКО лист: узел с детьми лишь раскрывает ветку и значение не меняет.
67
- */
68
- defineOptions({ name: 'ACascadeSelectV2' })
69
-
70
- const props = withDefaults(defineProps<Props>(), {
71
- required: false,
72
- keyToShow: 'name',
73
- keyToSelect: 'id',
74
- childrenKey: 'children',
75
- disabledKey: undefined,
76
- getOnlyKey: false,
77
- clearable: true,
78
- disabled: false,
79
- size: 'sm',
80
- error: undefined,
81
- startIcon: undefined,
82
- buttonBg: '',
83
- panelsSide: 'auto',
84
- submenuTrigger: 'auto',
85
- hoverOpenDelay: 120,
86
- hoverCloseDelay: 200,
87
- panelWidth: undefined,
88
- panelMinWidth: undefined,
89
- panelMaxHeight: undefined,
90
- panelTitle: undefined,
91
- showPathInTrigger: false,
92
- drawerTitle: undefined,
93
- drawerHeight: undefined,
94
- drawerMaxHeight: undefined,
95
- backLabel: 'Назад',
96
- emptyLabel: 'Ничего не найдено',
97
- mobileBreakpoint: 768,
98
- tabletBreakpoint: 1024,
99
- teleport: true,
100
- zIndex: 10000,
101
- })
102
-
103
- const emits = defineEmits<Emits>()
104
-
105
- defineSlots<{
106
- 'option'?: (props: { option: T, level: number, selected: boolean, hasChildren: boolean, active: boolean }) => unknown
107
- 'single-selected'?: (props: { value: T, path: T[] }) => unknown
108
- 'empty'?: () => unknown
109
- }>()
110
-
111
- const modelValue = defineModel<T | unknown | null>({ required: true })
112
- const isOpen = defineModel<boolean>('open', { default: false })
113
-
114
- const keys = computed<CascadeKeys>(() => ({
115
- keyToShow: props.keyToShow,
116
- keyToSelect: props.keyToSelect,
117
- childrenKey: props.childrenKey,
118
- disabledKey: props.disabledKey,
119
- }))
120
-
121
- /* Один обход дерева на смену options: дальше все выборки по ключу — O(1). */
122
- const index = computed(() => buildCascadeIndex(props.options, keys.value))
123
-
124
- const selectedKey = computed<unknown>(() => {
125
- const value = modelValue.value
126
- if (value == null || value === '') return undefined
127
- return props.getOnlyKey ? value : nodeKey(value as T, keys.value)
128
- })
129
-
130
- const selectedEntry = computed<CascadeEntry<T> | null>(() =>
131
- selectedKey.value === undefined ? null : index.value.get(selectedKey.value) ?? null,
132
- )
133
-
134
- /** Ключи предков выбранного листа — ими раскрываем ветку при открытии. */
135
- const selectedAncestorKeys = computed<unknown[]>(() =>
136
- selectedEntry.value ? selectedEntry.value.keyPath.slice(0, -1) : [],
137
- )
138
-
139
- // -- Навигация -------------------------------------------------------------
140
-
141
- /** Цепочка ключей раскрытых родителей: activePath[i] раскрыт из колонки i. */
142
- const activePath = ref<unknown[]>([])
143
-
144
- const resolved = computed(() => resolvePanels(props.options, activePath.value, index.value, keys.value))
145
- const panels = computed<T[][]>(() => resolved.value.panels)
146
- /** Путь, обрезанный до реально разрешимого: options могли смениться под нами. */
147
- const resolvedPath = computed<unknown[]>(() => resolved.value.keyPath)
148
-
149
- // -- Режим -----------------------------------------------------------------
150
-
151
- const isMobileViewport = useMediaQuery(computed(() => `(max-width: ${props.mobileBreakpoint - 1}px)`))
152
- const isTabletViewport = useMediaQuery(
153
- computed(() => `(min-width: ${props.mobileBreakpoint}px) and (max-width: ${props.tabletBreakpoint - 1}px)`),
154
- )
155
-
156
- const mode = computed<CascadeMode>(() => {
157
- if (isMobileViewport.value) return 'drawer'
158
- if (props.submenuTrigger === 'hover') return 'columns-hover'
159
- if (props.submenuTrigger === 'click') return 'columns-click'
160
- return isTabletViewport.value ? 'columns-click' : 'columns-hover'
161
- })
162
-
163
- const isDrawerMode = computed(() => mode.value === 'drawer')
164
-
165
- /**
166
- * Планшет и дровер показывают одну колонку — текущую: вернуться к родителю есть чем
167
- * («Назад»), а вторая колонка только съедала бы ширину. Десктоп показывает все.
168
- * Абсолютный `level` сохраняем, чтобы срез не ломал индексацию путей и рефов.
169
- */
170
- const panelViews = computed<{ options: T[], level: number }[]>(() => {
171
- const all = panels.value.map((options, level) => ({ options, level }))
172
- if (isDrawerMode.value || mode.value === 'columns-click') return all.slice(-1)
173
- return all
174
- })
175
-
176
- /* undefined доезжает до a-cascade-drawer и там подхватывает его собственные дефолты. */
177
- const drawerHeightStyle = computed(() => toCssSize(props.drawerHeight))
178
- const drawerMaxHeightStyle = computed(() => toCssSize(props.drawerMaxHeight))
179
-
180
- const drawerView = computed(() => panelViews.value[0] ?? { options: props.options, level: 0 })
181
- const drawerParentLabel = computed(() => {
182
- const level = drawerView.value.level
183
- if (level === 0) return ''
184
- const entry = index.value.get(resolvedPath.value[level - 1])
185
- return entry ? nodeLabel(entry.node, keys.value) : ''
186
- })
187
-
188
- // -- Позиционирование ------------------------------------------------------
189
-
190
- const wrapper = ref<HTMLElement | null>(null)
191
- const triggerRef = ref<{ el: HTMLButtonElement | null, valueEl: HTMLElement | null } | null>(null)
192
- const reference = computed<HTMLElement | null>(() => triggerRef.value?.el ?? null)
193
- const floating = ref<HTMLElement | null>(null)
194
-
195
- const panelsDir = ref<'right' | 'left'>('right')
196
-
197
- const autoMaxHeight = ref('320px')
198
- /* Ширина триггера, снятая floating-ui: по ней колонки выравниваются с полем, как в ASelectV2. */
199
- const autoPanelWidth = ref<string>()
200
-
201
- const panelMaxHeightStyle = computed(() => toCssSize(props.panelMaxHeight) ?? autoMaxHeight.value)
202
- const panelWidthStyle = computed(() => toCssSize(props.panelWidth) ?? autoPanelWidth.value)
203
- const panelMinWidthStyle = computed(() => toCssSize(props.panelMinWidth))
204
-
205
- function pxOf(value?: string): number {
206
- const match = /^([\d.]+)px$/.exec(value ?? '')
207
- return match ? Number.parseFloat(match[1]) : 0
208
- }
209
-
210
- /** Фактическая ширина колонки в пикселях; 0 — размер задан нечисловой единицей, мерить нечем. */
211
- const panelWidthPx = computed(() => {
212
- const width = pxOf(panelWidthStyle.value)
213
- if (!width) return 0
214
- /* min-width может оказаться больше ширины триггера — тогда колонка шире, чем кажется. */
215
- return Math.max(width, pxOf(panelMinWidthStyle.value))
216
- })
217
-
218
- const { width: viewportWidth } = useWindowSize()
219
-
220
- /**
221
- * Колонки фиксированной ширины перестают влезать в экран уже на третьем уровне,
222
- * а `shift` умеет только сдвигать. Поэтому режем лишние слева — тем же приёмом,
223
- * что планшет (`slice(-2)`) и дровер (`slice(-1)`).
224
- */
225
- const visiblePanelViews = computed(() => {
226
- const all = panelViews.value
227
- const panelPx = panelWidthPx.value
228
- if (isDrawerMode.value || !panelPx || !viewportWidth.value) return all
229
-
230
- const gap = 8 /* gap-2 между колонками */
231
- const edges = 16 /* по 8px от краёв — столько же просит shift({ padding: 8 }) */
232
- const fits = Math.max(1, Math.floor((viewportWidth.value - edges + gap) / (panelPx + gap)))
233
- return all.length > fits ? all.slice(-fits) : all
234
- })
235
-
236
- /**
237
- * «Назад» нужен на самой мелкой видимой колонке: если срез (планшетный или по ширине экрана)
238
- * спрятал родителя, вернуться к нему больше нечем. На остальных колонках он был бы дублем.
239
- */
240
- function showBackFor(level: number) {
241
- if (level === 0) return false
242
- return level === (visiblePanelViews.value[0]?.level ?? 0)
243
- }
244
-
245
- const { x, y, strategy, update } = useFloating(reference, floating, {
246
- /* Колонки растут вправо от левого края триггера; в зеркальном режиме — влево от правого. */
247
- placement: computed(() => (panelsDir.value === 'left' ? 'bottom-end' : 'bottom-start')),
248
- strategy: 'fixed',
249
- middleware: computed(() => [
250
- offset(6),
251
- flip({ fallbackPlacements: panelsDir.value === 'left' ? ['top-end'] : ['top-start'] }),
252
- shift({ padding: 8 }),
253
- floatingSize({
254
- /* Пишем в ref только на смену значения: apply зовётся на каждый кадр autoUpdate. */
255
- apply({ availableHeight, rects }) {
256
- const nextHeight = `${Math.max(160, Math.min(availableHeight, 360))}px`
257
- if (nextHeight !== autoMaxHeight.value) autoMaxHeight.value = nextHeight
258
-
259
- const nextWidth = `${Math.round(rects.reference.width)}px`
260
- if (nextWidth !== autoPanelWidth.value) autoPanelWidth.value = nextWidth
261
- },
262
- }),
263
- ]),
264
- })
265
-
266
- /* Только на смену: запись меняет placement, тот дёргает update — иначе петля. */
267
- function setDirection(next: 'right' | 'left') {
268
- if (next !== panelsDir.value) panelsDir.value = next
269
- }
270
-
271
- /**
272
- * Сторону выбираем один раз на изменение набора колонок, а не внутри middleware:
273
- * измерение в `apply` меняло бы placement, тот — измерение, и получалась бы петля.
274
- */
275
- function measureDirection() {
276
- if (props.panelsSide !== 'auto') {
277
- setDirection(props.panelsSide)
278
- return
279
- }
280
- const rect = reference.value?.getBoundingClientRect()
281
- const el = floating.value
282
- if (!rect || !el) return
283
- const needed = el.scrollWidth
284
- const spaceRight = window.innerWidth - rect.left - 8
285
- const spaceLeft = rect.right - 8
286
- setDirection(needed > spaceRight && spaceLeft > spaceRight ? 'left' : 'right')
287
- }
288
-
289
- watch(() => props.panelsSide, () => measureDirection(), { immediate: true })
290
- watch([visiblePanelViews, isOpen], () => {
291
- if (!isOpen.value || isDrawerMode.value) return
292
- nextTick(measureDirection)
293
- })
294
-
295
- let cleanup: (() => void) | null = null
296
- watch([isOpen, isDrawerMode], () => {
297
- cleanup?.()
298
- cleanup = null
299
- if (!isOpen.value || isDrawerMode.value) return
300
- nextTick(() => {
301
- if (reference.value && floating.value) {
302
- cleanup = autoUpdate(reference.value, floating.value, () => {
303
- update()
304
- measureDirection()
305
- })
306
- }
307
- })
308
- })
309
-
310
- // -- Таймеры hover-intent (по образцу useTooltipTrigger) --------------------
311
-
312
- let openTimer: ReturnType<typeof setTimeout> | null = null
313
- let collapseTimer: ReturnType<typeof setTimeout> | null = null
314
-
315
- function clearOpenTimer() {
316
- if (openTimer) {
317
- clearTimeout(openTimer)
318
- openTimer = null
319
- }
320
- }
321
-
322
- function clearCollapseTimer() {
323
- if (collapseTimer) {
324
- clearTimeout(collapseTimer)
325
- collapseTimer = null
326
- }
327
- }
328
-
329
- onUnmounted(() => {
330
- clearOpenTimer()
331
- clearCollapseTimer()
332
- cleanup?.()
333
- })
334
-
335
- // -- Действия --------------------------------------------------------------
336
-
337
- const toggleOpen = useToggle(isOpen)
338
-
339
- function openBranch(level: number, node: T) {
340
- activePath.value = [...resolvedPath.value.slice(0, level), nodeKey(node, keys.value)]
341
- }
342
-
343
- function collapseTo(level: number) {
344
- activePath.value = resolvedPath.value.slice(0, level)
345
- }
346
-
347
- function selectLeaf(node: T) {
348
- const entry = index.value.get(nodeKey(node, keys.value))
349
- modelValue.value = props.getOnlyKey ? nodeKey(node, keys.value) : node
350
- emits('onChange', node, entry?.path ?? [node])
351
- toggleOpen(false)
352
- }
353
-
354
- function activate(level: number, node: T) {
355
- if (isDisabled(node, keys.value)) return
356
- clearOpenTimer()
357
- clearCollapseTimer()
358
- if (isLeaf(node, keys.value)) {
359
- selectLeaf(node)
360
- return
361
- }
362
- openBranch(level, node)
363
- focusRow(level + 1, firstEnabledKey(level + 1))
364
- }
365
-
366
- function goBackFrom(level: number) {
367
- activePath.value = resolvedPath.value.slice(0, Math.max(0, level - 1))
368
- }
369
-
370
- function goBack() {
371
- goBackFrom(drawerView.value.level)
372
- }
373
-
374
- function onClear() {
375
- modelValue.value = null
376
- activePath.value = []
377
- emits('onClear')
378
- }
379
-
380
- defineExpose({ onClear })
381
-
382
- // -- Hover -----------------------------------------------------------------
383
-
384
- function onRowHover(level: number, node: T) {
385
- if (mode.value !== 'columns-hover') return
386
- clearCollapseTimer()
387
- clearOpenTimer()
388
- openTimer = setTimeout(() => {
389
- openTimer = null
390
- if (isDisabled(node, keys.value)) return
391
- if (isLeaf(node, keys.value)) collapseTo(level)
392
- else openBranch(level, node)
393
- }, props.hoverOpenDelay)
394
- }
395
-
396
- function onPanelsEnter() {
397
- clearCollapseTimer()
398
- }
399
-
400
- /* Уводя мышь со всего каскада, схлопываем его до ветки выбранного значения. */
401
- function onPanelsLeave() {
402
- clearOpenTimer()
403
- if (mode.value !== 'columns-hover') return
404
- clearCollapseTimer()
405
- collapseTimer = setTimeout(() => {
406
- collapseTimer = null
407
- activePath.value = selectedAncestorKeys.value
408
- }, props.hoverCloseDelay)
409
- }
410
-
411
- // -- Клавиатура ------------------------------------------------------------
412
-
413
- const focused = ref<{ level: number, key: unknown } | null>(null)
414
- const panelRefs = ref<Record<number, { focusItem: (key: unknown) => void } | null>>({})
415
-
416
- function setPanelRef(el: unknown, level: number) {
417
- if (el == null) delete panelRefs.value[level]
418
- else panelRefs.value[level] = el as { focusItem: (key: unknown) => void }
419
- }
420
-
421
- function enabledRows(level: number): T[] {
422
- return (panels.value[level] ?? []).filter(node => !isDisabled(node, keys.value))
423
- }
424
-
425
- function firstEnabledKey(level: number): unknown {
426
- const rows = enabledRows(level)
427
- return rows.length ? nodeKey(rows[0], keys.value) : undefined
428
- }
429
-
430
- function focusRow(level: number, key: unknown) {
431
- if (key === undefined || isDrawerMode.value) return
432
- focused.value = { level, key }
433
- nextTick(() => panelRefs.value[level]?.focusItem(key))
434
- }
435
-
436
- function nodeByKey(level: number, key: unknown): T | undefined {
437
- return (panels.value[level] ?? []).find(node => isEqual(nodeKey(node, keys.value), key))
438
- }
439
-
440
- function moveWithin(level: number, delta: number) {
441
- const rows = enabledRows(level)
442
- if (!rows.length) return
443
- const current = rows.findIndex(node => isEqual(nodeKey(node, keys.value), focused.value?.key))
444
- const next = current === -1
445
- ? (delta > 0 ? 0 : rows.length - 1)
446
- : (current + delta + rows.length) % rows.length
447
- focusRow(level, nodeKey(rows[next], keys.value))
448
- }
449
-
450
- function focusInitial() {
451
- if (isDrawerMode.value) return
452
- const entry = selectedEntry.value
453
- if (entry) {
454
- focusRow(entry.level, selectedKey.value)
455
- return
456
- }
457
- focusRow(0, firstEnabledKey(0))
458
- }
459
-
460
- function closeAndRestoreFocus() {
461
- toggleOpen(false)
462
- nextTick(() => reference.value?.focus())
463
- }
464
-
465
- function onKeydown(event: KeyboardEvent) {
466
- if (props.disabled) return
467
-
468
- if (!isOpen.value) {
469
- if (event.key === 'ArrowDown') {
470
- event.preventDefault()
471
- toggleOpen(true)
472
- }
473
- return
474
- }
475
-
476
- if (event.key === 'Escape') {
477
- event.preventDefault()
478
- closeAndRestoreFocus()
479
- return
480
- }
481
-
482
- if (event.key === 'Tab') {
483
- toggleOpen(false)
484
- return
485
- }
486
-
487
- if (isDrawerMode.value) return
488
-
489
- const level = focused.value?.level ?? 0
490
-
491
- switch (event.key) {
492
- case 'ArrowDown':
493
- event.preventDefault()
494
- moveWithin(level, 1)
495
- break
496
- case 'ArrowUp':
497
- event.preventDefault()
498
- moveWithin(level, -1)
499
- break
500
- case 'Home':
501
- event.preventDefault()
502
- focusRow(level, firstEnabledKey(level))
503
- break
504
- case 'End': {
505
- event.preventDefault()
506
- const rows = enabledRows(level)
507
- if (rows.length) focusRow(level, nodeKey(rows[rows.length - 1], keys.value))
508
- break
509
- }
510
- case 'ArrowRight': {
511
- const node = focused.value ? nodeByKey(level, focused.value.key) : undefined
512
- if (!node || isLeaf(node, keys.value)) break
513
- event.preventDefault()
514
- openBranch(level, node)
515
- focusRow(level + 1, firstEnabledKey(level + 1))
516
- break
517
- }
518
- case 'ArrowLeft': {
519
- if (level === 0) break
520
- event.preventDefault()
521
- const parentKey = resolvedPath.value[level - 1]
522
- goBackFrom(level)
523
- focusRow(level - 1, parentKey)
524
- break
525
- }
526
- }
527
- }
528
-
529
- // -- Открытие / закрытие ---------------------------------------------------
530
-
531
- watch(isOpen, (open) => {
532
- clearOpenTimer()
533
- clearCollapseTimer()
534
- if (open) {
535
- activePath.value = selectedAncestorKeys.value
536
- /* Ширину берём с триггера сразу: иначе колонка мигнёт фолбэком min-w-45 и прыгнет. */
537
- const triggerWidth = reference.value?.offsetWidth
538
- if (triggerWidth) autoPanelWidth.value = `${Math.round(triggerWidth)}px`
539
- nextTick(focusInitial)
540
- }
541
- else {
542
- focused.value = null
543
- }
544
- })
545
-
546
- onClickOutside(floating, () => {
547
- if (isOpen.value && !isDrawerMode.value) toggleOpen(false)
548
- }, { ignore: [reference] })
549
-
550
- // -- Отображение в триггере ------------------------------------------------
551
-
552
- const displayValue = computed(() => {
553
- const entry = selectedEntry.value
554
- if (!entry) return ''
555
- return props.showPathInTrigger
556
- ? pathLabel(entry.path, keys.value)
557
- : nodeLabel(entry.node, keys.value)
558
- })
559
-
560
- const rootPanelTitle = computed(() => props.panelTitle ?? props.label)
561
- </script>
562
-
563
- <template>
564
- <div
565
- ref="wrapper"
566
- class="cascade-select relative w-full text-body-md"
567
- @keydown="onKeydown"
568
- >
569
- <a-select-trigger
570
- ref="triggerRef"
571
- :label="label"
572
- :required="required"
573
- :size="size"
574
- :disabled="disabled"
575
- :error="error"
576
- :is-open="isOpen"
577
- :start-icon="startIcon"
578
- :button-bg="buttonBg"
579
- :clearable="clearable"
580
- :show-clear="clearable && !!selectedEntry"
581
- floating-label
582
- :has-value="!!selectedEntry"
583
- @toggle="toggleOpen()"
584
- @clear="onClear"
585
- >
586
- <span v-if="selectedEntry" class="block truncate leading-tight">
587
- <slot
588
- name="single-selected"
589
- :value="selectedEntry.node"
590
- :path="selectedEntry.path"
591
- >{{ displayValue }}</slot>
592
- </span>
593
- <span v-else class="text-secondary">&#8203;</span>
594
- </a-select-trigger>
595
-
596
- <client-only>
597
- <teleport
598
- to="body"
599
- :disabled="!teleport"
600
- >
601
- <transition
602
- enter-active-class="cascade-select__enter-active"
603
- enter-from-class="cascade-select__enter-from"
604
- enter-to-class="cascade-select__enter-to"
605
- leave-active-class="cascade-select__leave-active"
606
- leave-from-class="cascade-select__leave-from"
607
- leave-to-class="cascade-select__leave-to"
608
- >
609
- <div
610
- v-if="isOpen && !isDrawerMode"
611
- ref="floating"
612
- class="cascade-select__panels flex items-start gap-2"
613
- :class="{ 'flex-row-reverse': panelsDir === 'left' }"
614
- :style="{
615
- position: strategy,
616
- left: x != null ? `${x}px` : '',
617
- top: y != null ? `${y}px` : '',
618
- zIndex,
619
- }"
620
- @pointerenter="onPanelsEnter"
621
- @pointerleave="onPanelsLeave"
622
- @keydown="onKeydown"
623
- >
624
- <a-cascade-panel
625
- v-for="view in visiblePanelViews"
626
- :key="view.level"
627
- :ref="(el: any) => setPanelRef(el, view.level)"
628
- :options="view.options"
629
- :keys="keys"
630
- :level="view.level"
631
- :active-key="resolvedPath[view.level]"
632
- :selected-key="selectedKey"
633
- :focused-key="focused?.level === view.level ? focused?.key : undefined"
634
- :title="view.level === 0 ? rootPanelTitle : ''"
635
- :show-back="showBackFor(view.level)"
636
- :mirrored="panelsDir === 'left'"
637
- :back-label="backLabel"
638
- :width="panelWidthStyle"
639
- :min-width="panelMinWidthStyle"
640
- :max-height="panelMaxHeightStyle"
641
- :empty-label="emptyLabel"
642
- @activate="activate(view.level, $event)"
643
- @hover="onRowHover(view.level, $event)"
644
- @back="goBackFrom(view.level)"
645
- >
646
- <template #option="slotProps">
647
- <slot name="option" v-bind="slotProps" />
648
- </template>
649
- <template #empty>
650
- <slot name="empty" />
651
- </template>
652
- </a-cascade-panel>
653
- </div>
654
- </transition>
655
- </teleport>
656
-
657
- <a-cascade-drawer
658
- v-if="isDrawerMode"
659
- v-model="isOpen"
660
- :options="drawerView.options"
661
- :keys="keys"
662
- :level="drawerView.level"
663
- :title="drawerTitle ?? label"
664
- :parent-label="drawerParentLabel"
665
- :back-label="backLabel"
666
- :selected-key="selectedKey"
667
- :height="drawerHeightStyle"
668
- :max-height="drawerMaxHeightStyle"
669
- :z-index="zIndex"
670
- @activate="activate(drawerView.level, $event)"
671
- @back="goBack"
672
- >
673
- <template #option="slotProps">
674
- <slot name="option" v-bind="slotProps" />
675
- </template>
676
- <template #empty>
677
- <slot name="empty" />
678
- </template>
679
- </a-cascade-drawer>
680
- </client-only>
681
- </div>
682
- </template>
683
-
684
- <style scoped>
685
- .cascade-select__enter-active,
686
- .cascade-select__leave-active {
687
- transition:
688
- opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
689
- transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
690
- }
691
-
692
- .cascade-select__enter-from,
693
- .cascade-select__leave-to {
694
- opacity: 0;
695
- transform: translateY(-4px);
696
- }
697
-
698
- .cascade-select__enter-to,
699
- .cascade-select__leave-from {
700
- opacity: 1;
701
- transform: translateY(0);
702
- }
703
-
704
- @media (prefers-reduced-motion: reduce) {
705
- .cascade-select__enter-active,
706
- .cascade-select__leave-active {
707
- transition: none;
708
- }
709
- }
710
- </style>
1
+ <script generic="T" lang="ts" setup>
2
+ import type { CascadeEntry, CascadeKeys, CascadeMode } from './types'
3
+ import { toCssSize } from '#adata-ui/utils/toCssSize'
4
+ import { autoUpdate, flip, size as floatingSize, offset, shift, useFloating } from '@floating-ui/vue'
5
+ import { onClickOutside, useMediaQuery, useToggle, useWindowSize } from '@vueuse/core'
6
+ import { isEqual } from 'lodash-es'
7
+ import { buildCascadeIndex, isDisabled, isLeaf, nodeKey, nodeLabel, pathLabel, resolvePanels } from './cascade-utils'
8
+
9
+ interface Props {
10
+ label: string
11
+ options: T[]
12
+ required?: boolean
13
+ keyToShow?: string
14
+ keyToSelect?: string
15
+ childrenKey?: string
16
+ disabledKey?: string
17
+ /** `true` — в v-model лежит ключ листа, `false` — сам объект листа. */
18
+ getOnlyKey?: boolean
19
+ clearable?: boolean
20
+ disabled?: boolean
21
+ size?: 'sm' | 'md'
22
+ error?: string | string[]
23
+ startIcon?: object
24
+ buttonBg?: string
25
+ /** Куда растут подпанели. `auto` — вправо, а если не влезает, влево. */
26
+ panelsSide?: 'auto' | 'right' | 'left'
27
+ /** Принудительный режим раскрытия вместо авто-выбора по ширине экрана. */
28
+ submenuTrigger?: 'auto' | 'hover' | 'click'
29
+ hoverOpenDelay?: number
30
+ hoverCloseDelay?: number
31
+ /** Ширина колонки. По умолчанию — ширина триггера. */
32
+ panelWidth?: number | string
33
+ /** Нижняя граница ширины колонки — когда триггер узкий, а подписи длинные. */
34
+ panelMinWidth?: number | string
35
+ panelMaxHeight?: number | string
36
+ /** Подпись первой колонки. По умолчанию — `label`. */
37
+ panelTitle?: string
38
+ /** Показывать в триггере весь путь («Кварталы / 1 квартал 2026»). */
39
+ showPathInTrigger?: boolean
40
+ drawerTitle?: string
41
+ /** Фиксированная общая высота мобильного дровера («70dvh»). */
42
+ drawerHeight?: number | string
43
+ drawerMaxHeight?: number | string
44
+ backLabel?: string
45
+ emptyLabel?: string
46
+ mobileBreakpoint?: number
47
+ tabletBreakpoint?: number
48
+ teleport?: boolean
49
+ /** Слой выпадашки и мобильного дровера. */
50
+ zIndex?: number | string
51
+ }
52
+
53
+ interface Emits {
54
+ (e: 'onChange', option: T, path: T[]): void
55
+ (e: 'onClear'): void
56
+ }
57
+
58
+ /**
59
+ * Каскадный селект: опция-родитель раскрывает своих детей отдельной колонкой сбоку.
60
+ *
61
+ * Три режима, выбираются по ширине экрана (или принудительно через `submenuTrigger`):
62
+ * • `columns-hover` (десктоп) — подпанель по наведению;
63
+ * • `columns-click` (планшет) — подпанель по клику, у дочерней колонки есть «Назад»;
64
+ * • `drawer` (мобильный) — `a-drawer-v2` с drill-down по одной колонке.
65
+ *
66
+ * Выбирается ТОЛЬКО лист: узел с детьми лишь раскрывает ветку и значение не меняет.
67
+ */
68
+ defineOptions({ name: 'ACascadeSelectV2' })
69
+
70
+ const props = withDefaults(defineProps<Props>(), {
71
+ required: false,
72
+ keyToShow: 'name',
73
+ keyToSelect: 'id',
74
+ childrenKey: 'children',
75
+ disabledKey: undefined,
76
+ getOnlyKey: false,
77
+ clearable: true,
78
+ disabled: false,
79
+ size: 'sm',
80
+ error: undefined,
81
+ startIcon: undefined,
82
+ buttonBg: '',
83
+ panelsSide: 'auto',
84
+ submenuTrigger: 'auto',
85
+ hoverOpenDelay: 120,
86
+ hoverCloseDelay: 200,
87
+ panelWidth: undefined,
88
+ panelMinWidth: undefined,
89
+ panelMaxHeight: undefined,
90
+ panelTitle: undefined,
91
+ showPathInTrigger: false,
92
+ drawerTitle: undefined,
93
+ drawerHeight: undefined,
94
+ drawerMaxHeight: undefined,
95
+ backLabel: 'Назад',
96
+ emptyLabel: 'Ничего не найдено',
97
+ mobileBreakpoint: 768,
98
+ tabletBreakpoint: 1024,
99
+ teleport: true,
100
+ zIndex: 10000,
101
+ })
102
+
103
+ const emits = defineEmits<Emits>()
104
+
105
+ defineSlots<{
106
+ 'option'?: (props: { option: T, level: number, selected: boolean, hasChildren: boolean, active: boolean }) => unknown
107
+ 'single-selected'?: (props: { value: T, path: T[] }) => unknown
108
+ 'empty'?: () => unknown
109
+ }>()
110
+
111
+ const modelValue = defineModel<T | unknown | null>({ required: true })
112
+ const isOpen = defineModel<boolean>('open', { default: false })
113
+
114
+ const keys = computed<CascadeKeys>(() => ({
115
+ keyToShow: props.keyToShow,
116
+ keyToSelect: props.keyToSelect,
117
+ childrenKey: props.childrenKey,
118
+ disabledKey: props.disabledKey,
119
+ }))
120
+
121
+ /* Один обход дерева на смену options: дальше все выборки по ключу — O(1). */
122
+ const index = computed(() => buildCascadeIndex(props.options, keys.value))
123
+
124
+ const selectedKey = computed<unknown>(() => {
125
+ const value = modelValue.value
126
+ if (value == null || value === '') return undefined
127
+ return props.getOnlyKey ? value : nodeKey(value as T, keys.value)
128
+ })
129
+
130
+ const selectedEntry = computed<CascadeEntry<T> | null>(() =>
131
+ selectedKey.value === undefined ? null : index.value.get(selectedKey.value) ?? null,
132
+ )
133
+
134
+ /** Ключи предков выбранного листа — ими раскрываем ветку при открытии. */
135
+ const selectedAncestorKeys = computed<unknown[]>(() =>
136
+ selectedEntry.value ? selectedEntry.value.keyPath.slice(0, -1) : [],
137
+ )
138
+
139
+ // -- Навигация -------------------------------------------------------------
140
+
141
+ /** Цепочка ключей раскрытых родителей: activePath[i] раскрыт из колонки i. */
142
+ const activePath = ref<unknown[]>([])
143
+
144
+ const resolved = computed(() => resolvePanels(props.options, activePath.value, index.value, keys.value))
145
+ const panels = computed<T[][]>(() => resolved.value.panels)
146
+ /** Путь, обрезанный до реально разрешимого: options могли смениться под нами. */
147
+ const resolvedPath = computed<unknown[]>(() => resolved.value.keyPath)
148
+
149
+ // -- Режим -----------------------------------------------------------------
150
+
151
+ const isMobileViewport = useMediaQuery(computed(() => `(max-width: ${props.mobileBreakpoint - 1}px)`))
152
+ const isTabletViewport = useMediaQuery(
153
+ computed(() => `(min-width: ${props.mobileBreakpoint}px) and (max-width: ${props.tabletBreakpoint - 1}px)`),
154
+ )
155
+
156
+ const mode = computed<CascadeMode>(() => {
157
+ if (isMobileViewport.value) return 'drawer'
158
+ if (props.submenuTrigger === 'hover') return 'columns-hover'
159
+ if (props.submenuTrigger === 'click') return 'columns-click'
160
+ return isTabletViewport.value ? 'columns-click' : 'columns-hover'
161
+ })
162
+
163
+ const isDrawerMode = computed(() => mode.value === 'drawer')
164
+
165
+ /**
166
+ * Планшет и дровер показывают одну колонку — текущую: вернуться к родителю есть чем
167
+ * («Назад»), а вторая колонка только съедала бы ширину. Десктоп показывает все.
168
+ * Абсолютный `level` сохраняем, чтобы срез не ломал индексацию путей и рефов.
169
+ */
170
+ const panelViews = computed<{ options: T[], level: number }[]>(() => {
171
+ const all = panels.value.map((options, level) => ({ options, level }))
172
+ if (isDrawerMode.value || mode.value === 'columns-click') return all.slice(-1)
173
+ return all
174
+ })
175
+
176
+ /* undefined доезжает до a-cascade-drawer и там подхватывает его собственные дефолты. */
177
+ const drawerHeightStyle = computed(() => toCssSize(props.drawerHeight))
178
+ const drawerMaxHeightStyle = computed(() => toCssSize(props.drawerMaxHeight))
179
+
180
+ const drawerView = computed(() => panelViews.value[0] ?? { options: props.options, level: 0 })
181
+ const drawerParentLabel = computed(() => {
182
+ const level = drawerView.value.level
183
+ if (level === 0) return ''
184
+ const entry = index.value.get(resolvedPath.value[level - 1])
185
+ return entry ? nodeLabel(entry.node, keys.value) : ''
186
+ })
187
+
188
+ // -- Позиционирование ------------------------------------------------------
189
+
190
+ const wrapper = ref<HTMLElement | null>(null)
191
+ const triggerRef = ref<{ el: HTMLButtonElement | null, valueEl: HTMLElement | null } | null>(null)
192
+ const reference = computed<HTMLElement | null>(() => triggerRef.value?.el ?? null)
193
+ const floating = ref<HTMLElement | null>(null)
194
+
195
+ const panelsDir = ref<'right' | 'left'>('right')
196
+
197
+ const autoMaxHeight = ref('320px')
198
+ /* Ширина триггера, снятая floating-ui: по ней колонки выравниваются с полем, как в ASelectV2. */
199
+ const autoPanelWidth = ref<string>()
200
+
201
+ const panelMaxHeightStyle = computed(() => toCssSize(props.panelMaxHeight) ?? autoMaxHeight.value)
202
+ const panelWidthStyle = computed(() => toCssSize(props.panelWidth) ?? autoPanelWidth.value)
203
+ const panelMinWidthStyle = computed(() => toCssSize(props.panelMinWidth))
204
+
205
+ function pxOf(value?: string): number {
206
+ const match = /^([\d.]+)px$/.exec(value ?? '')
207
+ return match ? Number.parseFloat(match[1]) : 0
208
+ }
209
+
210
+ /** Фактическая ширина колонки в пикселях; 0 — размер задан нечисловой единицей, мерить нечем. */
211
+ const panelWidthPx = computed(() => {
212
+ const width = pxOf(panelWidthStyle.value)
213
+ if (!width) return 0
214
+ /* min-width может оказаться больше ширины триггера — тогда колонка шире, чем кажется. */
215
+ return Math.max(width, pxOf(panelMinWidthStyle.value))
216
+ })
217
+
218
+ const { width: viewportWidth } = useWindowSize()
219
+
220
+ /**
221
+ * Колонки фиксированной ширины перестают влезать в экран уже на третьем уровне,
222
+ * а `shift` умеет только сдвигать. Поэтому режем лишние слева — тем же приёмом,
223
+ * что планшет (`slice(-2)`) и дровер (`slice(-1)`).
224
+ */
225
+ const visiblePanelViews = computed(() => {
226
+ const all = panelViews.value
227
+ const panelPx = panelWidthPx.value
228
+ if (isDrawerMode.value || !panelPx || !viewportWidth.value) return all
229
+
230
+ const gap = 8 /* gap-2 между колонками */
231
+ const edges = 16 /* по 8px от краёв — столько же просит shift({ padding: 8 }) */
232
+ const fits = Math.max(1, Math.floor((viewportWidth.value - edges + gap) / (panelPx + gap)))
233
+ return all.length > fits ? all.slice(-fits) : all
234
+ })
235
+
236
+ /**
237
+ * «Назад» нужен на самой мелкой видимой колонке: если срез (планшетный или по ширине экрана)
238
+ * спрятал родителя, вернуться к нему больше нечем. На остальных колонках он был бы дублем.
239
+ */
240
+ function showBackFor(level: number) {
241
+ if (level === 0) return false
242
+ return level === (visiblePanelViews.value[0]?.level ?? 0)
243
+ }
244
+
245
+ const { x, y, strategy, update } = useFloating(reference, floating, {
246
+ /* Колонки растут вправо от левого края триггера; в зеркальном режиме — влево от правого. */
247
+ placement: computed(() => (panelsDir.value === 'left' ? 'bottom-end' : 'bottom-start')),
248
+ strategy: 'fixed',
249
+ middleware: computed(() => [
250
+ offset(6),
251
+ flip({ fallbackPlacements: panelsDir.value === 'left' ? ['top-end'] : ['top-start'] }),
252
+ shift({ padding: 8 }),
253
+ floatingSize({
254
+ /* Пишем в ref только на смену значения: apply зовётся на каждый кадр autoUpdate. */
255
+ apply({ availableHeight, rects }) {
256
+ const nextHeight = `${Math.max(160, Math.min(availableHeight, 360))}px`
257
+ if (nextHeight !== autoMaxHeight.value) autoMaxHeight.value = nextHeight
258
+
259
+ const nextWidth = `${Math.round(rects.reference.width)}px`
260
+ if (nextWidth !== autoPanelWidth.value) autoPanelWidth.value = nextWidth
261
+ },
262
+ }),
263
+ ]),
264
+ })
265
+
266
+ /* Только на смену: запись меняет placement, тот дёргает update — иначе петля. */
267
+ function setDirection(next: 'right' | 'left') {
268
+ if (next !== panelsDir.value) panelsDir.value = next
269
+ }
270
+
271
+ /**
272
+ * Сторону выбираем один раз на изменение набора колонок, а не внутри middleware:
273
+ * измерение в `apply` меняло бы placement, тот — измерение, и получалась бы петля.
274
+ */
275
+ function measureDirection() {
276
+ if (props.panelsSide !== 'auto') {
277
+ setDirection(props.panelsSide)
278
+ return
279
+ }
280
+ const rect = reference.value?.getBoundingClientRect()
281
+ const el = floating.value
282
+ if (!rect || !el) return
283
+ const needed = el.scrollWidth
284
+ const spaceRight = window.innerWidth - rect.left - 8
285
+ const spaceLeft = rect.right - 8
286
+ setDirection(needed > spaceRight && spaceLeft > spaceRight ? 'left' : 'right')
287
+ }
288
+
289
+ watch(() => props.panelsSide, () => measureDirection(), { immediate: true })
290
+ watch([visiblePanelViews, isOpen], () => {
291
+ if (!isOpen.value || isDrawerMode.value) return
292
+ nextTick(measureDirection)
293
+ })
294
+
295
+ let cleanup: (() => void) | null = null
296
+ watch([isOpen, isDrawerMode], () => {
297
+ cleanup?.()
298
+ cleanup = null
299
+ if (!isOpen.value || isDrawerMode.value) return
300
+ nextTick(() => {
301
+ if (reference.value && floating.value) {
302
+ cleanup = autoUpdate(reference.value, floating.value, () => {
303
+ update()
304
+ measureDirection()
305
+ })
306
+ }
307
+ })
308
+ })
309
+
310
+ // -- Таймеры hover-intent (по образцу useTooltipTrigger) --------------------
311
+
312
+ let openTimer: ReturnType<typeof setTimeout> | null = null
313
+ let collapseTimer: ReturnType<typeof setTimeout> | null = null
314
+
315
+ function clearOpenTimer() {
316
+ if (openTimer) {
317
+ clearTimeout(openTimer)
318
+ openTimer = null
319
+ }
320
+ }
321
+
322
+ function clearCollapseTimer() {
323
+ if (collapseTimer) {
324
+ clearTimeout(collapseTimer)
325
+ collapseTimer = null
326
+ }
327
+ }
328
+
329
+ onUnmounted(() => {
330
+ clearOpenTimer()
331
+ clearCollapseTimer()
332
+ cleanup?.()
333
+ })
334
+
335
+ // -- Действия --------------------------------------------------------------
336
+
337
+ const toggleOpen = useToggle(isOpen)
338
+
339
+ function openBranch(level: number, node: T) {
340
+ activePath.value = [...resolvedPath.value.slice(0, level), nodeKey(node, keys.value)]
341
+ }
342
+
343
+ function collapseTo(level: number) {
344
+ activePath.value = resolvedPath.value.slice(0, level)
345
+ }
346
+
347
+ function selectLeaf(node: T) {
348
+ const entry = index.value.get(nodeKey(node, keys.value))
349
+ modelValue.value = props.getOnlyKey ? nodeKey(node, keys.value) : node
350
+ emits('onChange', node, entry?.path ?? [node])
351
+ toggleOpen(false)
352
+ }
353
+
354
+ function activate(level: number, node: T) {
355
+ if (isDisabled(node, keys.value)) return
356
+ clearOpenTimer()
357
+ clearCollapseTimer()
358
+ if (isLeaf(node, keys.value)) {
359
+ selectLeaf(node)
360
+ return
361
+ }
362
+ openBranch(level, node)
363
+ focusRow(level + 1, firstEnabledKey(level + 1))
364
+ }
365
+
366
+ function goBackFrom(level: number) {
367
+ activePath.value = resolvedPath.value.slice(0, Math.max(0, level - 1))
368
+ }
369
+
370
+ function goBack() {
371
+ goBackFrom(drawerView.value.level)
372
+ }
373
+
374
+ function onClear() {
375
+ modelValue.value = null
376
+ activePath.value = []
377
+ emits('onClear')
378
+ }
379
+
380
+ defineExpose({ onClear })
381
+
382
+ // -- Hover -----------------------------------------------------------------
383
+
384
+ function onRowHover(level: number, node: T) {
385
+ if (mode.value !== 'columns-hover') return
386
+ clearCollapseTimer()
387
+ clearOpenTimer()
388
+ openTimer = setTimeout(() => {
389
+ openTimer = null
390
+ if (isDisabled(node, keys.value)) return
391
+ if (isLeaf(node, keys.value)) collapseTo(level)
392
+ else openBranch(level, node)
393
+ }, props.hoverOpenDelay)
394
+ }
395
+
396
+ function onPanelsEnter() {
397
+ clearCollapseTimer()
398
+ }
399
+
400
+ /* Уводя мышь со всего каскада, схлопываем его до ветки выбранного значения. */
401
+ function onPanelsLeave() {
402
+ clearOpenTimer()
403
+ if (mode.value !== 'columns-hover') return
404
+ clearCollapseTimer()
405
+ collapseTimer = setTimeout(() => {
406
+ collapseTimer = null
407
+ activePath.value = selectedAncestorKeys.value
408
+ }, props.hoverCloseDelay)
409
+ }
410
+
411
+ // -- Клавиатура ------------------------------------------------------------
412
+
413
+ const focused = ref<{ level: number, key: unknown } | null>(null)
414
+ const panelRefs = ref<Record<number, { focusItem: (key: unknown) => void } | null>>({})
415
+
416
+ function setPanelRef(el: unknown, level: number) {
417
+ if (el == null) delete panelRefs.value[level]
418
+ else panelRefs.value[level] = el as { focusItem: (key: unknown) => void }
419
+ }
420
+
421
+ function enabledRows(level: number): T[] {
422
+ return (panels.value[level] ?? []).filter(node => !isDisabled(node, keys.value))
423
+ }
424
+
425
+ function firstEnabledKey(level: number): unknown {
426
+ const rows = enabledRows(level)
427
+ return rows.length ? nodeKey(rows[0], keys.value) : undefined
428
+ }
429
+
430
+ function focusRow(level: number, key: unknown) {
431
+ if (key === undefined || isDrawerMode.value) return
432
+ focused.value = { level, key }
433
+ nextTick(() => panelRefs.value[level]?.focusItem(key))
434
+ }
435
+
436
+ function nodeByKey(level: number, key: unknown): T | undefined {
437
+ return (panels.value[level] ?? []).find(node => isEqual(nodeKey(node, keys.value), key))
438
+ }
439
+
440
+ function moveWithin(level: number, delta: number) {
441
+ const rows = enabledRows(level)
442
+ if (!rows.length) return
443
+ const current = rows.findIndex(node => isEqual(nodeKey(node, keys.value), focused.value?.key))
444
+ const next = current === -1
445
+ ? (delta > 0 ? 0 : rows.length - 1)
446
+ : (current + delta + rows.length) % rows.length
447
+ focusRow(level, nodeKey(rows[next], keys.value))
448
+ }
449
+
450
+ function focusInitial() {
451
+ if (isDrawerMode.value) return
452
+ const entry = selectedEntry.value
453
+ if (entry) {
454
+ focusRow(entry.level, selectedKey.value)
455
+ return
456
+ }
457
+ focusRow(0, firstEnabledKey(0))
458
+ }
459
+
460
+ function closeAndRestoreFocus() {
461
+ toggleOpen(false)
462
+ nextTick(() => reference.value?.focus())
463
+ }
464
+
465
+ function onKeydown(event: KeyboardEvent) {
466
+ if (props.disabled) return
467
+
468
+ if (!isOpen.value) {
469
+ if (event.key === 'ArrowDown') {
470
+ event.preventDefault()
471
+ toggleOpen(true)
472
+ }
473
+ return
474
+ }
475
+
476
+ if (event.key === 'Escape') {
477
+ event.preventDefault()
478
+ closeAndRestoreFocus()
479
+ return
480
+ }
481
+
482
+ if (event.key === 'Tab') {
483
+ toggleOpen(false)
484
+ return
485
+ }
486
+
487
+ if (isDrawerMode.value) return
488
+
489
+ const level = focused.value?.level ?? 0
490
+
491
+ switch (event.key) {
492
+ case 'ArrowDown':
493
+ event.preventDefault()
494
+ moveWithin(level, 1)
495
+ break
496
+ case 'ArrowUp':
497
+ event.preventDefault()
498
+ moveWithin(level, -1)
499
+ break
500
+ case 'Home':
501
+ event.preventDefault()
502
+ focusRow(level, firstEnabledKey(level))
503
+ break
504
+ case 'End': {
505
+ event.preventDefault()
506
+ const rows = enabledRows(level)
507
+ if (rows.length) focusRow(level, nodeKey(rows[rows.length - 1], keys.value))
508
+ break
509
+ }
510
+ case 'ArrowRight': {
511
+ const node = focused.value ? nodeByKey(level, focused.value.key) : undefined
512
+ if (!node || isLeaf(node, keys.value)) break
513
+ event.preventDefault()
514
+ openBranch(level, node)
515
+ focusRow(level + 1, firstEnabledKey(level + 1))
516
+ break
517
+ }
518
+ case 'ArrowLeft': {
519
+ if (level === 0) break
520
+ event.preventDefault()
521
+ const parentKey = resolvedPath.value[level - 1]
522
+ goBackFrom(level)
523
+ focusRow(level - 1, parentKey)
524
+ break
525
+ }
526
+ }
527
+ }
528
+
529
+ // -- Открытие / закрытие ---------------------------------------------------
530
+
531
+ watch(isOpen, (open) => {
532
+ clearOpenTimer()
533
+ clearCollapseTimer()
534
+ if (open) {
535
+ activePath.value = selectedAncestorKeys.value
536
+ /* Ширину берём с триггера сразу: иначе колонка мигнёт фолбэком min-w-45 и прыгнет. */
537
+ const triggerWidth = reference.value?.offsetWidth
538
+ if (triggerWidth) autoPanelWidth.value = `${Math.round(triggerWidth)}px`
539
+ nextTick(focusInitial)
540
+ }
541
+ else {
542
+ focused.value = null
543
+ }
544
+ })
545
+
546
+ onClickOutside(floating, () => {
547
+ if (isOpen.value && !isDrawerMode.value) toggleOpen(false)
548
+ }, { ignore: [reference] })
549
+
550
+ // -- Отображение в триггере ------------------------------------------------
551
+
552
+ const displayValue = computed(() => {
553
+ const entry = selectedEntry.value
554
+ if (!entry) return ''
555
+ return props.showPathInTrigger
556
+ ? pathLabel(entry.path, keys.value)
557
+ : nodeLabel(entry.node, keys.value)
558
+ })
559
+
560
+ const rootPanelTitle = computed(() => props.panelTitle ?? props.label)
561
+ </script>
562
+
563
+ <template>
564
+ <div
565
+ ref="wrapper"
566
+ class="cascade-select relative w-full text-body-md"
567
+ @keydown="onKeydown"
568
+ >
569
+ <a-select-trigger
570
+ ref="triggerRef"
571
+ :label="label"
572
+ :required="required"
573
+ :size="size"
574
+ :disabled="disabled"
575
+ :error="error"
576
+ :is-open="isOpen"
577
+ :start-icon="startIcon"
578
+ :button-bg="buttonBg"
579
+ :clearable="clearable"
580
+ :show-clear="clearable && !!selectedEntry"
581
+ floating-label
582
+ :has-value="!!selectedEntry"
583
+ @toggle="toggleOpen()"
584
+ @clear="onClear"
585
+ >
586
+ <span v-if="selectedEntry" class="block truncate leading-tight">
587
+ <slot
588
+ name="single-selected"
589
+ :value="selectedEntry.node"
590
+ :path="selectedEntry.path"
591
+ >{{ displayValue }}</slot>
592
+ </span>
593
+ <span v-else class="text-secondary">&#8203;</span>
594
+ </a-select-trigger>
595
+
596
+ <client-only>
597
+ <teleport
598
+ to="body"
599
+ :disabled="!teleport"
600
+ >
601
+ <transition
602
+ enter-active-class="cascade-select__enter-active"
603
+ enter-from-class="cascade-select__enter-from"
604
+ enter-to-class="cascade-select__enter-to"
605
+ leave-active-class="cascade-select__leave-active"
606
+ leave-from-class="cascade-select__leave-from"
607
+ leave-to-class="cascade-select__leave-to"
608
+ >
609
+ <div
610
+ v-if="isOpen && !isDrawerMode"
611
+ ref="floating"
612
+ class="cascade-select__panels flex items-start gap-2"
613
+ :class="{ 'flex-row-reverse': panelsDir === 'left' }"
614
+ :style="{
615
+ position: strategy,
616
+ left: x != null ? `${x}px` : '',
617
+ top: y != null ? `${y}px` : '',
618
+ zIndex,
619
+ }"
620
+ @pointerenter="onPanelsEnter"
621
+ @pointerleave="onPanelsLeave"
622
+ @keydown="onKeydown"
623
+ >
624
+ <a-cascade-panel
625
+ v-for="view in visiblePanelViews"
626
+ :key="view.level"
627
+ :ref="(el: any) => setPanelRef(el, view.level)"
628
+ :options="view.options"
629
+ :keys="keys"
630
+ :level="view.level"
631
+ :active-key="resolvedPath[view.level]"
632
+ :selected-key="selectedKey"
633
+ :focused-key="focused?.level === view.level ? focused?.key : undefined"
634
+ :title="view.level === 0 ? rootPanelTitle : ''"
635
+ :show-back="showBackFor(view.level)"
636
+ :mirrored="panelsDir === 'left'"
637
+ :back-label="backLabel"
638
+ :width="panelWidthStyle"
639
+ :min-width="panelMinWidthStyle"
640
+ :max-height="panelMaxHeightStyle"
641
+ :empty-label="emptyLabel"
642
+ @activate="activate(view.level, $event)"
643
+ @hover="onRowHover(view.level, $event)"
644
+ @back="goBackFrom(view.level)"
645
+ >
646
+ <template #option="slotProps">
647
+ <slot name="option" v-bind="slotProps" />
648
+ </template>
649
+ <template #empty>
650
+ <slot name="empty" />
651
+ </template>
652
+ </a-cascade-panel>
653
+ </div>
654
+ </transition>
655
+ </teleport>
656
+
657
+ <a-cascade-drawer
658
+ v-if="isDrawerMode"
659
+ v-model="isOpen"
660
+ :options="drawerView.options"
661
+ :keys="keys"
662
+ :level="drawerView.level"
663
+ :title="drawerTitle ?? label"
664
+ :parent-label="drawerParentLabel"
665
+ :back-label="backLabel"
666
+ :selected-key="selectedKey"
667
+ :height="drawerHeightStyle"
668
+ :max-height="drawerMaxHeightStyle"
669
+ :z-index="zIndex"
670
+ @activate="activate(drawerView.level, $event)"
671
+ @back="goBack"
672
+ >
673
+ <template #option="slotProps">
674
+ <slot name="option" v-bind="slotProps" />
675
+ </template>
676
+ <template #empty>
677
+ <slot name="empty" />
678
+ </template>
679
+ </a-cascade-drawer>
680
+ </client-only>
681
+ </div>
682
+ </template>
683
+
684
+ <style scoped>
685
+ .cascade-select__enter-active,
686
+ .cascade-select__leave-active {
687
+ transition:
688
+ opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
689
+ transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
690
+ }
691
+
692
+ .cascade-select__enter-from,
693
+ .cascade-select__leave-to {
694
+ opacity: 0;
695
+ transform: translateY(-4px);
696
+ }
697
+
698
+ .cascade-select__enter-to,
699
+ .cascade-select__leave-from {
700
+ opacity: 1;
701
+ transform: translateY(0);
702
+ }
703
+
704
+ @media (prefers-reduced-motion: reduce) {
705
+ .cascade-select__enter-active,
706
+ .cascade-select__leave-active {
707
+ transition: none;
708
+ }
709
+ }
710
+ </style>