adata-ui 2.1.41-beta.0 → 2.1.41-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -1
- package/assets/styles/main.css +4 -3
- package/assets/styles/theme.css +9 -2
- package/assets/styles/ui.css +77 -0
- package/components/elements/button/AButtonV2.vue +58 -15
- package/components/elements/count-up/ACountUp.vue +54 -0
- package/components/elements/curve-block-v2/ACurveBlockV2.vue +54 -18
- package/components/elements/infinite-carousel/AInfiniteCarousel.vue +46 -8
- package/components/elements/section-heading/ASectionHeading.vue +35 -0
- package/components/elements/step-slider/AStepSliderColCard.vue +75 -38
- package/components/elements/step-slider/AStepSliderColNumber.vue +74 -53
- package/components/features/color-mode/AColorMode.client.vue +1 -23
- package/components/forms/feedback-form/FeedbackFormV2.vue +3 -3
- package/components/forms/input/AInputV2.vue +28 -10
- package/components/forms/input/textarea/ATextareaV2.vue +10 -5
- package/components/navigation/footer/AFooter.vue +2 -2
- package/components/navigation/header/AHeader.vue +3 -3
- package/components/navigation/pill-tabs/APillTabsV2.vue +29 -14
- package/icons/letter/letter-a.vue +4 -2
- package/icons/letter/letter-b.vue +2 -2
- package/icons/letter/letter-c.vue +2 -2
- package/icons/letter/letter-d.vue +2 -2
- package/icons/letter/letter-e.vue +2 -2
- package/nuxt.config.ts +7 -4
- package/package.json +2 -2
package/CLAUDE.md
CHANGED
|
@@ -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` — в дарке невидим)
|
|
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`) — альфа перемножается.
|
package/assets/styles/main.css
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
/* CSS-вход adata-ui
|
|
2
|
-
|
|
3
|
-
в приложении не заводить —
|
|
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";
|
package/assets/styles/theme.css
CHANGED
|
@@ -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;
|
|
@@ -288,9 +295,9 @@
|
|
|
288
295
|
--text-color-danger: #FF6467;
|
|
289
296
|
/* bg */
|
|
290
297
|
--background-color-main: #161617;
|
|
291
|
-
--background-color-canvas: #
|
|
298
|
+
--background-color-canvas: #0F0F10;
|
|
292
299
|
--background-color-subtle: #171717;
|
|
293
|
-
--background-color-muted: #
|
|
300
|
+
--background-color-muted: #1F1F21;
|
|
294
301
|
--background-color-overlay: #00000099;
|
|
295
302
|
--background-color-brand: #068DFF;
|
|
296
303
|
--background-color-brand-subtle: #068DFF1A;
|
package/assets/styles/ui.css
CHANGED
|
@@ -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,6 +16,8 @@ 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'
|
|
20
22
|
}>(), {
|
|
21
23
|
view: 'solid',
|
|
@@ -26,6 +28,8 @@ const props = withDefaults(defineProps<{
|
|
|
26
28
|
loading: false,
|
|
27
29
|
disabled: false,
|
|
28
30
|
block: false,
|
|
31
|
+
glow: false,
|
|
32
|
+
square: false,
|
|
29
33
|
type: 'button',
|
|
30
34
|
})
|
|
31
35
|
|
|
@@ -33,38 +37,65 @@ const emit = defineEmits<{ click: [event: MouseEvent] }>()
|
|
|
33
37
|
|
|
34
38
|
const VIEW_CLASS: Record<Color, Record<View, string>> = {
|
|
35
39
|
blue: {
|
|
36
|
-
solid: 'bg-action-primary text-action-primary-fg
|
|
40
|
+
solid: 'bg-action-primary text-action-primary-fg hover:bg-action-primary-hover active:bg-action-primary-active',
|
|
41
|
+
gradient: 'bg-linear-to-r from-brand-500 to-brand-400 text-inverted hover:brightness-105',
|
|
37
42
|
outline: 'border border-brand bg-action-secondary text-brand hover:bg-brand-subtle active:bg-blue-subtle',
|
|
38
43
|
ghost: 'text-action-secondary-fg hover:bg-action-secondary-hover active:bg-badge',
|
|
39
44
|
soft: 'bg-brand-subtle text-brand hover:bg-blue-subtle active:bg-brand-200/70 dark:active:bg-brand-500/25',
|
|
40
45
|
},
|
|
41
46
|
green: {
|
|
42
|
-
solid: 'bg-success text-white
|
|
47
|
+
solid: 'bg-success text-white hover:bg-green-700 active:bg-green-800 dark:hover:bg-green-800 dark:active:bg-green-900',
|
|
48
|
+
gradient: 'bg-linear-to-r from-green-600 to-green-500 text-inverted hover:brightness-105',
|
|
43
49
|
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
50
|
ghost: 'text-success hover:bg-success-subtle active:bg-green-200/70 dark:active:bg-green-500/25',
|
|
45
51
|
soft: 'bg-success-subtle text-success hover:bg-green-subtle active:bg-green-200/70 dark:active:bg-green-500/25',
|
|
46
52
|
},
|
|
47
53
|
red: {
|
|
48
|
-
solid: 'bg-action-danger text-action-danger-fg
|
|
54
|
+
solid: 'bg-action-danger text-action-danger-fg hover:bg-action-danger-hover active:bg-action-danger-hover',
|
|
55
|
+
gradient: 'bg-linear-to-r from-red-600 to-red-500 text-inverted hover:brightness-105',
|
|
49
56
|
outline: 'border border-danger bg-action-secondary text-danger hover:bg-danger-subtle active:bg-red-subtle',
|
|
50
57
|
ghost: 'text-danger hover:bg-danger-subtle active:bg-red-subtle',
|
|
51
58
|
soft: 'bg-danger-subtle text-danger hover:bg-red-subtle active:bg-red-200/70 dark:active:bg-red-500/25',
|
|
52
59
|
},
|
|
53
60
|
gray: {
|
|
54
61
|
/* осознанно константный серый solid: action-gray триады нет в ДС */
|
|
55
|
-
solid: 'bg-gray-500 text-white
|
|
62
|
+
solid: 'bg-gray-500 text-white hover:bg-gray-600 active:bg-gray-700',
|
|
63
|
+
gradient: 'bg-linear-to-r from-gray-600 to-gray-500 text-inverted hover:brightness-105',
|
|
56
64
|
outline: 'border border-strong bg-action-secondary text-primary hover:border-strong hover:bg-action-secondary-hover active:bg-badge',
|
|
57
65
|
ghost: 'text-action-secondary-fg hover:bg-action-secondary-hover active:bg-badge',
|
|
58
66
|
soft: 'bg-badge text-quaternary hover:bg-badge active:bg-badge/70',
|
|
59
67
|
},
|
|
60
68
|
white: {
|
|
61
|
-
solid: 'bg-main text-brand-600
|
|
69
|
+
solid: 'bg-main text-brand-600 hover:bg-brand-subtle active:bg-blue-subtle',
|
|
70
|
+
gradient: 'bg-linear-to-r from-white to-brand-50 text-brand-600 hover:brightness-105',
|
|
62
71
|
outline: 'border border-brand bg-main text-brand-600 hover:bg-brand-subtle active:bg-blue-subtle',
|
|
63
72
|
ghost: 'text-brand-600 hover:bg-brand-subtle active:bg-blue-subtle',
|
|
64
73
|
soft: 'bg-brand-subtle text-brand-600 hover:bg-blue-subtle active:bg-brand-200/70 dark:active:bg-brand-500/25',
|
|
65
74
|
},
|
|
66
75
|
}
|
|
67
76
|
|
|
77
|
+
/* Тень вынесена из VIEW_CLASS: конфликт shadow-утилит Tailwind решает порядком
|
|
78
|
+
в стилях, а не порядком классов, поэтому glow должен ЗАМЕНЯТЬ базовую тень. */
|
|
79
|
+
const RAISED_VIEWS: View[] = ['solid', 'gradient']
|
|
80
|
+
|
|
81
|
+
const BASE_ELEVATION = 'shadow-xs hover:shadow-md'
|
|
82
|
+
|
|
83
|
+
/* Цветная тень: форма и цвет раздельно. Тон берётся из примитивов, а не из
|
|
84
|
+
семантики — ореол не должен сам менять оттенок между темами; пара dark: —
|
|
85
|
+
осознанная подгонка плотности (на #161617 40% блюмит). */
|
|
86
|
+
const GLOW_CLASS: Record<Color, string> = {
|
|
87
|
+
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',
|
|
88
|
+
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',
|
|
89
|
+
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',
|
|
90
|
+
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',
|
|
91
|
+
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',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const elevationClass = computed(() => {
|
|
95
|
+
if (!RAISED_VIEWS.includes(props.view)) return ''
|
|
96
|
+
return props.glow ? GLOW_CLASS[props.color] : BASE_ELEVATION
|
|
97
|
+
})
|
|
98
|
+
|
|
68
99
|
const SIZE_CLASS: Record<Size, string> = {
|
|
69
100
|
xs: 'h-6 gap-1 px-2 text-body-sm',
|
|
70
101
|
sm: 'h-8 gap-1.5 px-3 text-body-sm',
|
|
@@ -72,13 +103,24 @@ const SIZE_CLASS: Record<Size, string> = {
|
|
|
72
103
|
lg: 'h-12 gap-2.5 px-5 text-body-lg',
|
|
73
104
|
}
|
|
74
105
|
|
|
106
|
+
/* Квадрат под одну иконку: ЗАМЕНЯЕТ SIZE_CLASS, а не дописывается к нему —
|
|
107
|
+
`px-0` рядом с `px-4` проиграет (конфликт решает порядок в stylesheet), из-за
|
|
108
|
+
чего такие кнопки обычно лепят через `!w-10 !px-0`. */
|
|
109
|
+
const SQUARE_CLASS: Record<Size, string> = {
|
|
110
|
+
xs: 'size-6',
|
|
111
|
+
sm: 'size-8',
|
|
112
|
+
md: 'size-10',
|
|
113
|
+
lg: 'size-12',
|
|
114
|
+
}
|
|
115
|
+
|
|
75
116
|
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
|
-
|
|
117
|
+
'none': 'rounded-none',
|
|
118
|
+
'sm': 'rounded-sm',
|
|
119
|
+
'md': 'rounded-md',
|
|
120
|
+
'lg': 'rounded-lg',
|
|
121
|
+
'xl': 'rounded-xl',
|
|
122
|
+
'2xl': 'rounded-2xl',
|
|
123
|
+
'full': 'rounded-full',
|
|
82
124
|
}
|
|
83
125
|
|
|
84
126
|
const ICON_SIZE: Record<Size, string> = {
|
|
@@ -98,10 +140,11 @@ function onClick(event: MouseEvent) {
|
|
|
98
140
|
<button
|
|
99
141
|
:type="type"
|
|
100
142
|
:disabled="disabled || loading"
|
|
101
|
-
class="button-v2 inline-flex
|
|
143
|
+
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
144
|
:class="[
|
|
103
145
|
VIEW_CLASS[color][view],
|
|
104
|
-
|
|
146
|
+
elevationClass,
|
|
147
|
+
square ? SQUARE_CLASS[size] : SIZE_CLASS[size],
|
|
105
148
|
ROUNDED_CLASS[rounded],
|
|
106
149
|
block ? 'w-full' : '',
|
|
107
150
|
iconRight ? 'flex-row-reverse' : '',
|
|
@@ -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,24 @@ 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
13
|
}>(), {
|
|
14
|
-
color: '
|
|
14
|
+
color: 'var(--color-icon-brand)',
|
|
15
|
+
tint: undefined,
|
|
15
16
|
forceActive: true,
|
|
16
|
-
hoverAccent: false,
|
|
17
17
|
})
|
|
18
18
|
</script>
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
<template>
|
|
21
|
-
<
|
|
21
|
+
<nuxt-link-locale
|
|
22
22
|
: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
|
|
25
|
-
:class="{ 'active-item': !to && forceActive
|
|
23
|
+
:style="{ '--cb-color': color, '--cb-tint': tint }"
|
|
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"
|
|
25
|
+
:class="{ 'active-item': !to && forceActive }"
|
|
26
26
|
active-class="active-item"
|
|
27
27
|
>
|
|
28
|
-
<div class="arrow-icon absolute
|
|
28
|
+
<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
29
|
<svg
|
|
30
30
|
class="size-2"
|
|
31
31
|
viewBox="16 16 16 16"
|
|
@@ -49,36 +49,72 @@ withDefaults(defineProps<{
|
|
|
49
49
|
</div>
|
|
50
50
|
|
|
51
51
|
<div class="flex flex-col gap-2 pr-6">
|
|
52
|
-
<p class="title text-
|
|
52
|
+
<p class="title text-h5 text-primary transition-colors duration-200">
|
|
53
53
|
{{ title }}
|
|
54
54
|
</p>
|
|
55
|
-
<p class="text-body-md leading-relaxed text-
|
|
55
|
+
<p class="text-body-md leading-relaxed text-quaternary">
|
|
56
56
|
{{ description }}
|
|
57
57
|
</p>
|
|
58
58
|
</div>
|
|
59
|
-
</
|
|
59
|
+
</nuxt-link-locale>
|
|
60
60
|
</template>
|
|
61
61
|
|
|
62
62
|
<style scoped>
|
|
63
|
+
.curve-block-v2 {
|
|
64
|
+
--cb-fill: var(--cb-tint, color-mix(in srgb, var(--cb-color) 10%, transparent));
|
|
65
|
+
transition:
|
|
66
|
+
transform 0.2s ease,
|
|
67
|
+
border-color 0.2s ease,
|
|
68
|
+
box-shadow 0.2s ease;
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
.icon-item {
|
|
64
|
-
background-color:
|
|
72
|
+
background-color: var(--cb-fill);
|
|
65
73
|
color: var(--cb-color);
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
.
|
|
76
|
+
.curve-block-v2:hover {
|
|
77
|
+
transform: translateY(-4px);
|
|
69
78
|
border-color: var(--cb-color);
|
|
79
|
+
box-shadow: 0 0 22px -12px var(--cb-color);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.curve-block-v2:hover .title,
|
|
83
|
+
.curve-block-v2:hover .arrow-icon {
|
|
84
|
+
color: var(--cb-color);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.curve-block-v2:hover .arrow-icon {
|
|
88
|
+
background-color: var(--cb-fill);
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
.active-item {
|
|
73
92
|
border-color: var(--cb-color);
|
|
74
|
-
|
|
93
|
+
box-shadow: 0 0 22px -12px var(--cb-color);
|
|
75
94
|
}
|
|
95
|
+
|
|
96
|
+
.active-item:hover {
|
|
97
|
+
transform: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
.active-item .title,
|
|
77
101
|
.active-item .arrow-icon {
|
|
78
102
|
color: var(--cb-color);
|
|
79
103
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
color: var(--
|
|
104
|
+
|
|
105
|
+
.active-item .arrow-icon {
|
|
106
|
+
background-color: var(--cb-fill);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@media (prefers-reduced-motion: reduce) {
|
|
110
|
+
.curve-block-v2,
|
|
111
|
+
.curve-block-v2 .title,
|
|
112
|
+
.curve-block-v2 .arrow-icon {
|
|
113
|
+
transition: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.curve-block-v2:hover {
|
|
117
|
+
transform: none;
|
|
118
|
+
}
|
|
83
119
|
}
|
|
84
120
|
</style>
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
/* fade: gradient — краевые градиенты на bg-main (историческое поведение,
|
|
3
|
+
верно только когда карусель лежит на основной поверхности);
|
|
4
|
+
mask — краевая маска, не зависящая от фона под каруселью. */
|
|
5
|
+
withDefaults(defineProps<{
|
|
6
|
+
duration?: number
|
|
7
|
+
pauseOnHover?: boolean
|
|
8
|
+
fade?: 'gradient' | 'mask' | 'none'
|
|
9
|
+
}>(), {
|
|
10
|
+
duration: 25,
|
|
11
|
+
pauseOnHover: false,
|
|
12
|
+
fade: 'gradient',
|
|
13
|
+
})
|
|
3
14
|
</script>
|
|
4
15
|
|
|
5
16
|
<template>
|
|
6
|
-
<div
|
|
7
|
-
|
|
17
|
+
<div
|
|
18
|
+
class="carousel relative w-full overflow-hidden"
|
|
19
|
+
:class="[
|
|
20
|
+
fade === 'gradient' ? 'carousel--fade-gradient' : '',
|
|
21
|
+
fade === 'mask' ? 'carousel--fade-mask' : '',
|
|
22
|
+
pauseOnHover ? 'carousel--pausable' : '',
|
|
23
|
+
]"
|
|
24
|
+
>
|
|
25
|
+
<div
|
|
26
|
+
class="animate-scroll flex w-max"
|
|
27
|
+
:style="{ animationDuration: `${duration}s` }"
|
|
28
|
+
>
|
|
8
29
|
<div
|
|
9
30
|
v-for="i in 2"
|
|
10
31
|
:key="i"
|
|
@@ -16,17 +37,22 @@
|
|
|
16
37
|
</div>
|
|
17
38
|
</template>
|
|
18
39
|
|
|
19
|
-
<style
|
|
40
|
+
<style scoped>
|
|
20
41
|
@keyframes scroll {
|
|
21
42
|
0% { transform: translateX(0); }
|
|
22
43
|
100% { transform: translateX(-50%); }
|
|
23
44
|
}
|
|
45
|
+
|
|
24
46
|
.animate-scroll {
|
|
25
47
|
animation: scroll 25s linear infinite;
|
|
26
48
|
}
|
|
27
49
|
|
|
28
|
-
.carousel:
|
|
29
|
-
|
|
50
|
+
.carousel--pausable:hover .animate-scroll {
|
|
51
|
+
animation-play-state: paused;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.carousel--fade-gradient:before,
|
|
55
|
+
.carousel--fade-gradient:after {
|
|
30
56
|
position: absolute;
|
|
31
57
|
top: 0;
|
|
32
58
|
width: 120px;
|
|
@@ -35,13 +61,25 @@
|
|
|
35
61
|
z-index: 2;
|
|
36
62
|
}
|
|
37
63
|
|
|
38
|
-
.carousel:before {
|
|
64
|
+
.carousel--fade-gradient:before {
|
|
39
65
|
left: 0;
|
|
40
66
|
background: linear-gradient(to left, transparent, var(--background-color-main));
|
|
41
67
|
}
|
|
42
68
|
|
|
43
|
-
.carousel:after {
|
|
69
|
+
.carousel--fade-gradient:after {
|
|
44
70
|
right: 0;
|
|
45
71
|
background: linear-gradient(to right, transparent, var(--background-color-main));
|
|
46
72
|
}
|
|
73
|
+
|
|
74
|
+
/* Маска читает только альфу, поэтому #000 здесь — не цвет (осознанное
|
|
75
|
+
исключение из запрета сырых hex). */
|
|
76
|
+
.carousel--fade-mask {
|
|
77
|
+
mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@media (prefers-reduced-motion: reduce) {
|
|
81
|
+
.animate-scroll {
|
|
82
|
+
animation: none;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
47
85
|
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Заголовок блока лендинга: Display/Small → Display/Large + подзаголовок.
|
|
3
|
+
// Бейджи-eyebrow в блоках не используются — они живут только в hero и форме обратной связи.
|
|
4
|
+
withDefaults(defineProps<{
|
|
5
|
+
title?: string
|
|
6
|
+
subtitle?: string
|
|
7
|
+
center?: boolean
|
|
8
|
+
}>(), {
|
|
9
|
+
title: '',
|
|
10
|
+
subtitle: '',
|
|
11
|
+
center: false,
|
|
12
|
+
})
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div
|
|
17
|
+
class="flex flex-col gap-2.5"
|
|
18
|
+
:class="center ? 'items-center text-center' : 'text-left'"
|
|
19
|
+
>
|
|
20
|
+
<h2 class="text-display-sm text-primary md:text-display-lg">
|
|
21
|
+
<slot name="title">
|
|
22
|
+
{{ title }}
|
|
23
|
+
</slot>
|
|
24
|
+
</h2>
|
|
25
|
+
<p
|
|
26
|
+
v-if="subtitle || $slots.subtitle"
|
|
27
|
+
class="text-body-lg text-quaternary"
|
|
28
|
+
:class="{ 'max-w-2xl': center }"
|
|
29
|
+
>
|
|
30
|
+
<slot name="subtitle">
|
|
31
|
+
{{ subtitle }}
|
|
32
|
+
</slot>
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|