adata-ui 2.1.41-beta.1 → 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 +1 -1
- package/assets/styles/theme.css +11 -4
- package/components/elements/button/AButtonV2.vue +2 -0
- package/components/elements/curve-block-v2/ACurveBlockV2.vue +2 -0
- package/components/elements/empty-box/empty-box-V2.vue +1 -1
- package/components/elements/error-template/index.vue +8 -3
- 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 +8 -12
- package/components/features/go-top/GoTop.vue +4 -0
- package/components/forms/feedback-form/FeedbackFormV2.vue +16 -16
- package/components/forms/input/AInputV2.vue +43 -39
- 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/header/AHeader.vue +6 -2
- 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/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/package.json +1 -1
- 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()
|
|
@@ -110,10 +110,6 @@ watch(() => [props.interval, props.autoplay, props.steps.length], () => {
|
|
|
110
110
|
</script>
|
|
111
111
|
|
|
112
112
|
<template>
|
|
113
|
-
<!-- eslint-disable tailwindcss/no-unnecessary-arbitrary-value --
|
|
114
|
-
Размеры и типографика скопированы из макета литералами (см. предупреждения
|
|
115
|
-
правила). Токены здесь не использовать: подсказка leading-1.4 в v4 значит
|
|
116
|
-
0.35rem, а не line-height 1.4. -->
|
|
117
113
|
<div
|
|
118
114
|
class="grid grid-cols-1 gap-8"
|
|
119
115
|
:class="colsClass"
|
|
@@ -136,19 +132,19 @@ watch(() => [props.interval, props.autoplay, props.steps.length], () => {
|
|
|
136
132
|
:class="expandable ? 'items-center' : 'items-start'"
|
|
137
133
|
>
|
|
138
134
|
<span
|
|
139
|
-
class="flex size-
|
|
140
|
-
: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')"
|
|
141
137
|
>
|
|
142
138
|
<component
|
|
143
139
|
:is="step.icon"
|
|
144
140
|
v-if="step.icon"
|
|
145
|
-
class="size-5"
|
|
146
|
-
: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')"
|
|
147
143
|
/>
|
|
148
144
|
</span>
|
|
149
145
|
<div class="min-w-0 flex-1">
|
|
150
146
|
<p
|
|
151
|
-
class="block text-
|
|
147
|
+
class="block text-h5 transition-colors"
|
|
152
148
|
:class="i === active ? 'text-primary' : 'text-quaternary'"
|
|
153
149
|
>
|
|
154
150
|
{{ step.title }}
|
|
@@ -156,7 +152,7 @@ watch(() => [props.interval, props.autoplay, props.steps.length], () => {
|
|
|
156
152
|
|
|
157
153
|
<div v-if="!expandable">
|
|
158
154
|
<p
|
|
159
|
-
class="mt-0.5 block text-
|
|
155
|
+
class="mt-0.5 block text-body-md transition-colors"
|
|
160
156
|
:class="i === active ? 'text-quaternary' : 'text-secondary'"
|
|
161
157
|
>
|
|
162
158
|
{{ step.description }}
|
|
@@ -188,7 +184,7 @@ watch(() => [props.interval, props.autoplay, props.steps.length], () => {
|
|
|
188
184
|
: 'grid-rows-[0fr] opacity-0'"
|
|
189
185
|
>
|
|
190
186
|
<div class="overflow-hidden">
|
|
191
|
-
<p class="text-body-
|
|
187
|
+
<p class="text-body-md text-quaternary">
|
|
192
188
|
{{ step.description }}
|
|
193
189
|
</p>
|
|
194
190
|
<div
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { AIconChevronUp } from "#components";
|
|
3
3
|
|
|
4
|
+
const { t } = useI18n()
|
|
5
|
+
|
|
4
6
|
const props = defineProps<{
|
|
5
7
|
bottomOffset?: number
|
|
6
8
|
}>()
|
|
@@ -49,9 +51,11 @@ onUnmounted(() => {
|
|
|
49
51
|
:class="[{ 'opacity-0': !isShow }]"
|
|
50
52
|
:style="{ bottom: footerHeight + 'px' }"
|
|
51
53
|
>
|
|
54
|
+
<!-- Кнопка только с иконкой: без aria-label скринридер читает её как "button". -->
|
|
52
55
|
<a-button
|
|
53
56
|
@click="goTop"
|
|
54
57
|
form="icon"
|
|
58
|
+
:aria-label="t('reuse.goTop')"
|
|
55
59
|
:icon="AIconChevronUp"
|
|
56
60
|
icon-class="h-6 w-6"
|
|
57
61
|
size="lg"
|
|
@@ -5,6 +5,8 @@ import MessageSquare from '#adata-ui/icons/message/message-square.vue'
|
|
|
5
5
|
import AInputV2 from '#adata-ui/components/forms/input/AInputV2.vue'
|
|
6
6
|
import ATextareaV2 from '#adata-ui/components/forms/input/textarea/ATextareaV2.vue'
|
|
7
7
|
import AButtonV2 from '#adata-ui/components/elements/button/AButtonV2.vue'
|
|
8
|
+
import { buildLocalizedUrl } from '#adata-ui/utils/localizedNavigation'
|
|
9
|
+
import { PAGES } from '#adata-ui/shared/constans/pages'
|
|
8
10
|
|
|
9
11
|
const props = withDefaults(defineProps<{
|
|
10
12
|
userInfo: any
|
|
@@ -16,24 +18,22 @@ const props = withDefaults(defineProps<{
|
|
|
16
18
|
})
|
|
17
19
|
|
|
18
20
|
const emit = defineEmits(['submit'])
|
|
19
|
-
|
|
21
|
+
const { locale } = useI18n()
|
|
20
22
|
const { t } = useI18n()
|
|
21
|
-
|
|
22
|
-
const isLoading = defineModel<
|
|
23
|
-
const isModalOpen = defineModel<
|
|
24
|
-
|
|
23
|
+
const { landing } = useUrls()
|
|
24
|
+
const isLoading = defineModel<null | any>('isLoading', { default: false })
|
|
25
|
+
const isModalOpen = defineModel<null | any>('isModalOpen', { default: false })
|
|
25
26
|
const fullName = ref<string>('')
|
|
26
27
|
const phone = ref<string>('')
|
|
27
28
|
const mail = ref<string>('')
|
|
28
29
|
const comment = ref<string>('')
|
|
29
|
-
|
|
30
30
|
const submitted = ref(false)
|
|
31
31
|
|
|
32
32
|
const scheme = z.object({
|
|
33
|
-
fullName: z.string()
|
|
33
|
+
fullName: z.string(),
|
|
34
34
|
phone: z.string().nonempty(t('forms.feedback.fieldRequired')).min(17, t('forms.feedback.phoneMinLength', { count: 10 })),
|
|
35
|
-
mail: z.string()
|
|
36
|
-
comment: z.string().
|
|
35
|
+
mail: z.string(),
|
|
36
|
+
comment: z.string().max(250, t('forms.feedback.commentMaxLength', { count: 250 })),
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
const { validation, getError } = useUIValidation(
|
|
@@ -58,7 +58,6 @@ const send = () => {
|
|
|
58
58
|
mail.value = props.userInfo?.email || ''
|
|
59
59
|
comment.value = ''
|
|
60
60
|
}
|
|
61
|
-
|
|
62
61
|
defineExpose({
|
|
63
62
|
reset: () => (submitted.value = false),
|
|
64
63
|
})
|
|
@@ -73,7 +72,7 @@ onMounted(() => {
|
|
|
73
72
|
<!-- градиент промо-карточки — осознанный литерал (§05 аудита), но флипается в тёмной теме -->
|
|
74
73
|
<div class="grid grid-cols-1 items-center gap-8 rounded-[28px] border border-default bg-linear-to-br from-[#f0f8ff] via-[#eef4ff] to-[#f0f8ff] p-4 md:gap-10 md:p-12 lg:grid-cols-2 dark:from-[#1a2231] dark:via-[#15191f] dark:to-[#121316]">
|
|
75
74
|
<div>
|
|
76
|
-
<div class="mb-4 inline-flex items-center gap-2 rounded-full border border-default bg-main px-3
|
|
75
|
+
<div class="mb-4 inline-flex items-center gap-2 rounded-full border border-default bg-main h-8 px-3 text-body-md text-brand">
|
|
77
76
|
<message-square class="size-3.5" />
|
|
78
77
|
{{ t('forms.feedback.value') }}
|
|
79
78
|
</div>
|
|
@@ -99,7 +98,6 @@ onMounted(() => {
|
|
|
99
98
|
v-model="fullName"
|
|
100
99
|
variant="gray"
|
|
101
100
|
size="md"
|
|
102
|
-
required
|
|
103
101
|
:label="t('forms.demo.n')"
|
|
104
102
|
:error="getError('fullName')"
|
|
105
103
|
:disabled="isLoading"
|
|
@@ -121,7 +119,6 @@ onMounted(() => {
|
|
|
121
119
|
type="email"
|
|
122
120
|
variant="gray"
|
|
123
121
|
size="md"
|
|
124
|
-
required
|
|
125
122
|
:label="t('forms.feedback.email')"
|
|
126
123
|
:error="getError('mail')"
|
|
127
124
|
:disabled="isLoading"
|
|
@@ -136,7 +133,6 @@ onMounted(() => {
|
|
|
136
133
|
:label="t('forms.feedback.comment')"
|
|
137
134
|
:error="getError('comment')"
|
|
138
135
|
:disabled="isLoading"
|
|
139
|
-
required
|
|
140
136
|
/>
|
|
141
137
|
<a-button-v2
|
|
142
138
|
type="submit"
|
|
@@ -149,9 +145,13 @@ onMounted(() => {
|
|
|
149
145
|
</a-button-v2>
|
|
150
146
|
<p class="mt-1 text-center text-body-sm text-disabled">
|
|
151
147
|
{{ t('forms.feedback.consent') }}
|
|
152
|
-
<
|
|
148
|
+
<nuxt-link-locale
|
|
149
|
+
class="cursor-pointer text-brand"
|
|
150
|
+
:to="buildLocalizedUrl(locale, landing, PAGES.privacy)"
|
|
151
|
+
target="_blank"
|
|
152
|
+
>
|
|
153
153
|
{{ t('forms.feedback.consentAccent') }}
|
|
154
|
-
</
|
|
154
|
+
</nuxt-link-locale>
|
|
155
155
|
</p>
|
|
156
156
|
</form>
|
|
157
157
|
</div>
|