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 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` — в дарке невидим). **`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;
@@ -288,9 +295,9 @@
288
295
  --text-color-danger: #FF6467;
289
296
  /* bg */
290
297
  --background-color-main: #161617;
291
- --background-color-canvas: #1E2939;
298
+ --background-color-canvas: #0F0F10;
292
299
  --background-color-subtle: #171717;
293
- --background-color-muted: #262626;
300
+ --background-color-muted: #1F1F21;
294
301
  --background-color-overlay: #00000099;
295
302
  --background-color-brand: #068DFF;
296
303
  --background-color-brand-subtle: #068DFF1A;
@@ -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 shadow-xs hover:bg-action-primary-hover hover:shadow-md active:bg-action-primary-active',
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 shadow-xs hover:bg-green-700 hover:shadow-md active:bg-green-800 dark:hover:bg-green-800 dark:active:bg-green-900',
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 shadow-xs hover:bg-action-danger-hover hover:shadow-md active:bg-action-danger-hover',
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 shadow-xs hover:bg-gray-600 hover:shadow-md active:bg-gray-700',
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 shadow-xs hover:bg-brand-subtle hover:shadow-md active:bg-blue-subtle',
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
- full: 'rounded-full',
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 select-none items-center justify-center font-semibold transition-all duration-150 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
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
- SIZE_CLASS[size],
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: '#068dff',
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
- <NuxtLinkLocale
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 transition-all duration-200 hover:-translate-y-1 hover:border-strong hover:shadow-lg"
25
- :class="{ 'active-item': !to && forceActive, 'hover-accent': hoverAccent }"
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 right-4 top-4 flex size-7 items-center justify-center rounded-full bg-brand-subtle p-2 text-brand">
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-body-lg-strong text-primary">
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-secondary">
55
+ <p class="text-body-md leading-relaxed text-quaternary">
56
56
  {{ description }}
57
57
  </p>
58
58
  </div>
59
- </NuxtLinkLocale>
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: color-mix(in srgb, var(--cb-color) 10%, transparent);
72
+ background-color: var(--cb-fill);
65
73
  color: var(--cb-color);
66
74
  }
67
75
 
68
- .hover-accent:hover {
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
- background-color: color-mix(in srgb, var(--cb-color) 10%, transparent);
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
- .active-item .icon-item {
81
- background-color: var(--cb-color);
82
- color: var(--color-icon-on-brand);
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 class="carousel overflow-hidden w-full relative">
7
- <div class="flex w-max animate-scroll">
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 lang="scss">
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:before,
29
- .carousel:after {
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>