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
@@ -0,0 +1,237 @@
1
+ <script generic="T" lang="ts" setup>
2
+ import type { CascadeKeys } from '../types'
3
+ import { isDisabled, isLeaf, nodeKey, nodeLabel } from '../cascade-utils'
4
+
5
+ /**
6
+ * Одна колонка каскадного селекта. Компонент намеренно НЕ рекурсивен: панели
7
+ * приходят плоским массивом из ACascadeSelectV2, поэтому здесь нет самоссылки
8
+ * (и нет TS7022, на который натыкается рекурсивный ATreeSelectNode).
9
+ */
10
+ defineOptions({ name: 'CascadePanel' })
11
+
12
+ const props = withDefaults(defineProps<{
13
+ options: T[]
14
+ keys: CascadeKeys
15
+ level: number
16
+ /** Ключ раскрытого из этой колонки родителя. */
17
+ activeKey?: unknown
18
+ /** Ключ выбранного листа. */
19
+ selectedKey?: unknown
20
+ /** Ключ строки с roving-фокусом. */
21
+ focusedKey?: unknown
22
+ title?: string
23
+ showBack?: boolean
24
+ backLabel?: string
25
+ width?: string
26
+ minWidth?: string
27
+ maxHeight?: string
28
+ emptyLabel?: string
29
+ /**
30
+ * Колонки растут влево. Зеркалятся только строки-ветки: их шеврон разворачивается
31
+ * и уезжает влево, к своей будущей колонке. Листья (и заголовок, и «Назад») всегда
32
+ * читаются слева направо — у них никуда не ведёт, зеркалить нечего.
33
+ */
34
+ mirrored?: boolean
35
+ /** Без карточной обвязки (рамка/тень/фон) — колонка внутри дровера. */
36
+ bare?: boolean
37
+ }>(), {
38
+ activeKey: undefined,
39
+ selectedKey: undefined,
40
+ focusedKey: undefined,
41
+ title: '',
42
+ showBack: false,
43
+ backLabel: 'Назад',
44
+ width: undefined,
45
+ minWidth: undefined,
46
+ maxHeight: undefined,
47
+ emptyLabel: 'Ничего не найдено',
48
+ mirrored: false,
49
+ bare: false,
50
+ })
51
+
52
+ const emits = defineEmits<{
53
+ /** Клик/Enter по строке — родитель решает, раскрыть ветку или выбрать лист. */
54
+ activate: [node: T]
55
+ hover: [node: T]
56
+ back: []
57
+ }>()
58
+
59
+ defineSlots<{
60
+ option?: (props: { option: T, level: number, selected: boolean, hasChildren: boolean, active: boolean }) => unknown
61
+ empty?: () => unknown
62
+ }>()
63
+
64
+ const root = ref<HTMLElement | null>(null)
65
+ const listEl = ref<HTMLElement | null>(null)
66
+
67
+ const rootClass = computed(() => {
68
+ if (props.bare) return 'w-full text-start'
69
+ const card = 'rounded-lg border border-default bg-main p-1.5 text-start shadow-lg shadow-surface-dark/10 dark:shadow-surface-dark/30'
70
+ /* min-w-45 — фолбэк на первый кадр, пока ширина триггера ещё не измерена. */
71
+ return props.width ? card : `${card} min-w-45`
72
+ })
73
+
74
+ const rootStyle = computed(() => ({
75
+ width: props.bare ? undefined : props.width,
76
+ minWidth: props.bare ? undefined : props.minWidth,
77
+ maxHeight: props.maxHeight,
78
+ }))
79
+
80
+ function keyOf(node: T) {
81
+ return nodeKey(node, props.keys)
82
+ }
83
+
84
+ function hasChildren(node: T) {
85
+ return !isLeaf(node, props.keys)
86
+ }
87
+
88
+ function optionId(node: T) {
89
+ return `cascade-${props.level}-${String(keyOf(node))}`
90
+ }
91
+
92
+ /** Ставит фокус на строку по ключу — используется клавиатурной навигацией родителя. */
93
+ function focusItem(key: unknown) {
94
+ const el = listEl.value?.querySelector<HTMLElement>(`[data-cascade-key="${CSS.escape(String(key))}"]`)
95
+ if (!el) return
96
+ el.focus()
97
+ el.scrollIntoView({ block: 'nearest' })
98
+ }
99
+
100
+ defineExpose({ root, focusItem })
101
+ </script>
102
+
103
+ <template>
104
+ <div
105
+ ref="root"
106
+ class="cascade-panel flex min-w-0 flex-col"
107
+ :class="rootClass"
108
+ :style="rootStyle"
109
+ >
110
+ <button
111
+ v-if="showBack"
112
+ type="button"
113
+ class="mb-0.5 flex shrink-0 items-center gap-1 rounded-md px-2 py-1.5 text-body-sm text-secondary transition-colors hover:bg-action-secondary-hover hover:text-primary"
114
+ @click="emits('back')"
115
+ >
116
+ <!-- Шеврон-левый, развёрнутый на 180°, — точное зеркало правого: пути SVG симметричны. -->
117
+ <a-icon-chevron-left class="size-3.5 shrink-0" :class="{ 'rotate-180': mirrored }" />
118
+ <span>{{ backLabel }}</span>
119
+ </button>
120
+
121
+ <p
122
+ v-else-if="title"
123
+ class="shrink-0 px-3 py-1.5 text-body-sm text-secondary"
124
+ >
125
+ {{ title }}
126
+ </p>
127
+
128
+ <ul
129
+ ref="listEl"
130
+ role="listbox"
131
+ class="cascade-panel__list flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto"
132
+ >
133
+ <li v-if="!options.length" class="px-3 py-2 text-body-sm text-secondary">
134
+ <slot name="empty">
135
+ {{ emptyLabel }}
136
+ </slot>
137
+ </li>
138
+
139
+ <li
140
+ v-for="node in options"
141
+ :key="String(keyOf(node))"
142
+ role="none"
143
+ >
144
+ <button
145
+ :id="optionId(node)"
146
+ type="button"
147
+ role="option"
148
+ :data-cascade-key="String(keyOf(node))"
149
+ :aria-selected="keyOf(node) === selectedKey"
150
+ :aria-haspopup="hasChildren(node) ? 'listbox' : undefined"
151
+ :aria-expanded="hasChildren(node) ? keyOf(node) === activeKey : undefined"
152
+ :disabled="isDisabled(node, keys)"
153
+ :tabindex="keyOf(node) === focusedKey ? 0 : -1"
154
+ :class="{
155
+ 'cascade-panel__option--active': keyOf(node) === activeKey,
156
+ 'cascade-panel__option--selected': keyOf(node) === selectedKey,
157
+ 'cascade-panel__option--branch': hasChildren(node),
158
+ 'hover:bg-action-secondary-hover': !hasChildren(node),
159
+ 'flex-row-reverse text-end': mirrored && hasChildren(node),
160
+ }"
161
+ class="cascade-panel__option flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-body-md transition-colors duration-100 focus-visible:ring-2 focus-visible:ring-brand-500/30 focus-visible:outline-hidden"
162
+ @click="emits('activate', node)"
163
+ @pointerenter="emits('hover', node)"
164
+ >
165
+ <span class="truncate">
166
+ <slot
167
+ name="option"
168
+ :option="node"
169
+ :level="level"
170
+ :selected="keyOf(node) === selectedKey"
171
+ :has-children="hasChildren(node)"
172
+ :active="keyOf(node) === activeKey"
173
+ >{{ nodeLabel(node, keys) }}</slot>
174
+ </span>
175
+
176
+ <a-icon-chevron-right
177
+ v-if="hasChildren(node)"
178
+ class="size-4 shrink-0"
179
+ :class="[
180
+ keyOf(node) === activeKey ? 'text-icon-brand' : 'text-icon-secondary',
181
+ { 'rotate-180': mirrored },
182
+ ]"
183
+ />
184
+ <a-icon-check
185
+ v-else-if="keyOf(node) === selectedKey"
186
+ class="size-4 shrink-0 text-brand"
187
+ />
188
+ </button>
189
+ </li>
190
+ </ul>
191
+ </div>
192
+ </template>
193
+
194
+ <style scoped>
195
+ .cascade-panel__list {
196
+ scrollbar-width: none;
197
+ -ms-overflow-style: none;
198
+ }
199
+
200
+ .cascade-panel__list::-webkit-scrollbar {
201
+ width: 0;
202
+ height: 0;
203
+ display: none;
204
+ }
205
+
206
+ .cascade-panel__option {
207
+ cursor: pointer;
208
+ }
209
+
210
+ .cascade-panel__option:disabled {
211
+ cursor: not-allowed;
212
+ color: var(--text-color-disabled);
213
+ pointer-events: none;
214
+ }
215
+
216
+ /* Раскрытая ветка и выбранный лист — одна и та же brand-подсветка по макету ДС. */
217
+ .cascade-panel__option--active,
218
+ .cascade-panel__option--selected {
219
+ background-color: var(--background-color-brand-subtle);
220
+ color: var(--text-color-brand);
221
+ font-weight: 500;
222
+ }
223
+
224
+ /*
225
+ * Под курсором brand-подсветка обязана остаться brand-подсветкой:
226
+ * • строка с детьми берёт этот фон сразу — иначе на hoverOpenDelay виден серый ховер,
227
+ * который через 120 мс сменяется синим (то самое мигание);
228
+ * • у раскрытой ветки и выбранного листа серый ховер имеет ту же специфичность (0,2,0),
229
+ * так что без явного правила исход зависел бы от порядка стилей в бандле.
230
+ * Цвет текста и вес не трогаем: раскрытая ветка должна оставаться заметнее наведённой.
231
+ */
232
+ .cascade-panel__option--branch:hover,
233
+ .cascade-panel__option--active:hover,
234
+ .cascade-panel__option--selected:hover {
235
+ background-color: var(--background-color-brand-subtle);
236
+ }
237
+ </style>
@@ -0,0 +1,167 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Общий «хром» триггера селектов нового ДС: рамка, плавающий лейбл, крестик очистки
4
+ * и шеврон. Область значения приходит слотом, поэтому одна и та же кнопка обслуживает
5
+ * и одиночный текст (ASelectV2, ACascadeSelectV2), и строку чипов (ASelectV2 multiple).
6
+ *
7
+ * `el` / `valueEl` отдаются наружу через defineExpose: первый нужен как reference-элемент
8
+ * для floating-ui, второй — как контейнер измерения в useChipOverflow.
9
+ */
10
+ defineOptions({ name: 'SelectTrigger' })
11
+
12
+ const props = withDefaults(defineProps<{
13
+ label?: string
14
+ required?: boolean
15
+ size?: 'sm' | 'md'
16
+ disabled?: boolean
17
+ error?: string | string[]
18
+ isOpen?: boolean
19
+ startIcon?: object
20
+ buttonBg?: string
21
+ /** Резервирует место под крестик (pr-16 вместо pr-9) — совпадает с прежним `clearable`. */
22
+ clearable?: boolean
23
+ /** Крестик показывается только когда есть что чистить. */
24
+ showClear?: boolean
25
+ /** Лейбл ведёт себя как floating label (иначе за плейсхолдер отвечает слот). */
26
+ floatingLabel?: boolean
27
+ /** Значение выбрано — лейбл поднят, иначе стоит на месте плейсхолдера. */
28
+ hasValue?: boolean
29
+ }>(), {
30
+ label: '',
31
+ required: false,
32
+ size: 'sm',
33
+ disabled: false,
34
+ error: undefined,
35
+ isOpen: false,
36
+ startIcon: undefined,
37
+ buttonBg: '',
38
+ clearable: true,
39
+ showClear: false,
40
+ floatingLabel: false,
41
+ hasValue: false,
42
+ })
43
+
44
+ const emits = defineEmits<{
45
+ toggle: []
46
+ clear: []
47
+ }>()
48
+
49
+ const el = ref<HTMLButtonElement | null>(null)
50
+ const valueEl = ref<HTMLElement | null>(null)
51
+
52
+ /* Без floating label кнопка центрирует содержимое, с ним — прижимает значение к низу. */
53
+ const layoutClass = computed(() => {
54
+ if (!props.floatingLabel) {
55
+ return props.size === 'md' ? 'h-10 py-1.5' : 'h-9 py-1.5'
56
+ }
57
+ return props.size === 'md' ? 'h-10 pt-3 pb-0.5' : 'h-9 pt-3.5 pb-0.5'
58
+ })
59
+
60
+ defineExpose({ el, valueEl })
61
+ </script>
62
+
63
+ <template>
64
+ <button
65
+ ref="el"
66
+ :disabled="disabled"
67
+ aria-haspopup="listbox"
68
+ :aria-expanded="isOpen"
69
+ :class="[
70
+ layoutClass,
71
+ clearable ? 'pr-16' : 'pr-9',
72
+ { 'select-trigger--error': error },
73
+ disabled
74
+ ? 'border-default bg-badge/70'
75
+ : (buttonBg || 'border-default bg-main'),
76
+ ]"
77
+ class="select-trigger relative flex w-full items-center gap-2 rounded-10 border border-solid pl-4 text-primary transition-colors duration-200 hover:border-brand focus-visible:border-focus focus-visible:ring-2 focus-visible:ring-brand-500/20 focus-visible:outline-hidden"
78
+ @click="emits('toggle')"
79
+ >
80
+ <span
81
+ v-if="floatingLabel && label"
82
+ :data-size="size"
83
+ class="select-trigger__label pointer-events-none absolute top-1.5 left-4 text-[10px] leading-none text-secondary"
84
+ :class="hasValue ? 'select-trigger__label--floated' : 'select-trigger__label--placeholder'"
85
+ >
86
+ {{ label }}<span v-if="required" class="text-danger">&nbsp;*</span>
87
+ </span>
88
+
89
+ <component
90
+ :is="startIcon"
91
+ v-if="startIcon"
92
+ class="shrink-0"
93
+ />
94
+
95
+ <span ref="valueEl" class="relative w-full min-w-0 overflow-hidden text-start leading-tight">
96
+ <slot />
97
+ </span>
98
+
99
+ <!-- span, а не вложенная <button>: вложенные кнопки ломают SSR-разметку при гидрации -->
100
+ <span
101
+ v-if="showClear"
102
+ role="button"
103
+ tabindex="-1"
104
+ aria-label="Очистить"
105
+ class="select-trigger__clear absolute top-1/2 right-9 flex -translate-y-1/2 items-center justify-center rounded-full p-0.5 transition-colors hover:bg-badge"
106
+ @click.stop="emits('clear')"
107
+ @keydown.enter.stop.prevent="emits('clear')"
108
+ @keydown.space.stop.prevent="emits('clear')"
109
+ >
110
+ <a-icon-x-mark class="m-0! size-3.5" />
111
+ </span>
112
+
113
+ <span
114
+ :class="{ 'rotate-180': isOpen }"
115
+ class="absolute top-1/2 right-3 -translate-y-1/2 transition-transform duration-200"
116
+ >
117
+ <a-icon-chevron-down class="m-0! size-4 text-icon-disabled" />
118
+ </span>
119
+ </button>
120
+ </template>
121
+
122
+ <style scoped>
123
+ .select-trigger {
124
+ cursor: pointer;
125
+ }
126
+
127
+ .select-trigger:disabled {
128
+ pointer-events: none;
129
+ cursor: not-allowed;
130
+ opacity: 0.7;
131
+ }
132
+
133
+ .select-trigger--error {
134
+ border-color: var(--border-color-danger);
135
+ }
136
+
137
+ .select-trigger__clear {
138
+ cursor: pointer;
139
+ }
140
+
141
+ .select-trigger__label {
142
+ transform-origin: 0 0;
143
+ transform: translateY(0) scale(1);
144
+ transition:
145
+ transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
146
+ color 200ms ease-out;
147
+ will-change: transform;
148
+ }
149
+
150
+ .select-trigger__label--floated {
151
+ transform: translateY(0) scale(1);
152
+ }
153
+
154
+ .select-trigger__label--placeholder {
155
+ transform: translateY(7px) scale(1.4);
156
+ }
157
+
158
+ .select-trigger__label--placeholder[data-size="sm"] {
159
+ transform: translateY(5px) scale(1.4);
160
+ }
161
+
162
+ @media (prefers-reduced-motion: reduce) {
163
+ .select-trigger__label {
164
+ transition: none;
165
+ }
166
+ }
167
+ </style>
@@ -67,7 +67,7 @@ function stopTicker() {
67
67
 
68
68
  function startTicker() {
69
69
  stopTicker()
70
- if (!props.autoplay || props.steps.length <= 1)
70
+ if (isPaused.value || !props.autoplay || props.steps.length <= 1)
71
71
  return
72
72
  ticker = setInterval(() => {
73
73
  const now = Date.now()
@@ -87,14 +87,18 @@ function goTo(index: number) {
87
87
  startTicker()
88
88
  }
89
89
 
90
+ const isPaused = ref(false)
91
+
90
92
  function onMouseEnter() {
91
- if (props.pauseOnHover)
92
- stopTicker()
93
+ if (!props.pauseOnHover) return
94
+ isPaused.value = true
95
+ stopTicker()
93
96
  }
94
97
 
95
98
  function onMouseLeave() {
96
- if (props.pauseOnHover)
97
- startTicker()
99
+ if (!props.pauseOnHover) return
100
+ isPaused.value = false
101
+ startTicker()
98
102
  }
99
103
 
100
104
  onMounted(startTicker)
@@ -107,42 +111,50 @@ watch(() => [props.interval, props.autoplay, props.steps.length], () => {
107
111
 
108
112
  <template>
109
113
  <div
110
- class="grid grid-cols-1 gap-4 lg:gap-10"
114
+ class="grid grid-cols-1 gap-8"
111
115
  :class="colsClass"
116
+ @mouseenter="onMouseEnter"
117
+ @mouseleave="onMouseLeave"
112
118
  >
113
- <ol class="flex flex-col gap-3">
119
+ <ol class="flex flex-col gap-2">
114
120
  <li
115
121
  v-for="(step, i) in steps"
116
122
  :key="i"
117
- class="ss-card group relative cursor-pointer overflow-hidden rounded-2xl border bg-main transition-all duration-300"
123
+ class="ss-card group relative cursor-pointer overflow-hidden rounded-14 border bg-main transition-[background-color,box-shadow,border-color,opacity] duration-300"
118
124
  :class="i === active
119
- ? 'ss-card-active border-brand-200 dark:border-brand-500/40'
120
- : 'border-default hover:border-brand-100 dark:hover:border-brand-500/25'"
125
+ ? 'ss-card-active border-strong'
126
+ : 'border-default opacity-60 hover:bg-brand-subtle hover:opacity-100'"
121
127
  @click="goTo(i)"
122
128
  >
123
129
  <div class="p-4">
124
130
  <div
125
- class="flex gap-4"
131
+ class="flex gap-[13px]"
126
132
  :class="expandable ? 'items-center' : 'items-start'"
127
133
  >
128
134
  <span
129
- class="flex size-11 shrink-0 items-center justify-center rounded-xl"
130
- :class="step.iconBg || 'bg-brand-subtle'"
135
+ class="flex size-11 shrink-0 items-center justify-center rounded-lg transition-colors duration-300"
136
+ :class="step.iconBg || (i === active ? 'bg-brand' : 'bg-brand-subtle')"
131
137
  >
132
138
  <component
133
139
  :is="step.icon"
134
140
  v-if="step.icon"
135
- class="size-5"
136
- :class="step.iconColor || 'text-brand'"
141
+ class="size-5 transition-colors duration-300"
142
+ :class="step.iconColor || (i === active ? 'text-icon-on-brand' : 'text-icon-brand')"
137
143
  />
138
144
  </span>
139
145
  <div class="min-w-0 flex-1">
140
- <p class="text-body-md-strong md:text-body-lg-strong text-primary">
146
+ <p
147
+ class="block text-h5 transition-colors"
148
+ :class="i === active ? 'text-primary' : 'text-quaternary'"
149
+ >
141
150
  {{ step.title }}
142
151
  </p>
143
152
 
144
153
  <div v-if="!expandable">
145
- <p class="mt-1 text-body-sm text-secondary">
154
+ <p
155
+ class="mt-0.5 block text-body-md transition-colors"
156
+ :class="i === active ? 'text-quaternary' : 'text-secondary'"
157
+ >
146
158
  {{ step.description }}
147
159
  </p>
148
160
  <div
@@ -172,7 +184,7 @@ watch(() => [props.interval, props.autoplay, props.steps.length], () => {
172
184
  : 'grid-rows-[0fr] opacity-0'"
173
185
  >
174
186
  <div class="overflow-hidden">
175
- <p class="text-body-sm text-secondary">
187
+ <p class="text-body-md text-quaternary">
176
188
  {{ step.description }}
177
189
  </p>
178
190
  <div
@@ -194,40 +206,61 @@ watch(() => [props.interval, props.autoplay, props.steps.length], () => {
194
206
  </div>
195
207
  </div>
196
208
 
209
+ <!-- Прогресс — CSS-анимация scaleX, как в макете (не JS-ширина);
210
+ длительность приходит переменной: scoped-стили переименовывают
211
+ @keyframes, поэтому inline-строку animation здесь использовать нельзя.
212
+ Константный бренд-градиент — одинаков в темах. -->
197
213
  <span
198
214
  v-if="i === active && autoplay && steps.length > 1"
199
- class="absolute inset-x-0 bottom-0 h-1 bg-muted"
200
- >
201
- <!-- осознанно константный: фирменный бренд-градиент прогресса, одинаков в темах -->
202
- <span
203
- class="block h-full rounded-r-full bg-linear-to-r from-brand-500 to-brand-400 transition-[width] duration-100 ease-linear"
204
- :style="{ width: fillWidth }"
205
- />
206
- </span>
215
+ :key="active"
216
+ class="cap-prog absolute inset-x-0 bottom-0 h-[3px] bg-linear-to-r from-brand to-[#4da8ff]"
217
+ :class="{ 'is-paused': isPaused }"
218
+ :style="{ '--cap-dur': `${interval}ms` }"
219
+ />
207
220
  </li>
208
221
  </ol>
209
222
 
210
- <div
211
- @mouseenter="onMouseEnter"
212
- @mouseleave="onMouseLeave"
213
- >
214
- <div class="flex justify-center">
215
- <slot
216
- name="panel"
217
- :active="active"
218
- :step="steps[active]"
219
- :progress="progress"
220
- :count="steps.length"
221
- :fill-width="fillWidth"
222
- :go-to="goTo"
223
- />
224
- </div>
223
+ <div class="flex justify-center">
224
+ <slot
225
+ name="panel"
226
+ :active="active"
227
+ :step="steps[active]"
228
+ :progress="progress"
229
+ :count="steps.length"
230
+ :fill-width="fillWidth"
231
+ :go-to="goTo"
232
+ />
225
233
  </div>
226
234
  </div>
227
235
  </template>
228
236
 
229
237
  <style scoped>
230
238
  .ss-card-active {
231
- box-shadow: 0 22px 60px -18px color-mix(in srgb, var(--color-brand-500) 22%, transparent);
239
+ box-shadow: 0 14px 34px -18px color-mix(in srgb, var(--color-brand-500) 32%, transparent);
240
+ }
241
+
242
+ @keyframes cap-prog {
243
+ from {
244
+ transform: scaleX(0);
245
+ }
246
+ to {
247
+ transform: scaleX(1);
248
+ }
249
+ }
250
+
251
+ .cap-prog {
252
+ transform-origin: left;
253
+ animation: cap-prog var(--cap-dur, 4600ms) linear;
254
+ }
255
+
256
+ .cap-prog.is-paused {
257
+ animation-play-state: paused;
258
+ }
259
+
260
+ @media (prefers-reduced-motion: reduce) {
261
+ .cap-prog {
262
+ animation: none;
263
+ transform: none;
264
+ }
232
265
  }
233
266
  </style>