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.
- package/-error.vue +1 -1
- package/CLAUDE.md +3 -2
- package/assets/styles/main.css +4 -3
- package/assets/styles/theme.css +20 -6
- package/assets/styles/ui.css +77 -0
- package/components/elements/button/AButtonV2.vue +60 -15
- package/components/elements/count-up/ACountUp.vue +54 -0
- package/components/elements/curve-block-v2/ACurveBlockV2.vue +56 -18
- package/components/elements/empty-box/empty-box-V2.vue +1 -1
- package/components/elements/error-template/index.vue +8 -3
- package/components/elements/infinite-carousel/AInfiniteCarousel.vue +46 -8
- package/components/elements/section-heading/ASectionHeading.vue +35 -0
- package/components/elements/select/ACascadeSelectV2.vue +710 -0
- package/components/elements/select/ASelectV2.vue +70 -143
- package/components/elements/select/cascade-utils.ts +115 -0
- package/components/elements/select/types.ts +27 -0
- package/components/elements/select/ui/cascade-drawer.vue +146 -0
- package/components/elements/select/ui/cascade-panel.vue +237 -0
- package/components/elements/select/ui/select-trigger.vue +167 -0
- package/components/elements/step-slider/AStepSliderColCard.vue +75 -42
- package/components/elements/step-slider/AStepSliderColNumber.vue +74 -53
- package/components/features/color-mode/AColorMode.client.vue +1 -23
- package/components/features/go-top/GoTop.vue +4 -0
- package/components/forms/feedback-form/FeedbackFormV2.vue +18 -18
- package/components/forms/input/AInputV2.vue +69 -47
- package/components/forms/input/textarea/ATextareaV2.vue +10 -5
- package/components/modals/ReportBugModal.vue +1 -1
- package/components/modals/id/IdModals.vue +14 -10
- package/components/navigation/bottom-navigation/ABottomNavigation.vue +1 -1
- package/components/navigation/footer/AFooter.vue +2 -2
- package/components/navigation/header/AHeader.vue +8 -4
- package/components/navigation/pill-tabs/APillTabsV2.vue +29 -14
- package/components/overlays/drawer/ADrawerV2.vue +9 -2
- package/components/overlays/modal/AModalV2.vue +1 -0
- package/icons/ai/ai-diamond-unfilled.vue +26 -0
- package/icons/ai/ai-diamond.vue +11 -0
- package/icons/arrow/arrow-down-left.vue +6 -0
- package/icons/arrow/arrow-up-right.vue +6 -0
- package/icons/clock-rotate-ccw.vue +7 -0
- package/icons/crown.vue +6 -0
- 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/icons/list-checks.vue +3 -0
- package/icons/log-out.vue +21 -0
- package/icons/main-filter.vue +6 -1
- package/icons/refresh-cw.vue +8 -0
- package/icons/shield-check.vue +20 -0
- package/icons/shuffle.vue +3 -0
- package/icons/user-plus.vue +3 -0
- package/icons/wallet.vue +6 -0
- package/illustrations/notes-search.vue +20 -0
- package/illustrations/notes-sticky.vue +20 -0
- package/illustrations/scheme-notes.vue +21 -0
- package/illustrations/scheme-waypoints.vue +25 -0
- package/lang/en.ts +2 -1
- package/lang/kk.ts +3 -2
- package/lang/ru.ts +2 -1
- package/nuxt.config.ts +7 -4
- package/package.json +2 -2
- 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"> *</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
|
-
|
|
93
|
+
if (!props.pauseOnHover) return
|
|
94
|
+
isPaused.value = true
|
|
95
|
+
stopTicker()
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
function onMouseLeave() {
|
|
96
|
-
if (props.pauseOnHover)
|
|
97
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
120
|
-
: 'border-default hover:
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
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>
|