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.
Files changed (47) hide show
  1. package/-error.vue +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/assets/styles/theme.css +11 -4
  4. package/components/elements/button/AButtonV2.vue +2 -0
  5. package/components/elements/curve-block-v2/ACurveBlockV2.vue +2 -0
  6. package/components/elements/empty-box/empty-box-V2.vue +1 -1
  7. package/components/elements/error-template/index.vue +8 -3
  8. package/components/elements/select/ACascadeSelectV2.vue +710 -0
  9. package/components/elements/select/ASelectV2.vue +70 -143
  10. package/components/elements/select/cascade-utils.ts +115 -0
  11. package/components/elements/select/types.ts +27 -0
  12. package/components/elements/select/ui/cascade-drawer.vue +146 -0
  13. package/components/elements/select/ui/cascade-panel.vue +237 -0
  14. package/components/elements/select/ui/select-trigger.vue +167 -0
  15. package/components/elements/step-slider/AStepSliderColCard.vue +8 -12
  16. package/components/features/go-top/GoTop.vue +4 -0
  17. package/components/forms/feedback-form/FeedbackFormV2.vue +16 -16
  18. package/components/forms/input/AInputV2.vue +43 -39
  19. package/components/modals/ReportBugModal.vue +1 -1
  20. package/components/modals/id/IdModals.vue +14 -10
  21. package/components/navigation/bottom-navigation/ABottomNavigation.vue +1 -1
  22. package/components/navigation/header/AHeader.vue +6 -2
  23. package/components/overlays/drawer/ADrawerV2.vue +9 -2
  24. package/components/overlays/modal/AModalV2.vue +1 -0
  25. package/icons/ai/ai-diamond-unfilled.vue +26 -0
  26. package/icons/ai/ai-diamond.vue +11 -0
  27. package/icons/arrow/arrow-down-left.vue +6 -0
  28. package/icons/arrow/arrow-up-right.vue +6 -0
  29. package/icons/clock-rotate-ccw.vue +7 -0
  30. package/icons/crown.vue +6 -0
  31. package/icons/list-checks.vue +3 -0
  32. package/icons/log-out.vue +21 -0
  33. package/icons/main-filter.vue +6 -1
  34. package/icons/refresh-cw.vue +8 -0
  35. package/icons/shield-check.vue +20 -0
  36. package/icons/shuffle.vue +3 -0
  37. package/icons/user-plus.vue +3 -0
  38. package/icons/wallet.vue +6 -0
  39. package/illustrations/notes-search.vue +20 -0
  40. package/illustrations/notes-sticky.vue +20 -0
  41. package/illustrations/scheme-notes.vue +21 -0
  42. package/illustrations/scheme-waypoints.vue +25 -0
  43. package/lang/en.ts +2 -1
  44. package/lang/kk.ts +3 -2
  45. package/lang/ru.ts +2 -1
  46. package/package.json +1 -1
  47. 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()
@@ -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-10 shrink-0 items-center justify-center rounded-[12px]"
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-[14px] font-bold transition-colors"
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-[12px] leading-[1.4] transition-colors"
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-sm text-quaternary">
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< null | any>('isLoading', { default: false })
23
- const isModalOpen = defineModel< null | any>('isModalOpen', { default: false })
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().nonempty(t('forms.feedback.fieldRequired')),
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().nonempty(t('forms.feedback.fieldRequired')).email(t('forms.feedback.emailValid')),
36
- comment: z.string().nonempty(t('forms.feedback.fieldRequired')).min(10, t('forms.feedback.commentMinLength', { count: 10 })).max(250, t('forms.feedback.commentMaxLength', { count: 250 })),
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 py-1.5 text-body-md text-brand">
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
- <span class="text-brand">
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
- </span>
154
+ </nuxt-link-locale>
155
155
  </p>
156
156
  </form>
157
157
  </div>