adata-ui 2.1.41-beta.0 → 2.1.41-beta.10

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 (63) hide show
  1. package/-error.vue +1 -1
  2. package/CLAUDE.md +3 -2
  3. package/assets/styles/main.css +4 -3
  4. package/assets/styles/theme.css +20 -6
  5. package/assets/styles/ui.css +77 -0
  6. package/components/elements/button/AButtonV2.vue +60 -15
  7. package/components/elements/count-up/ACountUp.vue +54 -0
  8. package/components/elements/curve-block-v2/ACurveBlockV2.vue +56 -18
  9. package/components/elements/empty-box/empty-box-V2.vue +1 -1
  10. package/components/elements/error-template/index.vue +8 -3
  11. package/components/elements/infinite-carousel/AInfiniteCarousel.vue +46 -8
  12. package/components/elements/section-heading/ASectionHeading.vue +35 -0
  13. package/components/elements/select/ACascadeSelectV2.vue +710 -0
  14. package/components/elements/select/ASelectV2.vue +70 -143
  15. package/components/elements/select/cascade-utils.ts +115 -0
  16. package/components/elements/select/types.ts +27 -0
  17. package/components/elements/select/ui/cascade-drawer.vue +146 -0
  18. package/components/elements/select/ui/cascade-panel.vue +237 -0
  19. package/components/elements/select/ui/select-trigger.vue +167 -0
  20. package/components/elements/step-slider/AStepSliderColCard.vue +75 -42
  21. package/components/elements/step-slider/AStepSliderColNumber.vue +74 -53
  22. package/components/features/color-mode/AColorMode.client.vue +1 -23
  23. package/components/features/go-top/GoTop.vue +4 -0
  24. package/components/forms/feedback-form/FeedbackFormV2.vue +18 -18
  25. package/components/forms/input/AInputV2.vue +69 -47
  26. package/components/forms/input/textarea/ATextareaV2.vue +10 -5
  27. package/components/modals/ReportBugModal.vue +1 -1
  28. package/components/modals/id/IdModals.vue +14 -10
  29. package/components/navigation/bottom-navigation/ABottomNavigation.vue +1 -1
  30. package/components/navigation/footer/AFooter.vue +2 -2
  31. package/components/navigation/header/AHeader.vue +8 -4
  32. package/components/navigation/pill-tabs/APillTabsV2.vue +29 -14
  33. package/components/overlays/drawer/ADrawerV2.vue +9 -2
  34. package/components/overlays/modal/AModalV2.vue +1 -0
  35. package/icons/ai/ai-diamond-unfilled.vue +26 -0
  36. package/icons/ai/ai-diamond.vue +11 -0
  37. package/icons/arrow/arrow-down-left.vue +6 -0
  38. package/icons/arrow/arrow-up-right.vue +6 -0
  39. package/icons/clock-rotate-ccw.vue +7 -0
  40. package/icons/crown.vue +6 -0
  41. package/icons/letter/letter-a.vue +4 -2
  42. package/icons/letter/letter-b.vue +2 -2
  43. package/icons/letter/letter-c.vue +2 -2
  44. package/icons/letter/letter-d.vue +2 -2
  45. package/icons/letter/letter-e.vue +2 -2
  46. package/icons/list-checks.vue +3 -0
  47. package/icons/log-out.vue +21 -0
  48. package/icons/main-filter.vue +6 -1
  49. package/icons/refresh-cw.vue +8 -0
  50. package/icons/shield-check.vue +20 -0
  51. package/icons/shuffle.vue +3 -0
  52. package/icons/user-plus.vue +3 -0
  53. package/icons/wallet.vue +6 -0
  54. package/illustrations/notes-search.vue +20 -0
  55. package/illustrations/notes-sticky.vue +20 -0
  56. package/illustrations/scheme-notes.vue +21 -0
  57. package/illustrations/scheme-waypoints.vue +25 -0
  58. package/lang/en.ts +2 -1
  59. package/lang/kk.ts +3 -2
  60. package/lang/ru.ts +2 -1
  61. package/nuxt.config.ts +7 -4
  62. package/package.json +2 -2
  63. package/utils/toCssSize.ts +11 -0
