adata-ui 2.1.41-beta.19 → 2.1.41-beta.20
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/components/elements/banner/ABanner.vue +2 -2
- package/components/elements/button/AButtonV2.vue +1 -1
- package/components/elements/select/AGroupSelectV2.vue +592 -0
- package/components/elements/status-badge/AStatusBadge.vue +43 -8
- package/components/elements/step-slider/AStepSliderRowCard.vue +197 -0
- package/components/forms/input/AInputV2.vue +1 -1
- package/components/modals/id/IdConfirmAccountOtpModal.vue +1 -4
- package/components/modals/id/IdLoginModal.vue +2 -1
- package/icons/calendar2.vue +23 -0
- package/icons/file/folder.vue +15 -0
- package/icons/file-download.vue +8 -0
- package/icons/filter2.vue +22 -0
- package/icons/funnel.vue +5 -0
- package/icons/qr-code.vue +25 -0
- package/icons/recycle.vue +10 -0
- package/icons/reply.vue +15 -0
- package/icons/target.vue +7 -0
- package/icons/upload.vue +16 -0
- package/icons/user-x.vue +11 -0
- package/package.json +1 -1
|
@@ -39,7 +39,7 @@ onBeforeMount(() => {
|
|
|
39
39
|
v-show="isDark"
|
|
40
40
|
:href="bannerData?.redirect_url"
|
|
41
41
|
target="_blank"
|
|
42
|
-
:class="location === 'horizontal' ? '' : 'mt-4 hidden w-0 min-w-full lg:flex'"
|
|
42
|
+
:class="location === 'horizontal' ? 'hidden lg:block' : 'mt-4 hidden w-0 min-w-full lg:flex'"
|
|
43
43
|
>
|
|
44
44
|
<img
|
|
45
45
|
class="w-full"
|
|
@@ -51,7 +51,7 @@ onBeforeMount(() => {
|
|
|
51
51
|
v-show="!isDark"
|
|
52
52
|
:href="bannerData?.redirect_url"
|
|
53
53
|
target="_blank"
|
|
54
|
-
:class="location === 'horizontal' ? '' : 'mt-4 hidden w-0 min-w-full lg:flex'"
|
|
54
|
+
:class="location === 'horizontal' ? 'hidden lg:block' : 'mt-4 hidden w-0 min-w-full lg:flex'"
|
|
55
55
|
>
|
|
56
56
|
<img
|
|
57
57
|
class="w-full"
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
<script generic="T" lang="ts" setup>
|
|
2
|
+
import type { Placement } from '@floating-ui/vue'
|
|
3
|
+
import { toCssSize } from '#adata-ui/utils/toCssSize'
|
|
4
|
+
import { autoUpdate, flip, size as floatingSize, offset, shift, useFloating } from '@floating-ui/vue'
|
|
5
|
+
import { onClickOutside, useToggle } from '@vueuse/core'
|
|
6
|
+
import { isEqual } from 'lodash-es'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Селект по двухуровневому справочнику: категория и вложенные в неё виды.
|
|
10
|
+
*
|
|
11
|
+
* Отличие от `ACascadeSelectV2` — тот показывает уровни отдельными колонками и даёт
|
|
12
|
+
* выбрать ТОЛЬКО лист. Здесь оба уровня лежат в одном списке и категория выбирается
|
|
13
|
+
* наравне с дочерним видом (`selectableParents`): справочники вроде «вида нарушения»
|
|
14
|
+
* допускают и общую категорию, и уточнение.
|
|
15
|
+
*
|
|
16
|
+
* На вход идёт ПЛОСКИЙ список — ровно в том виде, в каком его отдаёт бэк
|
|
17
|
+
* (`[{ id, name, parent_id }]`). Группировку компонент делает сам, поэтому
|
|
18
|
+
* консьюмерам не нужно раскладывать справочник в дерево у себя.
|
|
19
|
+
*
|
|
20
|
+
* Весь «хром» триггера — общий `a-select-trigger`, тот же, что у `ASelectV2`,
|
|
21
|
+
* поэтому поле выглядит идентично обычному селекту.
|
|
22
|
+
*/
|
|
23
|
+
defineOptions({ name: 'AGroupSelectV2' })
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
26
|
+
required: false,
|
|
27
|
+
keyToShow: 'name',
|
|
28
|
+
keyToSelect: 'id',
|
|
29
|
+
parentKey: 'parent_id',
|
|
30
|
+
disabledKey: undefined,
|
|
31
|
+
getOnlyKey: false,
|
|
32
|
+
selectableParents: true,
|
|
33
|
+
clearable: true,
|
|
34
|
+
disabled: false,
|
|
35
|
+
size: 'sm',
|
|
36
|
+
error: undefined,
|
|
37
|
+
startIcon: undefined,
|
|
38
|
+
buttonBg: '',
|
|
39
|
+
searchable: false,
|
|
40
|
+
searchLabel: 'Поиск...',
|
|
41
|
+
showPathInTrigger: false,
|
|
42
|
+
dropdownWidth: undefined,
|
|
43
|
+
dropdownMaxHeight: undefined,
|
|
44
|
+
dropdownAlign: 'start',
|
|
45
|
+
emptyLabel: 'Ничего не найдено',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const emits = defineEmits<Emits>()
|
|
49
|
+
|
|
50
|
+
defineSlots<{
|
|
51
|
+
'option'?: (props: { option: T, isParent: boolean, selected: boolean }) => unknown
|
|
52
|
+
'single-selected'?: (props: { value: T, parent: T | null }) => unknown
|
|
53
|
+
'empty'?: () => unknown
|
|
54
|
+
}>()
|
|
55
|
+
|
|
56
|
+
interface Props {
|
|
57
|
+
label: string
|
|
58
|
+
/** Плоский список: вложенность задаётся через `parentKey`, а не массивом детей. */
|
|
59
|
+
options: T[]
|
|
60
|
+
required?: boolean
|
|
61
|
+
keyToShow?: string
|
|
62
|
+
keyToSelect?: string
|
|
63
|
+
/** Ключ со ссылкой на родителя. `null`/`undefined` — элемент верхнего уровня. */
|
|
64
|
+
parentKey?: string
|
|
65
|
+
/** Необязательный ключ-флаг «опция недоступна». */
|
|
66
|
+
disabledKey?: string
|
|
67
|
+
/** `true` — в v-model лежит ключ опции, `false` — сам объект. */
|
|
68
|
+
getOnlyKey?: boolean
|
|
69
|
+
/** Можно ли выбрать саму категорию, а не только вложенный вид. */
|
|
70
|
+
selectableParents?: boolean
|
|
71
|
+
clearable?: boolean
|
|
72
|
+
disabled?: boolean
|
|
73
|
+
size?: 'sm' | 'md'
|
|
74
|
+
error?: string | string[]
|
|
75
|
+
startIcon?: object
|
|
76
|
+
buttonBg?: string
|
|
77
|
+
searchable?: boolean
|
|
78
|
+
searchLabel?: string
|
|
79
|
+
/** Показывать в триггере весь путь («Категория / Вид»). */
|
|
80
|
+
showPathInTrigger?: boolean
|
|
81
|
+
dropdownWidth?: number | string
|
|
82
|
+
dropdownMaxHeight?: number | string
|
|
83
|
+
dropdownAlign?: 'start' | 'center' | 'end'
|
|
84
|
+
emptyLabel?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Emits {
|
|
88
|
+
(e: 'onChange', option: T, parent: T | null): void
|
|
89
|
+
(e: 'onClear'): void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const modelValue = defineModel<T | unknown | null>({ required: true })
|
|
93
|
+
const isOpen = defineModel<boolean>('open', { default: false })
|
|
94
|
+
|
|
95
|
+
function readKey(node: T, key: string): unknown {
|
|
96
|
+
return (node as Record<string, unknown>)[key]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function nodeKey(node: T): unknown {
|
|
100
|
+
return readKey(node, props.keyToSelect)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function nodeLabel(node: T): string {
|
|
104
|
+
return String(readKey(node, props.keyToShow) ?? '')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function nodeDisabled(node: T): boolean {
|
|
108
|
+
return props.disabledKey ? Boolean(readKey(node, props.disabledKey)) : false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// -- Группировка -----------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
interface Group {
|
|
114
|
+
parent: T
|
|
115
|
+
children: T[]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Один проход по плоскому списку. Элемент считается дочерним, только если его
|
|
120
|
+
* `parent_id` реально указывает на существующий верхнеуровневый элемент —
|
|
121
|
+
* «сирота» со ссылкой в никуда поднимается в корень, а не теряется из списка.
|
|
122
|
+
*/
|
|
123
|
+
const groups = computed<Group[]>(() => {
|
|
124
|
+
const items = props.options ?? []
|
|
125
|
+
if (!items.length) return []
|
|
126
|
+
|
|
127
|
+
const rootKeys = new Set(
|
|
128
|
+
items.filter(item => readKey(item, props.parentKey) == null).map(item => nodeKey(item)),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const childrenByParent = new Map<unknown, T[]>()
|
|
132
|
+
const roots: T[] = []
|
|
133
|
+
|
|
134
|
+
items.forEach((item) => {
|
|
135
|
+
const parentId = readKey(item, props.parentKey)
|
|
136
|
+
if (parentId != null && rootKeys.has(parentId)) {
|
|
137
|
+
const siblings = childrenByParent.get(parentId) ?? []
|
|
138
|
+
siblings.push(item)
|
|
139
|
+
childrenByParent.set(parentId, siblings)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
roots.push(item)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return roots.map(root => ({ parent: root, children: childrenByParent.get(nodeKey(root)) ?? [] }))
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
/** Плоский индекс «ключ → узел и его родитель» — по нему решается отображение в триггере. */
|
|
149
|
+
const index = computed(() => {
|
|
150
|
+
const map = new Map<unknown, { node: T, parent: T | null }>()
|
|
151
|
+
groups.value.forEach(({ parent, children }) => {
|
|
152
|
+
map.set(nodeKey(parent), { node: parent, parent: null })
|
|
153
|
+
children.forEach(child => map.set(nodeKey(child), { node: child, parent }))
|
|
154
|
+
})
|
|
155
|
+
return map
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// -- Поиск -----------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
const searchValue = ref('')
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Группа остаётся в списке, если совпал сам заголовок (тогда показываем её целиком)
|
|
164
|
+
* или хоть один вложенный вид (тогда заголовок остаётся как контекст, даже если
|
|
165
|
+
* сам не совпал — иначе непонятно, к чему относится найденный вид).
|
|
166
|
+
*/
|
|
167
|
+
const visibleGroups = computed<Group[]>(() => {
|
|
168
|
+
const needle = searchValue.value.trim().toLowerCase()
|
|
169
|
+
if (!needle) return groups.value
|
|
170
|
+
|
|
171
|
+
return groups.value.flatMap(({ parent, children }) => {
|
|
172
|
+
if (nodeLabel(parent).toLowerCase().includes(needle)) return [{ parent, children }]
|
|
173
|
+
const matched = children.filter(child => nodeLabel(child).toLowerCase().includes(needle))
|
|
174
|
+
return matched.length ? [{ parent, children: matched }] : []
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const isEmpty = computed(() => visibleGroups.value.length === 0)
|
|
179
|
+
|
|
180
|
+
// -- Выбранное значение ----------------------------------------------------
|
|
181
|
+
|
|
182
|
+
const selectedKey = computed<unknown>(() => {
|
|
183
|
+
const value = modelValue.value
|
|
184
|
+
if (value == null || value === '') return undefined
|
|
185
|
+
return props.getOnlyKey ? value : nodeKey(value as T)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const selectedEntry = computed(() =>
|
|
189
|
+
selectedKey.value === undefined ? null : index.value.get(selectedKey.value) ?? null,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const displayValue = computed(() => {
|
|
193
|
+
const entry = selectedEntry.value
|
|
194
|
+
if (!entry) return ''
|
|
195
|
+
if (props.showPathInTrigger && entry.parent) {
|
|
196
|
+
return `${nodeLabel(entry.parent)} / ${nodeLabel(entry.node)}`
|
|
197
|
+
}
|
|
198
|
+
return nodeLabel(entry.node)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
function isSelected(node: T): boolean {
|
|
202
|
+
return selectedKey.value !== undefined && isEqual(selectedKey.value, nodeKey(node))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Заголовок группы кликабелен, только если категории вообще разрешено выбирать. */
|
|
206
|
+
function isParentSelectable(parent: T): boolean {
|
|
207
|
+
return props.selectableParents && !nodeDisabled(parent)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// -- Позиционирование ------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
const wrapper = ref<HTMLElement | null>(null)
|
|
213
|
+
const triggerRef = ref<{ el: HTMLButtonElement | null, valueEl: HTMLElement | null } | null>(null)
|
|
214
|
+
const reference = computed<HTMLElement | null>(() => triggerRef.value?.el ?? null)
|
|
215
|
+
const floating = ref<HTMLElement | null>(null)
|
|
216
|
+
|
|
217
|
+
const dropdownPlacement = computed<Placement>(() => {
|
|
218
|
+
if (props.dropdownAlign === 'center') return 'bottom'
|
|
219
|
+
if (props.dropdownAlign === 'end') return 'bottom-end'
|
|
220
|
+
return 'bottom-start'
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const dropdownFallback = computed<Placement[]>(() => {
|
|
224
|
+
if (props.dropdownAlign === 'center') return ['top']
|
|
225
|
+
if (props.dropdownAlign === 'end') return ['top-end']
|
|
226
|
+
return ['top-start']
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const { x, y, strategy, update } = useFloating(reference, floating, {
|
|
230
|
+
placement: dropdownPlacement,
|
|
231
|
+
strategy: 'absolute',
|
|
232
|
+
middleware: computed(() => [
|
|
233
|
+
offset(6),
|
|
234
|
+
flip({ fallbackPlacements: dropdownFallback.value }),
|
|
235
|
+
shift({ padding: 8 }),
|
|
236
|
+
floatingSize({
|
|
237
|
+
apply({ availableHeight, elements, rects }) {
|
|
238
|
+
const el = elements.floating as HTMLElement
|
|
239
|
+
el.style.width = toCssSize(props.dropdownWidth) ?? `${rects.reference.width}px`
|
|
240
|
+
const maxHeightOverride = toCssSize(props.dropdownMaxHeight)
|
|
241
|
+
el.style.maxHeight = maxHeightOverride
|
|
242
|
+
? `min(${maxHeightOverride}, ${availableHeight}px)`
|
|
243
|
+
: `${Math.max(140, Math.min(availableHeight, 320))}px`
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
]),
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
let cleanup: (() => void) | null = null
|
|
250
|
+
watch(isOpen, (open) => {
|
|
251
|
+
if (open && reference.value && floating.value) {
|
|
252
|
+
cleanup = autoUpdate(reference.value, floating.value, update)
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
cleanup?.()
|
|
256
|
+
cleanup = null
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
onUnmounted(() => cleanup?.())
|
|
260
|
+
|
|
261
|
+
const toggleOpen = useToggle(isOpen)
|
|
262
|
+
|
|
263
|
+
onClickOutside(wrapper, () => {
|
|
264
|
+
if (isOpen.value) toggleOpen(false)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// -- Действия --------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
function onSelect(node: T) {
|
|
270
|
+
if (nodeDisabled(node)) return
|
|
271
|
+
modelValue.value = props.getOnlyKey ? nodeKey(node) : node
|
|
272
|
+
emits('onChange', node, index.value.get(nodeKey(node))?.parent ?? null)
|
|
273
|
+
toggleOpen(false)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function onClear() {
|
|
277
|
+
modelValue.value = null
|
|
278
|
+
emits('onClear')
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
defineExpose({ onClear })
|
|
282
|
+
|
|
283
|
+
// -- Клавиатура ------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/** Строки в порядке отрисовки — только те, на которые реально можно встать. */
|
|
286
|
+
const navigableRows = computed<T[]>(() =>
|
|
287
|
+
visibleGroups.value.flatMap(({ parent, children }) => [
|
|
288
|
+
...(isParentSelectable(parent) ? [parent] : []),
|
|
289
|
+
...children.filter(child => !nodeDisabled(child)),
|
|
290
|
+
]),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
const focusedKey = ref<unknown>(undefined)
|
|
294
|
+
const listRef = ref<HTMLElement | null>(null)
|
|
295
|
+
|
|
296
|
+
function focusRow(key: unknown) {
|
|
297
|
+
focusedKey.value = key
|
|
298
|
+
nextTick(() => {
|
|
299
|
+
listRef.value
|
|
300
|
+
?.querySelector<HTMLElement>(`[data-row-key="${String(key)}"]`)
|
|
301
|
+
?.scrollIntoView({ block: 'nearest' })
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function moveFocus(delta: number) {
|
|
306
|
+
const rows = navigableRows.value
|
|
307
|
+
if (!rows.length) return
|
|
308
|
+
const current = rows.findIndex(row => isEqual(nodeKey(row), focusedKey.value))
|
|
309
|
+
const next = current === -1
|
|
310
|
+
? (delta > 0 ? 0 : rows.length - 1)
|
|
311
|
+
: (current + delta + rows.length) % rows.length
|
|
312
|
+
focusRow(nodeKey(rows[next]))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function onKeydown(event: KeyboardEvent) {
|
|
316
|
+
if (props.disabled) return
|
|
317
|
+
|
|
318
|
+
if (!isOpen.value) {
|
|
319
|
+
if (event.key === 'ArrowDown') {
|
|
320
|
+
event.preventDefault()
|
|
321
|
+
toggleOpen(true)
|
|
322
|
+
}
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
switch (event.key) {
|
|
327
|
+
case 'Escape':
|
|
328
|
+
event.preventDefault()
|
|
329
|
+
toggleOpen(false)
|
|
330
|
+
nextTick(() => reference.value?.focus())
|
|
331
|
+
break
|
|
332
|
+
case 'Tab':
|
|
333
|
+
toggleOpen(false)
|
|
334
|
+
break
|
|
335
|
+
case 'ArrowDown':
|
|
336
|
+
event.preventDefault()
|
|
337
|
+
moveFocus(1)
|
|
338
|
+
break
|
|
339
|
+
case 'ArrowUp':
|
|
340
|
+
event.preventDefault()
|
|
341
|
+
moveFocus(-1)
|
|
342
|
+
break
|
|
343
|
+
case 'Home':
|
|
344
|
+
event.preventDefault()
|
|
345
|
+
if (navigableRows.value.length) focusRow(nodeKey(navigableRows.value[0]))
|
|
346
|
+
break
|
|
347
|
+
case 'End': {
|
|
348
|
+
event.preventDefault()
|
|
349
|
+
const rows = navigableRows.value
|
|
350
|
+
if (rows.length) focusRow(nodeKey(rows[rows.length - 1]))
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
case 'Enter': {
|
|
354
|
+
const row = navigableRows.value.find(item => isEqual(nodeKey(item), focusedKey.value))
|
|
355
|
+
if (!row) break
|
|
356
|
+
event.preventDefault()
|
|
357
|
+
onSelect(row)
|
|
358
|
+
break
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Открытие — на выбранном значении, закрытие — сброс поиска и фокуса. */
|
|
364
|
+
watch(isOpen, (open) => {
|
|
365
|
+
if (open) {
|
|
366
|
+
focusedKey.value = selectedKey.value ?? nodeKey(navigableRows.value[0] ?? ({} as T))
|
|
367
|
+
nextTick(() => focusRow(focusedKey.value))
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
searchValue.value = ''
|
|
371
|
+
focusedKey.value = undefined
|
|
372
|
+
})
|
|
373
|
+
</script>
|
|
374
|
+
|
|
375
|
+
<template>
|
|
376
|
+
<div
|
|
377
|
+
ref="wrapper"
|
|
378
|
+
class="group-select relative w-full text-body-md"
|
|
379
|
+
@keydown="onKeydown"
|
|
380
|
+
>
|
|
381
|
+
<a-select-trigger
|
|
382
|
+
ref="triggerRef"
|
|
383
|
+
:label="label"
|
|
384
|
+
:required="required"
|
|
385
|
+
:size="size"
|
|
386
|
+
:disabled="disabled"
|
|
387
|
+
:error="error"
|
|
388
|
+
:is-open="isOpen"
|
|
389
|
+
:start-icon="startIcon"
|
|
390
|
+
:button-bg="buttonBg"
|
|
391
|
+
:clearable="clearable"
|
|
392
|
+
:show-clear="clearable && !!selectedEntry"
|
|
393
|
+
floating-label
|
|
394
|
+
:has-value="!!selectedEntry"
|
|
395
|
+
@toggle="toggleOpen()"
|
|
396
|
+
@clear="onClear"
|
|
397
|
+
>
|
|
398
|
+
<span v-if="selectedEntry" class="block truncate leading-tight">
|
|
399
|
+
<slot
|
|
400
|
+
name="single-selected"
|
|
401
|
+
:value="selectedEntry.node"
|
|
402
|
+
:parent="selectedEntry.parent"
|
|
403
|
+
>{{ displayValue }}</slot>
|
|
404
|
+
</span>
|
|
405
|
+
<span v-else class="text-secondary">​</span>
|
|
406
|
+
</a-select-trigger>
|
|
407
|
+
|
|
408
|
+
<transition
|
|
409
|
+
enter-active-class="group-select__dropdown-enter-active"
|
|
410
|
+
enter-from-class="group-select__dropdown-enter-from"
|
|
411
|
+
enter-to-class="group-select__dropdown-enter-to"
|
|
412
|
+
leave-active-class="group-select__dropdown-leave-active"
|
|
413
|
+
leave-from-class="group-select__dropdown-leave-from"
|
|
414
|
+
leave-to-class="group-select__dropdown-leave-to"
|
|
415
|
+
>
|
|
416
|
+
<div
|
|
417
|
+
v-if="isOpen"
|
|
418
|
+
ref="floating"
|
|
419
|
+
class="group-select__dropdown z-10000 rounded-lg border border-default bg-main shadow-lg shadow-surface-dark/10 dark:shadow-surface-dark/30"
|
|
420
|
+
:style="{
|
|
421
|
+
position: strategy,
|
|
422
|
+
left: x != null ? `${x}px` : '',
|
|
423
|
+
top: y != null ? `${y}px` : '',
|
|
424
|
+
}"
|
|
425
|
+
>
|
|
426
|
+
<div v-if="searchable" class="px-2.5 pt-2.5">
|
|
427
|
+
<a-input-standard
|
|
428
|
+
v-model="searchValue"
|
|
429
|
+
:label="searchLabel"
|
|
430
|
+
:size="size"
|
|
431
|
+
clearable
|
|
432
|
+
color="gray"
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<ul
|
|
437
|
+
ref="listRef"
|
|
438
|
+
class="group-select__list flex flex-col gap-0.5 p-1.5"
|
|
439
|
+
role="listbox"
|
|
440
|
+
>
|
|
441
|
+
<li v-if="isEmpty" class="px-3 py-6 text-center text-body-sm text-quaternary">
|
|
442
|
+
<slot name="empty">
|
|
443
|
+
{{ emptyLabel }}
|
|
444
|
+
</slot>
|
|
445
|
+
</li>
|
|
446
|
+
|
|
447
|
+
<li
|
|
448
|
+
v-for="group in visibleGroups"
|
|
449
|
+
:key="String(nodeKey(group.parent))"
|
|
450
|
+
>
|
|
451
|
+
<!-- Категория: кнопка, когда её разрешено выбирать, иначе просто заголовок. -->
|
|
452
|
+
<component
|
|
453
|
+
:is="isParentSelectable(group.parent) ? 'button' : 'div'"
|
|
454
|
+
:data-row-key="String(nodeKey(group.parent))"
|
|
455
|
+
:type="isParentSelectable(group.parent) ? 'button' : undefined"
|
|
456
|
+
:role="isParentSelectable(group.parent) ? 'option' : 'presentation'"
|
|
457
|
+
:aria-selected="isParentSelectable(group.parent) ? isSelected(group.parent) : undefined"
|
|
458
|
+
:disabled="isParentSelectable(group.parent) ? nodeDisabled(group.parent) : undefined"
|
|
459
|
+
class="flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-start"
|
|
460
|
+
:class="[
|
|
461
|
+
isParentSelectable(group.parent)
|
|
462
|
+
? 'group-select__row text-body-md-medium text-primary transition-colors duration-100 hover:bg-action-secondary-hover'
|
|
463
|
+
: 'text-overline text-quaternary',
|
|
464
|
+
{ 'group-select__row--selected': isSelected(group.parent) },
|
|
465
|
+
{ 'group-select__row--focused': isEqual(focusedKey, nodeKey(group.parent)) },
|
|
466
|
+
]"
|
|
467
|
+
@click="isParentSelectable(group.parent) ? onSelect(group.parent) : undefined"
|
|
468
|
+
>
|
|
469
|
+
<span class="min-w-0 flex-1 truncate">
|
|
470
|
+
<slot
|
|
471
|
+
name="option"
|
|
472
|
+
:option="group.parent"
|
|
473
|
+
:is-parent="true"
|
|
474
|
+
:selected="isSelected(group.parent)"
|
|
475
|
+
>{{ nodeLabel(group.parent) }}</slot>
|
|
476
|
+
</span>
|
|
477
|
+
<a-icon-check
|
|
478
|
+
v-if="isSelected(group.parent)"
|
|
479
|
+
class="size-4 shrink-0 text-brand"
|
|
480
|
+
/>
|
|
481
|
+
</component>
|
|
482
|
+
|
|
483
|
+
<!-- Вложенные виды: отступ и точка-маркер, чтобы связь с категорией читалась. -->
|
|
484
|
+
<ul v-if="group.children.length" class="mt-0.5 flex flex-col gap-0.5">
|
|
485
|
+
<li v-for="child in group.children" :key="String(nodeKey(child))">
|
|
486
|
+
<button
|
|
487
|
+
type="button"
|
|
488
|
+
role="option"
|
|
489
|
+
:data-row-key="String(nodeKey(child))"
|
|
490
|
+
:aria-selected="isSelected(child)"
|
|
491
|
+
:disabled="nodeDisabled(child)"
|
|
492
|
+
class="group-select__row flex w-full items-center gap-2 rounded-md py-2 pr-3 pl-6 text-start text-body-md text-secondary transition-colors duration-100 hover:bg-action-secondary-hover"
|
|
493
|
+
:class="[
|
|
494
|
+
{ 'group-select__row--selected': isSelected(child) },
|
|
495
|
+
{ 'group-select__row--focused': isEqual(focusedKey, nodeKey(child)) },
|
|
496
|
+
]"
|
|
497
|
+
@click="onSelect(child)"
|
|
498
|
+
>
|
|
499
|
+
<span class="group-select__bullet shrink-0 rounded-full" />
|
|
500
|
+
<span class="min-w-0 flex-1 truncate">
|
|
501
|
+
<slot
|
|
502
|
+
name="option"
|
|
503
|
+
:option="child"
|
|
504
|
+
:is-parent="false"
|
|
505
|
+
:selected="isSelected(child)"
|
|
506
|
+
>{{ nodeLabel(child) }}</slot>
|
|
507
|
+
</span>
|
|
508
|
+
<a-icon-check
|
|
509
|
+
v-if="isSelected(child)"
|
|
510
|
+
class="size-4 shrink-0 text-brand"
|
|
511
|
+
/>
|
|
512
|
+
</button>
|
|
513
|
+
</li>
|
|
514
|
+
</ul>
|
|
515
|
+
</li>
|
|
516
|
+
</ul>
|
|
517
|
+
</div>
|
|
518
|
+
</transition>
|
|
519
|
+
</div>
|
|
520
|
+
</template>
|
|
521
|
+
|
|
522
|
+
<style scoped>
|
|
523
|
+
.group-select__dropdown-enter-active,
|
|
524
|
+
.group-select__dropdown-leave-active {
|
|
525
|
+
transition:
|
|
526
|
+
opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
527
|
+
transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.group-select__dropdown-enter-from,
|
|
531
|
+
.group-select__dropdown-leave-to {
|
|
532
|
+
opacity: 0;
|
|
533
|
+
transform: translateY(-4px);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.group-select__dropdown-enter-to,
|
|
537
|
+
.group-select__dropdown-leave-from {
|
|
538
|
+
opacity: 1;
|
|
539
|
+
transform: translateY(0);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.group-select__list {
|
|
543
|
+
max-height: 250px;
|
|
544
|
+
overflow-y: auto;
|
|
545
|
+
overflow-x: hidden;
|
|
546
|
+
scrollbar-width: none;
|
|
547
|
+
-ms-overflow-style: none;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.group-select__list::-webkit-scrollbar {
|
|
551
|
+
width: 0;
|
|
552
|
+
height: 0;
|
|
553
|
+
display: none;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.group-select__row {
|
|
557
|
+
cursor: pointer;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/* Размер и цвет точки — здесь, а не утилитами: consumer может тянуть CSS слоя
|
|
561
|
+
из npm-копии, и утилита, которую больше никто не использует, не сгенерируется. */
|
|
562
|
+
.group-select__bullet {
|
|
563
|
+
width: 6px;
|
|
564
|
+
height: 6px;
|
|
565
|
+
background-color: var(--color-icon-secondary);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.group-select__row:disabled {
|
|
569
|
+
cursor: default;
|
|
570
|
+
opacity: 0.5;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/* Фокус с клавиатуры: рамка внутрь, чтобы не спорить с соседними строками.
|
|
574
|
+
Цвет сильно разбавлен — сплошной --border-color-focus на узкой строке бьёт по глазам. */
|
|
575
|
+
.group-select__row--focused {
|
|
576
|
+
outline: 1px solid color-mix(in srgb, var(--border-color-focus) 35%, transparent);
|
|
577
|
+
outline-offset: -1px;
|
|
578
|
+
background-color: color-mix(in srgb, var(--border-color-focus) 6%, transparent);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.group-select__row--selected {
|
|
582
|
+
background-color: var(--background-color-brand-subtle);
|
|
583
|
+
font-weight: 500;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
@media (prefers-reduced-motion: reduce) {
|
|
587
|
+
.group-select__dropdown-enter-active,
|
|
588
|
+
.group-select__dropdown-leave-active {
|
|
589
|
+
transition: none;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
</style>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import { useResizeObserver } from '@vueuse/core'
|
|
3
|
+
|
|
2
4
|
type BadgeType = 'primary' | 'success' | 'danger' | 'gray' | 'warning'
|
|
3
5
|
type BadgeView = 'default' | 'inverted' | 'transparent' | 'subtle'
|
|
4
6
|
type BadgeSize = 'xs' | 'sm' | 'md' | 'lg'
|
|
@@ -22,7 +24,7 @@ interface Props {
|
|
|
22
24
|
customClasses?: string
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
defineOptions({ name: 'AStatusBadge' })
|
|
27
|
+
defineOptions({ name: 'AStatusBadge', inheritAttrs: false })
|
|
26
28
|
|
|
27
29
|
const props = withDefaults(defineProps<Props>(), {
|
|
28
30
|
type: 'primary',
|
|
@@ -100,7 +102,7 @@ const pointSwitchValues: ViewType = {
|
|
|
100
102
|
|
|
101
103
|
const classes = computed(() =>
|
|
102
104
|
[
|
|
103
|
-
'text-body-md rounded-2xl select-none inline-flex gap-[5.5px] justify-center items-center border',
|
|
105
|
+
'text-body-md rounded-2xl select-none inline-flex min-w-0 max-w-full gap-[5.5px] justify-center items-center border',
|
|
104
106
|
typeSwitchValues[props.view][props.type],
|
|
105
107
|
props.size === 'xs' ? 'px-2 py-[2px] text-body-sm font-bold' : '',
|
|
106
108
|
props.size === 'sm' ? 'px-[10px] py-1 text-body-sm font-semibold h-6' : '',
|
|
@@ -113,13 +115,46 @@ const pointClasses = computed(() =>
|
|
|
113
115
|
'w-1.5 h-1.5 rounded-full shrink-0',
|
|
114
116
|
pointSwitchValues[props.view][props.type],
|
|
115
117
|
])
|
|
118
|
+
|
|
119
|
+
const label = ref<HTMLElement | null>(null)
|
|
120
|
+
const isTruncated = ref(false)
|
|
121
|
+
const tooltipText = ref('')
|
|
122
|
+
|
|
123
|
+
/* Обрезку меряем по самому тексту: ширину бейджу задаёт потребитель
|
|
124
|
+
(max-w/сетка), поэтому ResizeObserver ловит ресайз, а mouseenter — смену
|
|
125
|
+
содержимого слота, при которой размер обрезанного span не меняется. */
|
|
126
|
+
function measure() {
|
|
127
|
+
const el = label.value
|
|
128
|
+
if (!el) return
|
|
129
|
+
isTruncated.value = el.scrollWidth > el.clientWidth + 1
|
|
130
|
+
tooltipText.value = el.textContent?.trim() ?? ''
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
useResizeObserver(label, measure)
|
|
134
|
+
onMounted(() => nextTick(measure))
|
|
116
135
|
</script>
|
|
117
136
|
|
|
118
137
|
<template>
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
<a-tooltip-v2
|
|
139
|
+
class="max-w-full min-w-0"
|
|
140
|
+
content-class="max-h-[50vh] max-w-[min(20rem,calc(100vw-2rem))] overflow-hidden text-body-sm whitespace-normal text-primary wrap-anywhere"
|
|
141
|
+
:text="tooltipText"
|
|
142
|
+
:disabled="!isTruncated"
|
|
143
|
+
>
|
|
144
|
+
<div
|
|
145
|
+
v-bind="$attrs"
|
|
146
|
+
:class="[[classes], customClasses]"
|
|
147
|
+
@mouseenter="measure"
|
|
148
|
+
>
|
|
149
|
+
<slot name="point">
|
|
150
|
+
<span v-if="point" :class="pointClasses" />
|
|
151
|
+
</slot>
|
|
152
|
+
<span
|
|
153
|
+
ref="label"
|
|
154
|
+
class="truncate"
|
|
155
|
+
>
|
|
156
|
+
<slot />
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
</a-tooltip-v2>
|
|
125
160
|
</template>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
import { useElementHover } from '@vueuse/core'
|
|
4
|
+
import { computed, ref, watch } from 'vue'
|
|
5
|
+
|
|
6
|
+
export interface StepSliderRowCardStep {
|
|
7
|
+
title: string
|
|
8
|
+
description: string
|
|
9
|
+
icon?: Component
|
|
10
|
+
iconBg?: string
|
|
11
|
+
iconColor?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type RowBreakpoint = 'sm' | 'md' | 'lg'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
steps: StepSliderRowCardStep[]
|
|
18
|
+
interval?: number
|
|
19
|
+
autoplay?: boolean
|
|
20
|
+
pauseOnHover?: boolean
|
|
21
|
+
rowFrom?: RowBreakpoint
|
|
22
|
+
connectors?: boolean
|
|
23
|
+
titleTag?: 'p' | 'h2' | 'h3' | 'h4' | 'h5'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
27
|
+
interval: 5000,
|
|
28
|
+
autoplay: true,
|
|
29
|
+
pauseOnHover: true,
|
|
30
|
+
rowFrom: 'lg',
|
|
31
|
+
connectors: true,
|
|
32
|
+
titleTag: 'p',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const active = defineModel<number>({ default: 0 })
|
|
36
|
+
|
|
37
|
+
const ROW_LAYOUT: Record<RowBreakpoint, { grid: string, lift: string, connector: string }> = {
|
|
38
|
+
sm: { grid: 'sm:grid-cols-none sm:grid-flow-col sm:auto-cols-fr', lift: 'sm:-translate-y-1.5', connector: 'sm:flex' },
|
|
39
|
+
md: { grid: 'md:grid-cols-none md:grid-flow-col md:auto-cols-fr', lift: 'md:-translate-y-1.5', connector: 'md:flex' },
|
|
40
|
+
lg: { grid: 'lg:grid-cols-none lg:grid-flow-col lg:auto-cols-fr', lift: 'lg:-translate-y-1.5', connector: 'lg:flex' },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const root = ref<HTMLElement>()
|
|
44
|
+
const hovered = useElementHover(root)
|
|
45
|
+
|
|
46
|
+
const count = computed(() => props.steps.length)
|
|
47
|
+
const layout = computed(() => ROW_LAYOUT[props.rowFrom])
|
|
48
|
+
const running = computed(() => props.autoplay && count.value > 1)
|
|
49
|
+
const paused = computed(() => props.pauseOnHover && hovered.value)
|
|
50
|
+
|
|
51
|
+
const timingStyle = computed(() => ({
|
|
52
|
+
animationDuration: `${props.interval}ms`,
|
|
53
|
+
animationPlayState: paused.value ? 'paused' : 'running',
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
function goTo(to: number) {
|
|
57
|
+
if (count.value === 0)
|
|
58
|
+
return
|
|
59
|
+
active.value = ((to % count.value) + count.value) % count.value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function next() {
|
|
63
|
+
goTo(active.value + 1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
watch(count, (value) => {
|
|
67
|
+
if (active.value >= value)
|
|
68
|
+
goTo(0)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
defineExpose({ goTo, next })
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<ol
|
|
76
|
+
ref="root"
|
|
77
|
+
class="grid grid-cols-1 items-stretch gap-5"
|
|
78
|
+
:class="layout.grid"
|
|
79
|
+
>
|
|
80
|
+
<li
|
|
81
|
+
v-for="(step, i) in steps"
|
|
82
|
+
:key="i"
|
|
83
|
+
class="ss-row-card relative flex cursor-pointer flex-col rounded-xl border bg-main p-6 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-(--border-color-focus)"
|
|
84
|
+
:class="i === active
|
|
85
|
+
? ['ss-row-card-active border-brand', layout.lift]
|
|
86
|
+
: 'border-default opacity-50'"
|
|
87
|
+
@click="goTo(i)"
|
|
88
|
+
>
|
|
89
|
+
<div class="mb-4 flex items-center justify-between">
|
|
90
|
+
<span
|
|
91
|
+
class="flex size-12 shrink-0 items-center justify-center rounded-xl transition-colors duration-500"
|
|
92
|
+
:class="step.iconBg || (i === active ? 'bg-brand' : 'bg-brand-subtle')"
|
|
93
|
+
>
|
|
94
|
+
<component
|
|
95
|
+
:is="step.icon"
|
|
96
|
+
v-if="step.icon"
|
|
97
|
+
class="size-5.5 transition-colors duration-500"
|
|
98
|
+
:class="step.iconColor || (i === active ? 'text-icon-on-brand' : 'text-icon-brand')"
|
|
99
|
+
/>
|
|
100
|
+
</span>
|
|
101
|
+
|
|
102
|
+
<span
|
|
103
|
+
class="text-display-lg leading-none transition-colors duration-500"
|
|
104
|
+
:class="i === active
|
|
105
|
+
? 'text-brand-200 dark:text-brand-900'
|
|
106
|
+
: 'text-secondary-50 dark:text-secondary-800'"
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
>
|
|
109
|
+
{{ i + 1 }}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<component :is="titleTag" class="mb-1.5 text-h5 text-primary">
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
class="text-left focus-visible:outline-hidden"
|
|
117
|
+
:aria-current="i === active ? 'step' : undefined"
|
|
118
|
+
@click.stop="goTo(i)"
|
|
119
|
+
>
|
|
120
|
+
{{ step.title }}
|
|
121
|
+
</button>
|
|
122
|
+
</component>
|
|
123
|
+
|
|
124
|
+
<p
|
|
125
|
+
class="text-body-md leading-relaxed text-quaternary"
|
|
126
|
+
:class="{ 'mb-4': $slots.visual }"
|
|
127
|
+
>
|
|
128
|
+
{{ step.description }}
|
|
129
|
+
</p>
|
|
130
|
+
|
|
131
|
+
<div
|
|
132
|
+
v-if="$slots.visual"
|
|
133
|
+
class="mt-auto"
|
|
134
|
+
>
|
|
135
|
+
<slot
|
|
136
|
+
name="visual"
|
|
137
|
+
:step="step"
|
|
138
|
+
:index="i"
|
|
139
|
+
:is-active="i === active"
|
|
140
|
+
:active="active"
|
|
141
|
+
:paused="paused"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<span
|
|
146
|
+
v-if="i === active && running"
|
|
147
|
+
class="ss-row-prog pointer-events-none absolute inset-x-4 bottom-0 h-[3px] rounded-full bg-linear-to-r from-brand to-brand-400"
|
|
148
|
+
:style="timingStyle"
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
@animationend="next"
|
|
151
|
+
/>
|
|
152
|
+
|
|
153
|
+
<span
|
|
154
|
+
v-if="connectors && i < steps.length - 1"
|
|
155
|
+
class="pointer-events-none absolute top-1/2 right-[-13px] z-10 hidden size-6 -translate-y-1/2 items-center justify-center rounded-full border bg-main transition-colors duration-500"
|
|
156
|
+
:class="[layout.connector, i < active ? 'border-brand text-brand' : 'border-default text-quaternary']"
|
|
157
|
+
aria-hidden="true"
|
|
158
|
+
>
|
|
159
|
+
<a-icon-arrow-right class="size-3.5" />
|
|
160
|
+
</span>
|
|
161
|
+
</li>
|
|
162
|
+
</ol>
|
|
163
|
+
</template>
|
|
164
|
+
|
|
165
|
+
<style scoped>
|
|
166
|
+
.ss-row-card {
|
|
167
|
+
transition: border-color .5s ease-out, box-shadow .5s ease-out, opacity .5s ease-out, translate .5s ease-out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.ss-row-card-active {
|
|
171
|
+
box-shadow: 0 20px 46px -24px color-mix(in srgb, var(--color-brand-500) 55%, transparent);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.ss-row-prog {
|
|
175
|
+
transform-origin: left;
|
|
176
|
+
animation: ss-row-prog linear forwards;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@keyframes ss-row-prog {
|
|
180
|
+
from {
|
|
181
|
+
transform: scaleX(0);
|
|
182
|
+
}
|
|
183
|
+
to {
|
|
184
|
+
transform: scaleX(1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@media (prefers-reduced-motion: reduce) {
|
|
189
|
+
.ss-row-card {
|
|
190
|
+
transition: none;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.ss-row-prog {
|
|
194
|
+
animation: none;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
@@ -351,7 +351,7 @@ function rowDisplay(item: T): unknown {
|
|
|
351
351
|
:is="startIcon"
|
|
352
352
|
v-if="startIcon"
|
|
353
353
|
class="shrink-0"
|
|
354
|
-
:class="[startIconClass || 'size-4 text-icon-
|
|
354
|
+
:class="[startIconClass || 'size-4 text-icon-secondary', iconOffsetClass]"
|
|
355
355
|
/>
|
|
356
356
|
|
|
357
357
|
<input
|
|
@@ -16,7 +16,6 @@ const otp = ref(['', '', '', '', '', ''])
|
|
|
16
16
|
const otpFormatted = computed(() => {
|
|
17
17
|
return otp.value.join('')
|
|
18
18
|
})
|
|
19
|
-
const showError = ref(false)
|
|
20
19
|
const isLoading = ref(false)
|
|
21
20
|
|
|
22
21
|
async function loginUser() {
|
|
@@ -72,7 +71,7 @@ async function onConfirm() {
|
|
|
72
71
|
confirmSuccessfulModal.value = true
|
|
73
72
|
}
|
|
74
73
|
catch (error) {
|
|
75
|
-
|
|
74
|
+
$toast.error(apiErrorMessage(error, t('error.validation')))
|
|
76
75
|
}
|
|
77
76
|
finally {
|
|
78
77
|
isLoading.value = false
|
|
@@ -81,7 +80,6 @@ async function onConfirm() {
|
|
|
81
80
|
|
|
82
81
|
function resetOtp() {
|
|
83
82
|
otp.value = ['', '', '', '', '', '']
|
|
84
|
-
showError.value = false
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
function onClose() {
|
|
@@ -162,7 +160,6 @@ onMounted(() => {
|
|
|
162
160
|
|
|
163
161
|
<id-otp-input
|
|
164
162
|
v-model="otp"
|
|
165
|
-
v-model:error="showError"
|
|
166
163
|
@on-completed="onConfirm"
|
|
167
164
|
/>
|
|
168
165
|
|
|
@@ -323,9 +323,10 @@ function onForgotPassword() {
|
|
|
323
323
|
block
|
|
324
324
|
:icon="social.icon"
|
|
325
325
|
:data-test-id="`login-form-${social.id}-button`"
|
|
326
|
+
class="max-sm:gap-0"
|
|
326
327
|
@click="authWithSocial(social.id)"
|
|
327
328
|
>
|
|
328
|
-
{{ social.label }}
|
|
329
|
+
<span class="hidden sm:inline">{{ social.label }}</span>
|
|
329
330
|
</a-button-v2>
|
|
330
331
|
</div>
|
|
331
332
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts" setup="">
|
|
2
|
+
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-days">
|
|
8
|
+
<path d="M8 2v3"/>
|
|
9
|
+
<path d="M16 2v3"/>
|
|
10
|
+
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
|
11
|
+
<path d="M3 9h18"/>
|
|
12
|
+
<path d="M8 13h.01"/>
|
|
13
|
+
<path d="M12 13h.01"/>
|
|
14
|
+
<path d="M16 13h.01"/>
|
|
15
|
+
<path d="M8 17h.01"/>
|
|
16
|
+
<path d="M12 17h.01"/>
|
|
17
|
+
<path d="M16 17h.01"/>
|
|
18
|
+
</svg>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<style scoped>
|
|
22
|
+
|
|
23
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts" setup="">
|
|
2
|
+
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder">
|
|
8
|
+
<path
|
|
9
|
+
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>
|
|
10
|
+
</svg>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style scoped>
|
|
14
|
+
|
|
15
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-down">
|
|
3
|
+
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/>
|
|
4
|
+
<path d="M14 2v5a1 1 0 0 0 1 1h5"/>
|
|
5
|
+
<path d="M12 18v-6"/>
|
|
6
|
+
<path d="m9 15 3 3 3-3"/>
|
|
7
|
+
</svg>
|
|
8
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts" setup="">
|
|
2
|
+
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sliders-horizontal">
|
|
8
|
+
<path d="M10 5H3"/>
|
|
9
|
+
<path d="M12 19H3"/>
|
|
10
|
+
<path d="M14 3v4"/>
|
|
11
|
+
<path d="M16 17v4"/>
|
|
12
|
+
<path d="M21 12h-9"/>
|
|
13
|
+
<path d="M21 19h-5"/>
|
|
14
|
+
<path d="M21 5h-7"/>
|
|
15
|
+
<path d="M8 10v4"/>
|
|
16
|
+
<path d="M8 12H3"/>
|
|
17
|
+
</svg>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<style scoped>
|
|
21
|
+
|
|
22
|
+
</style>
|
package/icons/funnel.vue
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-funnel">
|
|
3
|
+
<path d="M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z"/>
|
|
4
|
+
</svg>
|
|
5
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts" setup="">
|
|
2
|
+
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-qr-code">
|
|
8
|
+
<rect width="5" height="5" x="3" y="3" rx="1"/>
|
|
9
|
+
<rect width="5" height="5" x="16" y="3" rx="1"/>
|
|
10
|
+
<rect width="5" height="5" x="3" y="16" rx="1"/>
|
|
11
|
+
<path d="M21 16h-3a2 2 0 0 0-2 2v3"/>
|
|
12
|
+
<path d="M21 21v.01"/>
|
|
13
|
+
<path d="M12 7v3a2 2 0 0 1-2 2H7"/>
|
|
14
|
+
<path d="M3 12h.01"/>
|
|
15
|
+
<path d="M12 3h.01"/>
|
|
16
|
+
<path d="M12 16v.01"/>
|
|
17
|
+
<path d="M16 12h1"/>
|
|
18
|
+
<path d="M21 12v.01"/>
|
|
19
|
+
<path d="M12 21v-1"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<style scoped>
|
|
24
|
+
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-recycle">
|
|
3
|
+
<path d="M7 19H4.815a1.83 1.83 0 0 1-1.57-.881 1.785 1.785 0 0 1-.004-1.784L7.196 9.5"/>
|
|
4
|
+
<path d="M11 19h8.203a1.83 1.83 0 0 0 1.556-.89 1.784 1.784 0 0 0 0-1.775l-1.226-2.12"/>
|
|
5
|
+
<path d="m14 16-3 3 3 3"/>
|
|
6
|
+
<path d="M8.293 13.596 7.196 9.5 3.1 10.598"/>
|
|
7
|
+
<path d="m9.344 5.811 1.093-1.892A1.83 1.83 0 0 1 11.985 3a1.784 1.784 0 0 1 1.546.888l3.943 6.843"/>
|
|
8
|
+
<path d="m13.378 9.633 4.096 1.098 1.097-4.096"/>
|
|
9
|
+
</svg>
|
|
10
|
+
</template>
|
package/icons/reply.vue
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts" setup="">
|
|
2
|
+
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-reply">
|
|
8
|
+
<path d="M20 18v-2a4 4 0 0 0-4-4H4"/>
|
|
9
|
+
<path d="m9 17-5-5 5-5"/>
|
|
10
|
+
</svg>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style scoped>
|
|
14
|
+
|
|
15
|
+
</style>
|
package/icons/target.vue
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-target">
|
|
3
|
+
<circle cx="12" cy="12" r="10"/>
|
|
4
|
+
<circle cx="12" cy="12" r="6"/>
|
|
5
|
+
<circle cx="12" cy="12" r="2"/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
package/icons/upload.vue
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts" setup="">
|
|
2
|
+
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-upload">
|
|
8
|
+
<path d="M12 3v12"/>
|
|
9
|
+
<path d="m17 8-5-5-5 5"/>
|
|
10
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
11
|
+
</svg>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<style scoped>
|
|
15
|
+
|
|
16
|
+
</style>
|
package/icons/user-x.vue
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts" setup="">
|
|
2
|
+
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-x"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="17" x2="22" y1="8" y2="13"/><line x1="22" x2="17" y1="8" y2="13"/></svg>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<style scoped>
|
|
10
|
+
|
|
11
|
+
</style>
|