adata-ui 2.1.41-beta.5 → 2.1.41-beta.7
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/CLAUDE.md +1 -1
- package/assets/styles/theme.css +11 -4
- 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/overlays/drawer/ADrawerV2.vue +9 -2
- package/components/overlays/modal/AModalV2.vue +1 -0
- package/icons/arrow/arrow-down-left.vue +6 -0
- package/icons/arrow/arrow-up-right.vue +6 -0
- package/icons/crown.vue +6 -0
- package/icons/refresh-cw.vue +8 -0
- package/icons/user-plus.vue +3 -0
- package/illustrations/scheme-notes.vue +21 -0
- package/illustrations/scheme-waypoints.vue +25 -0
- 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>
|
|
@@ -14,7 +14,10 @@ const props = withDefaults(defineProps<{
|
|
|
14
14
|
swipeToClose?: boolean
|
|
15
15
|
swipeThreshold?: number
|
|
16
16
|
showDragHandle?: boolean
|
|
17
|
+
/** Фиксированная общая высота листа («70dvh»). По умолчанию высота по контенту. */
|
|
18
|
+
height?: string
|
|
17
19
|
maxHeight?: string
|
|
20
|
+
zIndex?: number | string
|
|
18
21
|
}>(), {
|
|
19
22
|
closeOnOverlay: true,
|
|
20
23
|
closeOnEsc: true,
|
|
@@ -26,7 +29,9 @@ const props = withDefaults(defineProps<{
|
|
|
26
29
|
swipeToClose: true,
|
|
27
30
|
swipeThreshold: 65,
|
|
28
31
|
showDragHandle: true,
|
|
32
|
+
height: undefined,
|
|
29
33
|
maxHeight: '100dvh',
|
|
34
|
+
zIndex: 10000,
|
|
30
35
|
})
|
|
31
36
|
|
|
32
37
|
const emit = defineEmits<{
|
|
@@ -54,12 +59,13 @@ const blurClass = computed(() => {
|
|
|
54
59
|
})
|
|
55
60
|
|
|
56
61
|
const overlayClass = computed(() => {
|
|
57
|
-
const base = ['drawer-v2-overlay fixed inset-0
|
|
62
|
+
const base = ['drawer-v2-overlay fixed inset-0 flex items-end justify-stretch bg-overlay']
|
|
58
63
|
if (blurClass.value) base.push(blurClass.value)
|
|
59
64
|
return base.join(' ')
|
|
60
65
|
})
|
|
61
66
|
|
|
62
|
-
const
|
|
67
|
+
const overlayStyle = computed(() => ({ zIndex: props.zIndex }))
|
|
68
|
+
const contentStyle = computed(() => ({ height: props.height, maxHeight: props.maxHeight }))
|
|
63
69
|
|
|
64
70
|
const overlayTransitionName = computed(() => {
|
|
65
71
|
if (swipeClosing.value) return ''
|
|
@@ -226,6 +232,7 @@ function onTouchEnd() {
|
|
|
226
232
|
v-if="open"
|
|
227
233
|
ref="overlayRef"
|
|
228
234
|
:class="overlayClass"
|
|
235
|
+
:style="overlayStyle"
|
|
229
236
|
@mousedown="onOverlayClick"
|
|
230
237
|
>
|
|
231
238
|
<transition :name="contentTransitionName" appear>
|
|
@@ -0,0 +1,6 @@
|
|
|
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-arrow-down-left-icon lucide-arrow-down-left">
|
|
3
|
+
<path d="M17 7 7 17"/>
|
|
4
|
+
<path d="M17 17H7V7"/>
|
|
5
|
+
</svg>
|
|
6
|
+
</template>
|
|
@@ -0,0 +1,6 @@
|
|
|
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-arrow-up-right-icon lucide-arrow-up-right">
|
|
3
|
+
<path d="M7 7h10v10"/>
|
|
4
|
+
<path d="M7 17 17 7"/>
|
|
5
|
+
</svg>
|
|
6
|
+
</template>
|
package/icons/crown.vue
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
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-crown-icon lucide-crown">
|
|
3
|
+
<path d="M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z"/>
|
|
4
|
+
<path d="M5 21h14"/>
|
|
5
|
+
</svg>
|
|
6
|
+
</template>
|
|
@@ -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-refresh-cw-icon lucide-refresh-cw">
|
|
3
|
+
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
|
|
4
|
+
<path d="M21 3v5h-5"/>
|
|
5
|
+
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
|
|
6
|
+
<path d="M8 16H3v5"/>
|
|
7
|
+
</svg>
|
|
8
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
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-user-plus-icon lucide-user-plus"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M19 8v6"/><path d="M22 11h-6"/><circle cx="9" cy="7" r="4"/></svg>
|
|
3
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<template>
|
|
5
|
+
<svg
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
width="24"
|
|
8
|
+
height="24"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
stroke-width="2"
|
|
13
|
+
stroke-linecap="round"
|
|
14
|
+
stroke-linejoin="round"
|
|
15
|
+
class="lucide lucide-info-icon lucide-info"
|
|
16
|
+
>
|
|
17
|
+
<circle cx="12" cy="12" r="10"
|
|
18
|
+
/><path d="M12 16v-4" />
|
|
19
|
+
<path d="M12 8h.01"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<template>
|
|
5
|
+
<svg
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
width="24"
|
|
8
|
+
height="24"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
stroke-width="2"
|
|
13
|
+
stroke-linecap="round"
|
|
14
|
+
stroke-linejoin="round"
|
|
15
|
+
class="lucide lucide-waypoints-icon lucide-waypoints"
|
|
16
|
+
>
|
|
17
|
+
<path d="m10.586 5.414-5.172 5.172" />
|
|
18
|
+
<path d="m18.586 13.414-5.172 5.172" />
|
|
19
|
+
<path d="M6 12h12" />
|
|
20
|
+
<circle cx="12" cy="20" r="2" />
|
|
21
|
+
<circle cx="12" cy="4" r="2" />
|
|
22
|
+
<circle cx="20" cy="12" r="2" />
|
|
23
|
+
<circle cx="4" cy="12" r="2" />
|
|
24
|
+
</svg>
|
|
25
|
+
</template>
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Нормализует размер из props в CSS-значение: число трактуем как пиксели,
|
|
3
|
+
* строку отдаём как есть (`70dvh`, `min(320px, 50vw)`), пустое — как «не задано».
|
|
4
|
+
*
|
|
5
|
+
* Общая для селектов и дроверов: и `ASelectV2`, и `ACascadeSelectV2` принимают
|
|
6
|
+
* размеры выпадашки в виде `number | string`.
|
|
7
|
+
*/
|
|
8
|
+
export function toCssSize(value?: number | string | null): string | undefined {
|
|
9
|
+
if (value == null || value === '') return undefined
|
|
10
|
+
return typeof value === 'number' ? `${value}px` : value
|
|
11
|
+
}
|