package/-error.vue CHANGED
@@ -135,7 +135,7 @@ function handleError() {
135
135
  <div
136
136
  v-for="item in errorData.content?.list"
137
137
  :key="item.id"
138
- class="flex items-center gap-1 text-sm font-medium"
138
+ class="flex items-center gap-1 text-sm font-medium"
139
139
  >
140
140
  <a-icon-check class="stroke-icon-brand" />
141
141
  {{ item.label }}
package/CLAUDE.md CHANGED
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
6
6
 
7
7
  adata-ui — UI-кит проектов adata.kz, оформленный как **Nuxt 3 layer** (`main` = `nuxt.config.ts`, публикуется npm-пакетом `adata-ui`). Консьюмеры (pk-frontend) подключают его через `extends` и автоматически получают компоненты, Tailwind и стили. Разработка и просмотр компонентов — в `.playground` (extends `'..'`).
8
8
 
9
- Ветка `tailwind-v4` (2026-07) — миграция на Tailwind v4 + новая палитра + тёмная тема на семантических токенах; `predesign` ещё на v3. pk-frontend мигрирован на v4/семантику (ветка ADTC-8931, extends `'../adata-ui'`); `deepblue` удалён из theme.css после его миграции (2026-07-28). Каталог `docs/` удалён из дерева; реестр миграции — в git-истории: `git show 69039ec:docs/color-migration.md`.
9
+ Миграция на Tailwind v4 + новая палитра + тёмная тема на семантических токенах влита в `predesign` (ветка `tailwind-v4`, 2026-07) — рабочая ветка уже на v4. pk-frontend мигрирован на v4/семантику (ветка ADTC-8931, extends `'../adata-ui'`); `deepblue` удалён из theme.css после его миграции (2026-07-28). Каталог `docs/` удалён из дерева; реестр миграции — в git-истории: `git show 69039ec:docs/color-migration.md`.
10
10
 
11
11
  ## Команды
12
12
 
@@ -35,7 +35,8 @@ adata-ui — UI-кит проектов adata.kz, оформленный как
35
35
 
36
36
  - **Примитивные шкалы (`gray-*`, `red-*`, `brand-*`, `secondary-*`, `white`/`black`) константны в обеих темах.** Под `.dark` переключаются только семантические токены (блоки `@generated-semantic-colors[-dark]` в theme.css, источник — Figma-артефакт «Семантические цвета»).
37
37
  - В новом коде цвета задавать семантикой: `text-primary/secondary/quaternary/disabled/brand/link/success/warning/danger`, `bg-main/subtle/muted/overlay/badge/brand[-subtle]/{success,warning,danger,red…rose}[-subtle]`, `border-default/strong/focus/brand/…`, `bg-action-{primary,secondary,danger}[-hover|-active]` + `text-action-*-fg`, `text/fill/stroke-icon-*`, `fill-main`/`stroke-main` (флип белых заливок иллюстраций). Реестр и маппинг — `git show 69039ec:docs/color-migration.md`.
38
- - **Роли поверхностей (по Figma-макетам ДС)**: страница — `bg-main`; карточки/секции — `bg-subtle` + `border-default` (subtle полупрозрачен и без бордера не читается); утилитарный непрозрачный серый (thead, скелетоны, зебра, треки, плашки) — `bg-muted`; hover поверхностей — `bg-action-secondary-hover` (не `bg-subtle` — в дарке невидим). **`bg-canvas` не использовать** (dark #1E2939 сине-серый, выведен из употребления).
38
+ - **Роли поверхностей (по Figma-макетам ДС)**: страница — `bg-main`; карточки/секции — `bg-subtle` + `border-default` (subtle полупрозрачен и без бордера не читается); утилитарный непрозрачный серый (thead, скелетоны, зебра, треки, плашки) — `bg-muted`; hover поверхностей — `bg-action-secondary-hover` (не `bg-subtle` — в дарке невидим); «утопленная» зона (инпуты, плитки, «экраны» иллюстраций) — `bg-canvas` (dark #0F0F10 — глубже `bg-main`; прежний сине-серый #1E2939 заменён аудитом семантических цветов 2026-08).
39
+ - **Пара иконка+плитка**: цвет `text-icon-{цвет}` всегда идёт с плиткой `bg-{цвет}-subtle` того же оттенка — колор-кодирование блоков (лендинги, карточки модулей) строится только так.
39
40
  - Примитив в разметке легален, когда цвет должен быть одинаков в темах: инвертированные поверхности (`bg-secondary-500` футера/тултипов), белый декор на цветном (`bg-white` точки/бегунки), `chart-*`, `surface-dark*`. Цветовые `dark:`-классы разрешены для краевых случаев без токена (`dark:bg-white/5`-тинты и т.п.).
40
41
  - Для ring/outline border-токены доступны var-синтаксисом: `ring-(--border-color-default)`, `outline-(--border-color-danger)/60`.
41
42
  - В `<style>`-блоках: `var(--text-color-*)`, `var(--background-color-*)`, `var(--border-color-*)`, `var(--color-action-*|icon-*)`; полупрозрачность — `color-mix(in srgb, var(--…) N%, transparent)`. `theme('colors…')` в v4 не работает. **Не вешать `/N` на полупрозрачные токены** (`bg-subtle`, `bg-overlay`, dark `brand-subtle`/`danger-subtle`) — альфа перемножается.
@@ -1,6 +1,7 @@
1
- /* CSS-вход adata-ui: подключается конфигом слоя (css: в nuxt.config.ts),
2
- консьюмерам ничего настраивать не нужно. Свой @import "tailwindcss"
3
- в приложении не заводить — будет второй tailwind-корень и дубли утилит. */
1
+ /* CSS-вход adata-ui. Подключать его обязан КОНСЬЮМЕР (css: в своём nuxt.config),
2
+ а не слой: у слоя из node_modules Nuxt дописывает авто-импорты в сам CSS и
3
+ ломает dev-SSR. Свой @import "tailwindcss" в приложении не заводить —
4
+ будет второй tailwind-корень и дубли утилит. */
4
5
  @import "tailwindcss";
5
6
  @import "./theme.css";
6
7
  @import "./ui.css";
@@ -7,15 +7,22 @@
7
7
  @theme static {
8
8
  --font-sans: "Open Sans", sans-serif;
9
9
 
10
+ /* Точка, с которой лендинги ДС-2026 расширяют контейнер (макет: min-[1920px]) */
11
+ --breakpoint-3xl: 1920px;
12
+ /* Дефолтная шкала v4 кончается на --container-7xl = 80rem = 1280px */
13
+ --container-8xl: 96rem;
14
+
10
15
  --radius-*: initial;
11
16
  --radius: 4px;
12
17
  --radius-xs: 4px;
13
18
  --radius-sm: 6px;
14
19
  --radius-md: 8px;
15
20
  --radius-10: 10px;
21
+ --radius-14: 14px;
16
22
  --radius-lg: 12px;
17
23
  --radius-xl: 16px;
18
24
  --radius-2xl: 20px;
25
+ --radius-3xl: 24px;
19
26
 
20
27
  --text-display-hero: 60px;
21
28
  --text-display-hero--line-height: 72px;
@@ -136,6 +143,13 @@
136
143
  --color-secondary-900: #10171E;
137
144
  /* @generated-colors:end */
138
145
 
146
+ /* ОСОЗНАННОЕ ОТКЛОНЕНИЕ ОТ АРТЕФАКТА (2026-08-24, по просьбе PM): роль warning для текста и
147
+ subtle-фона переведена с amber на yellow — --text-color-warning (#A65F00 / dark #FFDF20) и
148
+ --background-color-warning-subtle (#FEF9C2 / dark #432004), т.е. yellow-700/300 и yellow-100/950.
149
+ Причина: прежняя пара amber-500 на amber-100 давала ≈1.9:1 — провал WCAG AA для мелкого текста.
150
+ Остальные warning-токены (--background-color-warning, --border-color-warning, --color-icon-warning)
151
+ оставлены как в артефакте. При регенерации семантических блоков из Figma эти четыре значения
152
+ (здесь и в @generated-semantic-colors-dark ниже) нужно вернуть вручную. */
139
153
  /* @generated-semantic-colors:start (светлые значения; тёмные — в @layer base .dark ниже)
140
154
  Источник: Figma-артефакт «Семантические цвета» (таблица токенов — в git-истории: git show 69039ec:docs/color-migration.md).
141
155
  Старый генератор (git show 8862721) к этому блоку неприменим: регенерация = дамп таблицы артефакта.
@@ -152,7 +166,7 @@
152
166
  --text-color-link: #068DFF;
153
167
  --text-color-link-hover: #007AE0;
154
168
  --text-color-success: #00A63E;
155
- --text-color-warning: #FE9A00;
169
+ --text-color-warning: #A65F00;
156
170
  --text-color-danger: #FB2C36;
157
171
  /* bg */
158
172
  --background-color-main: #FFFFFF;
@@ -165,7 +179,7 @@
165
179
  --background-color-success: #00A63E;
166
180
  --background-color-success-subtle: #DCFCE7;
167
181
  --background-color-warning: #FE9A00;
168
- --background-color-warning-subtle: #FEF3C6;
182
+ --background-color-warning-subtle: #FEF9C2;
169
183
  --background-color-danger: #FB2C36;
170
184
  --background-color-danger-subtle: #FEF2F2;
171
185
  --background-color-badge: #E5E7EB;
@@ -284,20 +298,20 @@
284
298
  --text-color-link: #3DA6FF;
285
299
  --text-color-link-hover: #B3DCFF;
286
300
  --text-color-success: #00C950;
287
- --text-color-warning: #FFB900;
301
+ --text-color-warning: #FFDF20;
288
302
  --text-color-danger: #FF6467;
289
303
  /* bg */
290
304
  --background-color-main: #161617;
291
- --background-color-canvas: #1E2939;
305
+ --background-color-canvas: #0F0F10;
292
306
  --background-color-subtle: #171717;
293
- --background-color-muted: #262626;
307
+ --background-color-muted: #1F1F21;
294
308
  --background-color-overlay: #00000099;
295
309
  --background-color-brand: #068DFF;
296
310
  --background-color-brand-subtle: #068DFF1A;
297
311
  --background-color-success: #008236;
298
312
  --background-color-success-subtle: #032E15;
299
313
  --background-color-warning: #E17100;
300
- --background-color-warning-subtle: #461901;
314
+ --background-color-warning-subtle: #432004;
301
315
  --background-color-danger: #FF6467;
302
316
  --background-color-danger-subtle: #4608094D;
303
317
  --background-color-badge: #4A5565;
@@ -14,6 +14,18 @@ body {
14
14
  background-color: var(--background-color-main);
15
15
  }
16
16
 
17
+ /* Отключает жёлоб скроллбара на страницах со своим скроллом (схемы связей).
18
+ Вешается через useHead({ htmlAttrs: { class: … } }) и снимается при уходе со
19
+ страницы — в отличие от глобального <style> в layout'е, который утекал в
20
+ <head> навсегда. Специфичность класса выше html, !important не нужен. */
21
+ html.no-scroll-gutter {
22
+ scrollbar-gutter: initial;
23
+ }
24
+
25
+ html.no-scroll-gutter::-webkit-scrollbar {
26
+ width: 0;
27
+ }
28
+
17
29
  html::-webkit-scrollbar {
18
30
  width: 15px;
19
31
  }
@@ -168,3 +180,68 @@ input[type="number"] {
168
180
  clip: rect(0 0 0 0);
169
181
  overflow: hidden;
170
182
  }
183
+
184
+ /* Подложка hero-иллюстраций лендингов (§05 аудита: декоративный градиент — литерал).
185
+ Тема — по классу .dark, как у .bg-gradient-blue. */
186
+ .a-illus-backdrop {
187
+ position: relative;
188
+ overflow: hidden;
189
+ background: linear-gradient(145deg, #e4f2ff 0%, #c3e1ff 48%, #a5d2ff 100%);
190
+ box-shadow: 0 30px 60px -28px rgba(6, 141, 255, .5), inset 0 1px 0 rgba(255, 255, 255, .6);
191
+ }
192
+
193
+ .a-illus-backdrop::before {
194
+ content: "";
195
+ position: absolute;
196
+ inset: 0;
197
+ background: radial-gradient(130px 130px at 80% 16%, rgba(255, 255, 255, .6), transparent 70%);
198
+ pointer-events: none;
199
+ }
200
+
201
+ .a-illus-backdrop::after {
202
+ content: "";
203
+ position: absolute;
204
+ inset: 0;
205
+ background-image: radial-gradient(rgba(6, 141, 255, .16) 1.3px, transparent 1.4px);
206
+ background-size: 15px 15px;
207
+ opacity: .55;
208
+ mask-image: linear-gradient(135deg, #000, transparent 55%);
209
+ pointer-events: none;
210
+ }
211
+
212
+ .dark .a-illus-backdrop {
213
+ background: linear-gradient(145deg, #15273b 0%, #101a27 55%, #0d1622 100%);
214
+ box-shadow: 0 30px 60px -28px rgba(6, 141, 255, .3), inset 0 1px 0 rgba(255, 255, 255, .05);
215
+ }
216
+
217
+ .dark .a-illus-backdrop::before {
218
+ background: radial-gradient(130px 130px at 80% 16%, rgba(6, 141, 255, .14), transparent 70%);
219
+ }
220
+
221
+ /* Голубой ореол за hero лендинга (§05 аудита: декоративный градиент — литерал).
222
+ Тёмного варианта нет намеренно: та же rgba читается и на #fff, и на #161617.
223
+ Вешать на корень страницы лендинга, а не на body — pk-frontend это всё
224
+ приложение, а не набор лендингов. */
225
+ .a-page-glow {
226
+ position: relative;
227
+ z-index: 0;
228
+ }
229
+
230
+ /* z-index: -1 красится поверх фона хоста, но под его контентом — только если у
231
+ хоста есть свой контекст наложения (z-index: 0 выше). */
232
+ .a-page-glow::after {
233
+ content: "";
234
+ position: absolute;
235
+ inset: 0;
236
+ z-index: -1;
237
+ pointer-events: none;
238
+ background-repeat: no-repeat;
239
+ background-image: radial-gradient(
240
+ closest-side circle,
241
+ rgba(43, 127, 255, .1) 0%,
242
+ rgba(43, 127, 255, .05) 48%,
243
+ rgba(43, 127, 255, 0) 100%
244
+ );
245
+ background-size: 1500px 1500px;
246
+ background-position: 50% -420px;
247
+ }
@@ -1,10 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import type { Component } from 'vue'
3
3
 
4
- type View = 'solid' | 'outline' | 'ghost' | 'soft'
4
+ type View = 'solid' | 'outline' | 'ghost' | 'soft' | 'gradient'
5
5
  type Size = 'xs' | 'sm' | 'md' | 'lg'
6
6
  type Color = 'blue' | 'green' | 'red' | 'gray' | 'white'
7
- type Rounded = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
7
+ type Rounded = 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
8
8
 
9
9
  const props = withDefaults(defineProps<{
10
10
  view?: View
@@ -16,7 +16,10 @@ const props = withDefaults(defineProps<{
16
16
  loading?: boolean
17
17
  disabled?: boolean
18
18
  block?: boolean
19
+ glow?: boolean
20
+ square?: boolean
19
21
  type?: 'button' | 'submit' | 'reset'
22
+ dataTestId?: string
20
23
  }>(), {
21
24
  view: 'solid',
22
25
  color: 'blue',
@@ -26,6 +29,8 @@ const props = withDefaults(defineProps<{
26
29
  loading: false,
27
30
  disabled: false,
28
31
  block: false,
32
+ glow: false,
33
+ square: false,
29
34
  type: 'button',
30
35
  })
31
36
 
@@ -33,38 +38,65 @@ const emit = defineEmits<{ click: [event: MouseEvent] }>()
33
38
 
34
39
  const VIEW_CLASS: Record<Color, Record<View, string>> = {
35
40
  blue: {
36
- solid: 'bg-action-primary text-action-primary-fg shadow-xs hover:bg-action-primary-hover hover:shadow-md active:bg-action-primary-active',
41
+ solid: 'bg-action-primary text-action-primary-fg hover:bg-action-primary-hover active:bg-action-primary-active',
42
+ gradient: 'bg-linear-to-r from-brand-500 to-brand-400 text-inverted hover:brightness-105',
37
43
  outline: 'border border-brand bg-action-secondary text-brand hover:bg-brand-subtle active:bg-blue-subtle',
38
44
  ghost: 'text-action-secondary-fg hover:bg-action-secondary-hover active:bg-badge',
39
45
  soft: 'bg-brand-subtle text-brand hover:bg-blue-subtle active:bg-brand-200/70 dark:active:bg-brand-500/25',
40
46
  },
41
47
  green: {
42
- solid: 'bg-success text-white shadow-xs hover:bg-green-700 hover:shadow-md active:bg-green-800 dark:hover:bg-green-800 dark:active:bg-green-900',
48
+ solid: 'bg-success text-white hover:bg-green-700 active:bg-green-800 dark:hover:bg-green-800 dark:active:bg-green-900',
49
+ gradient: 'bg-linear-to-r from-green-600 to-green-500 text-inverted hover:brightness-105',
43
50
  outline: 'border border-success bg-action-secondary text-success hover:bg-success-subtle active:bg-green-200/70 dark:active:bg-green-500/25',
44
51
  ghost: 'text-success hover:bg-success-subtle active:bg-green-200/70 dark:active:bg-green-500/25',
45
52
  soft: 'bg-success-subtle text-success hover:bg-green-subtle active:bg-green-200/70 dark:active:bg-green-500/25',
46
53
  },
47
54
  red: {
48
- solid: 'bg-action-danger text-action-danger-fg shadow-xs hover:bg-action-danger-hover hover:shadow-md active:bg-action-danger-hover',
55
+ solid: 'bg-action-danger text-action-danger-fg hover:bg-action-danger-hover active:bg-action-danger-hover',
56
+ gradient: 'bg-linear-to-r from-red-600 to-red-500 text-inverted hover:brightness-105',
49
57
  outline: 'border border-danger bg-action-secondary text-danger hover:bg-danger-subtle active:bg-red-subtle',
50
58
  ghost: 'text-danger hover:bg-danger-subtle active:bg-red-subtle',
51
59
  soft: 'bg-danger-subtle text-danger hover:bg-red-subtle active:bg-red-200/70 dark:active:bg-red-500/25',
52
60
  },
53
61
  gray: {
54
62
  /* осознанно константный серый solid: action-gray триады нет в ДС */
55
- solid: 'bg-gray-500 text-white shadow-xs hover:bg-gray-600 hover:shadow-md active:bg-gray-700',
63
+ solid: 'bg-gray-500 text-white hover:bg-gray-600 active:bg-gray-700',
64
+ gradient: 'bg-linear-to-r from-gray-600 to-gray-500 text-inverted hover:brightness-105',
56
65
  outline: 'border border-strong bg-action-secondary text-primary hover:border-strong hover:bg-action-secondary-hover active:bg-badge',
57
66
  ghost: 'text-action-secondary-fg hover:bg-action-secondary-hover active:bg-badge',
58
67
  soft: 'bg-badge text-quaternary hover:bg-badge active:bg-badge/70',
59
68
  },
60
69
  white: {
61
- solid: 'bg-main text-brand-600 shadow-xs hover:bg-brand-subtle hover:shadow-md active:bg-blue-subtle',
70
+ solid: 'bg-main text-brand-600 hover:bg-brand-subtle active:bg-blue-subtle',
71
+ gradient: 'bg-linear-to-r from-white to-brand-50 text-brand-600 hover:brightness-105',
62
72
  outline: 'border border-brand bg-main text-brand-600 hover:bg-brand-subtle active:bg-blue-subtle',
63
73
  ghost: 'text-brand-600 hover:bg-brand-subtle active:bg-blue-subtle',
64
74
  soft: 'bg-brand-subtle text-brand-600 hover:bg-blue-subtle active:bg-brand-200/70 dark:active:bg-brand-500/25',
65
75
  },
66
76
  }
67
77
 
78
+ /* Тень вынесена из VIEW_CLASS: конфликт shadow-утилит Tailwind решает порядком
79
+ в стилях, а не порядком классов, поэтому glow должен ЗАМЕНЯТЬ базовую тень. */
80
+ const RAISED_VIEWS: View[] = ['solid', 'gradient']
81
+
82
+ const BASE_ELEVATION = 'shadow-xs hover:shadow-md'
83
+
84
+ /* Цветная тень: форма и цвет раздельно. Тон берётся из примитивов, а не из
85
+ семантики — ореол не должен сам менять оттенок между темами; пара dark: —
86
+ осознанная подгонка плотности (на #161617 40% блюмит). */
87
+ const GLOW_CLASS: Record<Color, string> = {
88
+ blue: 'shadow-md shadow-brand-500/30 hover:shadow-lg hover:shadow-brand-500/40 dark:shadow-brand-400/25 dark:hover:shadow-brand-400/35',
89
+ green: 'shadow-md shadow-green-600/30 hover:shadow-lg hover:shadow-green-600/40 dark:shadow-green-500/25 dark:hover:shadow-green-500/35',
90
+ red: 'shadow-md shadow-red-600/30 hover:shadow-lg hover:shadow-red-600/40 dark:shadow-red-500/25 dark:hover:shadow-red-500/35',
91
+ gray: 'shadow-md shadow-gray-500/25 hover:shadow-lg hover:shadow-gray-500/35 dark:shadow-gray-400/20 dark:hover:shadow-gray-400/30',
92
+ white: 'shadow-md shadow-brand-500/25 hover:shadow-lg hover:shadow-brand-500/35 dark:shadow-brand-400/20 dark:hover:shadow-brand-400/30',
93
+ }
94
+
95
+ const elevationClass = computed(() => {
96
+ if (!RAISED_VIEWS.includes(props.view)) return ''
97
+ return props.glow ? GLOW_CLASS[props.color] : BASE_ELEVATION
98
+ })
99
+
68
100
  const SIZE_CLASS: Record<Size, string> = {
69
101
  xs: 'h-6 gap-1 px-2 text-body-sm',
70
102
  sm: 'h-8 gap-1.5 px-3 text-body-sm',
@@ -72,13 +104,24 @@ const SIZE_CLASS: Record<Size, string> = {
72
104
  lg: 'h-12 gap-2.5 px-5 text-body-lg',
73
105
  }
74
106
 
107
+ /* Квадрат под одну иконку: ЗАМЕНЯЕТ SIZE_CLASS, а не дописывается к нему —
108
+ `px-0` рядом с `px-4` проиграет (конфликт решает порядок в stylesheet), из-за
109
+ чего такие кнопки обычно лепят через `!w-10 !px-0`. */
110
+ const SQUARE_CLASS: Record<Size, string> = {
111
+ xs: 'size-6',
112
+ sm: 'size-8',
113
+ md: 'size-10',
114
+ lg: 'size-12',
115
+ }
116
+
75
117
  const ROUNDED_CLASS: Record<Rounded, string> = {
76
- none: 'rounded-none',
77
- sm: 'rounded-sm',
78
- md: 'rounded-md',
79
- lg: 'rounded-lg',
80
- xl: 'rounded-xl',
81
- full: 'rounded-full',
118
+ 'none': 'rounded-none',
119
+ 'sm': 'rounded-sm',
120
+ 'md': 'rounded-md',
121
+ 'lg': 'rounded-lg',
122
+ 'xl': 'rounded-xl',
123
+ '2xl': 'rounded-2xl',
124
+ 'full': 'rounded-full',
82
125
  }
83
126
 
84
127
  const ICON_SIZE: Record<Size, string> = {
@@ -98,14 +141,16 @@ function onClick(event: MouseEvent) {
98
141
  <button
99
142
  :type="type"
100
143
  :disabled="disabled || loading"
101
- class="button-v2 inline-flex select-none items-center justify-center font-semibold transition-all duration-150 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
144
+ class="button-v2 inline-flex items-center justify-center font-semibold transition-all duration-150 select-none focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
102
145
  :class="[
103
146
  VIEW_CLASS[color][view],
104
- SIZE_CLASS[size],
147
+ elevationClass,
148
+ square ? SQUARE_CLASS[size] : SIZE_CLASS[size],
105
149
  ROUNDED_CLASS[rounded],
106
150
  block ? 'w-full' : '',
107
151
  iconRight ? 'flex-row-reverse' : '',
108
152
  ]"
153
+ :data-test-id="dataTestId"
109
154
  @click="onClick"
110
155
  >
111
156
  <a-icon-loader-circle
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ // Декоративный счётчик: анимирует 0 → `to` при монтировании.
3
+ // С пропом `play` поведение другое: значение сразу стоит на `to`, а анимация
4
+ // играет заново на каждое изменение триггера (ховер карточки в лендингах).
5
+ const props = withDefaults(defineProps<{
6
+ to: number
7
+ duration?: number
8
+ suffix?: string
9
+ locale?: string
10
+ play?: number
11
+ }>(), {
12
+ duration: 1100,
13
+ suffix: '',
14
+ locale: 'ru-RU',
15
+ })
16
+
17
+ const isTriggered = computed(() => props.play !== undefined)
18
+
19
+ const value = ref(isTriggered.value ? props.to : 0)
20
+ const display = computed(() => `${Math.round(value.value).toLocaleString(props.locale)}${props.suffix}`)
21
+
22
+ let raf = 0
23
+
24
+ function run() {
25
+ cancelAnimationFrame(raf)
26
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
27
+ value.value = props.to
28
+ return
29
+ }
30
+ value.value = 0
31
+ let start = 0
32
+ const step = (ts: number) => {
33
+ if (!start) start = ts
34
+ const progress = Math.min(1, (ts - start) / props.duration)
35
+ value.value = props.to * (1 - (1 - progress) ** 3)
36
+ if (progress < 1) raf = requestAnimationFrame(step)
37
+ }
38
+ raf = requestAnimationFrame(step)
39
+ }
40
+
41
+ onMounted(() => {
42
+ if (!isTriggered.value) run()
43
+ })
44
+
45
+ watch(() => props.play, (next) => {
46
+ if (next) run()
47
+ })
48
+
49
+ onUnmounted(() => cancelAnimationFrame(raf))
50
+ </script>
51
+
52
+ <template>
53
+ <span>{{ display }}</span>
54
+ </template>
@@ -8,24 +8,26 @@ withDefaults(defineProps<{
8
8
  description: string
9
9
  to?: string
10
10
  color?: string
11
+ tint?: string
11
12
  forceActive?: boolean
12
- hoverAccent?: boolean
13
+ dataTestId?: string
13
14
  }>(), {
14
- color: '#068dff',
15
+ color: 'var(--color-icon-brand)',
16
+ tint: undefined,
15
17
  forceActive: true,
16
- hoverAccent: false,
17
18
  })
18
19
  </script>
19
- `
20
+
20
21
  <template>
21
- <NuxtLinkLocale
22
+ <nuxt-link-locale
22
23
  :to="to"
23
- :style="{ '--cb-color': color }"
24
- class="curve-block-v2 group relative flex w-full cursor-pointer flex-col gap-4 rounded-2xl border border-default bg-main p-6 text-left transition-all duration-200 hover:-translate-y-1 hover:border-strong hover:shadow-lg"
25
- :class="{ 'active-item': !to && forceActive, 'hover-accent': hoverAccent }"
24
+ :style="{ '--cb-color': color, '--cb-tint': tint }"
25
+ class="curve-block-v2 group relative flex w-full cursor-pointer flex-col gap-4 rounded-2xl border border-default bg-main p-6 text-left"
26
+ :class="{ 'active-item': !to && forceActive }"
26
27
  active-class="active-item"
28
+ :data-test-id="dataTestId"
27
29
  >
28
- <div class="arrow-icon absolute right-4 top-4 flex size-7 items-center justify-center rounded-full bg-brand-subtle p-2 text-brand">
30
+ <div class="arrow-icon absolute top-4 right-4 flex size-8 items-center justify-center rounded-full bg-canvas p-2 text-secondary transition-all duration-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5">
29
31
  <svg
30
32
  class="size-2"
31
33
  viewBox="16 16 16 16"
@@ -49,36 +51,72 @@ withDefaults(defineProps<{
49
51
  </div>
50
52
 
51
53
  <div class="flex flex-col gap-2 pr-6">
52
- <p class="title text-body-lg-strong text-primary">
54
+ <p class="title text-h5 text-primary transition-colors duration-200">
53
55
  {{ title }}
54
56
  </p>
55
- <p class="text-body-md leading-relaxed text-secondary">
57
+ <p class="text-body-md leading-relaxed text-quaternary">
56
58
  {{ description }}
57
59
  </p>
58
60
  </div>
59
- </NuxtLinkLocale>
61
+ </nuxt-link-locale>
60
62
  </template>
61
63
 
62
64
  <style scoped>
65
+ .curve-block-v2 {
66
+ --cb-fill: var(--cb-tint, color-mix(in srgb, var(--cb-color) 10%, transparent));
67
+ transition:
68
+ transform 0.2s ease,
69
+ border-color 0.2s ease,
70
+ box-shadow 0.2s ease;
71
+ }
72
+
63
73
  .icon-item {
64
- background-color: color-mix(in srgb, var(--cb-color) 10%, transparent);
74
+ background-color: var(--cb-fill);
65
75
  color: var(--cb-color);
66
76
  }
67
77
 
68
- .hover-accent:hover {
78
+ .curve-block-v2:hover {
79
+ transform: translateY(-4px);
69
80
  border-color: var(--cb-color);
81
+ box-shadow: 0 0 22px -12px var(--cb-color);
82
+ }
83
+
84
+ .curve-block-v2:hover .title,
85
+ .curve-block-v2:hover .arrow-icon {
86
+ color: var(--cb-color);
87
+ }
88
+
89
+ .curve-block-v2:hover .arrow-icon {
90
+ background-color: var(--cb-fill);
70
91
  }
71
92
 
72
93
  .active-item {
73
94
  border-color: var(--cb-color);
74
- background-color: color-mix(in srgb, var(--cb-color) 10%, transparent);
95
+ box-shadow: 0 0 22px -12px var(--cb-color);
75
96
  }
97
+
98
+ .active-item:hover {
99
+ transform: none;
100
+ }
101
+
76
102
  .active-item .title,
77
103
  .active-item .arrow-icon {
78
104
  color: var(--cb-color);
79
105
  }
80
- .active-item .icon-item {
81
- background-color: var(--cb-color);
82
- color: var(--color-icon-on-brand);
106
+
107
+ .active-item .arrow-icon {
108
+ background-color: var(--cb-fill);
109
+ }
110
+
111
+ @media (prefers-reduced-motion: reduce) {
112
+ .curve-block-v2,
113
+ .curve-block-v2 .title,
114
+ .curve-block-v2 .arrow-icon {
115
+ transition: none;
116
+ }
117
+
118
+ .curve-block-v2:hover {
119
+ transform: none;
120
+ }
83
121
  }
84
122
  </style>
@@ -57,7 +57,7 @@ defineProps<Props>()
57
57
 
58
58
  <p
59
59
  v-if="subtitle"
60
- class="mt-1 text-secondary"
60
+ class="mt-1 text-secondary text-body-md"
61
61
  >
62
62
  {{ subtitle }}
63
63
  </p>
@@ -3,8 +3,11 @@ import { AIconArrowLeft, AIconHome } from '#components'
3
3
 
4
4
  interface Props {
5
5
  statusCode: number
6
+ homePath?: string
6
7
  }
7
- const props = defineProps<Props>()
8
+ const props = withDefaults(defineProps<Props>(), {
9
+ homePath: '/',
10
+ })
8
11
 
9
12
  const { t } = useI18n()
10
13
  const localePath = useLocalePath()
@@ -14,7 +17,7 @@ const errorTitle = computed(() => t(`errorPage.${props.statusCode}.title`))
14
17
  const errorSubTitle = computed(() => t(`errorPage.${props.statusCode}.subTitle`))
15
18
 
16
19
  function goToMainPage() {
17
- return navigateTo(`${localePath('/')}`, { external: true })
20
+ return navigateTo(`${localePath(props.homePath)}`, { external: true })
18
21
  }
19
22
 
20
23
  function goBack() {
@@ -22,7 +25,7 @@ function goBack() {
22
25
  router.back()
23
26
  }
24
27
  else {
25
- return navigateTo(`${localePath('/')}`, { external: true })
28
+ return navigateTo(`${localePath(props.homePath)}`, { external: true })
26
29
  }
27
30
  }
28
31
  </script>
@@ -56,7 +59,9 @@ function goBack() {
56
59
  >
57
60
  {{ t('actions.goToMainPage') }}
58
61
  </a-button-v2>
62
+
59
63
  <a-button-v2
64
+ v-if="![404, 500, 502, 503, 504].includes(statusCode)"
60
65
  :icon="AIconArrowLeft"
61
66
  view="outline"
62
67
  color="gray"