adata-ui 2.1.41-beta.21 → 2.1.41-beta.22
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/.playground/app.vue +51 -0
- package/.playground/nuxt.config.ts +3 -0
- package/components/elements/calendar-v2/ACalendarV2.vue +1150 -0
- package/components/elements/calendar-v2/ADatePickerV2.vue +508 -0
- package/components/elements/calendar-v2/README.md +127 -0
- package/components/elements/calendar-v2/date-utils.ts +217 -0
- package/components/elements/calendar-v2/presets.ts +58 -0
- package/components/elements/calendar-v2/types.ts +84 -0
- package/components/elements/select/ASelectV2.vue +32 -8
- package/components/elements/select/ui/select-trigger.vue +53 -10
- package/icons/download-2.vue +16 -0
- package/icons/download-rounded.vue +5 -4
- package/icons/play.vue +14 -0
- package/lang/en.ts +26 -3
- package/lang/kk.ts +26 -3
- package/lang/ru.ts +26 -3
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { DateRange, DisabledDates } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Чистые date-хелперы календаря — без Vue, чтобы их можно было покрыть тестами
|
|
5
|
+
* и переиспользовать в потребителях.
|
|
6
|
+
*
|
|
7
|
+
* Все даты нормализуются к началу суток в ЛОКАЛЬНОЙ зоне: календарь показывает
|
|
8
|
+
* тот же день, который пользователь видит на часах. Сериализация в ISO делается
|
|
9
|
+
* вручную (`toISODate`), а не через `toISOString`, — иначе UTC-сдвиг съезжает на
|
|
10
|
+
* сутки для зон восточнее Гринвича (Алматы — UTC+5).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const MS_IN_DAY = 86_400_000
|
|
14
|
+
|
|
15
|
+
/** Воскресенье — чтобы строить названия дней недели в любом порядке. */
|
|
16
|
+
const WEEKDAY_REFERENCE = new Date(2024, 0, 7)
|
|
17
|
+
|
|
18
|
+
export function startOfDay(date: Date): Date {
|
|
19
|
+
const next = new Date(date)
|
|
20
|
+
next.setHours(0, 0, 0, 0)
|
|
21
|
+
return next
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isValidDate(value: unknown): value is Date {
|
|
25
|
+
return value instanceof Date && !Number.isNaN(value.getTime())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Приводит `Date | string | number | null` к нормализованной дате или null. */
|
|
29
|
+
export function toDate(value: Date | string | number | null | undefined): Date | null {
|
|
30
|
+
if (value == null || value === '') return null
|
|
31
|
+
const date = value instanceof Date ? new Date(value) : new Date(value)
|
|
32
|
+
return isValidDate(date) ? date : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isSameDay(a: Date | null, b: Date | null): boolean {
|
|
36
|
+
if (!a || !b) return false
|
|
37
|
+
return a.getFullYear() === b.getFullYear()
|
|
38
|
+
&& a.getMonth() === b.getMonth()
|
|
39
|
+
&& a.getDate() === b.getDate()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isSameMonth(a: Date | null, b: Date | null): boolean {
|
|
43
|
+
if (!a || !b) return false
|
|
44
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function addDays(date: Date, amount: number): Date {
|
|
48
|
+
const next = new Date(date)
|
|
49
|
+
next.setDate(next.getDate() + amount)
|
|
50
|
+
return next
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Прибавляет месяцы, зажимая число: 31 января + 1 месяц = 28/29 февраля,
|
|
55
|
+
* а не 2/3 марта (как получилось бы при наивном setMonth).
|
|
56
|
+
*/
|
|
57
|
+
export function addMonths(date: Date, amount: number): Date {
|
|
58
|
+
const next = new Date(date)
|
|
59
|
+
const day = next.getDate()
|
|
60
|
+
next.setDate(1)
|
|
61
|
+
next.setMonth(next.getMonth() + amount)
|
|
62
|
+
next.setDate(Math.min(day, daysInMonth(next.getFullYear(), next.getMonth())))
|
|
63
|
+
return next
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function addYears(date: Date, amount: number): Date {
|
|
67
|
+
return addMonths(date, amount * 12)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function daysInMonth(year: number, month: number): number {
|
|
71
|
+
return new Date(year, month + 1, 0).getDate()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function startOfMonth(date: Date): Date {
|
|
75
|
+
return new Date(date.getFullYear(), date.getMonth(), 1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function endOfMonth(date: Date): Date {
|
|
79
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function startOfYear(date: Date): Date {
|
|
83
|
+
return new Date(date.getFullYear(), 0, 1)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function endOfYear(date: Date): Date {
|
|
87
|
+
return new Date(date.getFullYear(), 11, 31)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function startOfWeek(date: Date, firstDayOfWeek: number): Date {
|
|
91
|
+
const shift = (date.getDay() - firstDayOfWeek + 7) % 7
|
|
92
|
+
return startOfDay(addDays(date, -shift))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function compareDays(a: Date, b: Date): number {
|
|
96
|
+
return startOfDay(a).getTime() - startOfDay(b).getTime()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isBeforeDay(a: Date, b: Date): boolean {
|
|
100
|
+
return compareDays(a, b) < 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isAfterDay(a: Date, b: Date): boolean {
|
|
104
|
+
return compareDays(a, b) > 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isWithinDays(date: Date, start: Date | null, end: Date | null): boolean {
|
|
108
|
+
if (!start || !end) return false
|
|
109
|
+
const from = compareDays(start, end) <= 0 ? start : end
|
|
110
|
+
const to = compareDays(start, end) <= 0 ? end : start
|
|
111
|
+
return !isBeforeDay(date, from) && !isAfterDay(date, to)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function clampDate(date: Date, min: Date | null, max: Date | null): Date {
|
|
115
|
+
if (min && isBeforeDay(date, min)) return startOfDay(min)
|
|
116
|
+
if (max && isAfterDay(date, max)) return startOfDay(max)
|
|
117
|
+
return startOfDay(date)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Range всегда отдаётся наружу упорядоченным, как бы его ни кликали. */
|
|
121
|
+
export function normalizeRange(range: DateRange): DateRange {
|
|
122
|
+
const { start, end } = range
|
|
123
|
+
if (start && end && isAfterDay(start, end)) {
|
|
124
|
+
return { start: end, end: start }
|
|
125
|
+
}
|
|
126
|
+
return range
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isDateDisabled(
|
|
130
|
+
date: Date,
|
|
131
|
+
options: { min?: Date | null, max?: Date | null, disabledDates?: DisabledDates },
|
|
132
|
+
): boolean {
|
|
133
|
+
const { min, max, disabledDates } = options
|
|
134
|
+
|
|
135
|
+
if (min && isBeforeDay(date, min)) return true
|
|
136
|
+
if (max && isAfterDay(date, max)) return true
|
|
137
|
+
|
|
138
|
+
if (typeof disabledDates === 'function') return disabledDates(date)
|
|
139
|
+
if (Array.isArray(disabledDates)) return disabledDates.some(item => isSameDay(item, date))
|
|
140
|
+
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Матрица месяца 6×7. Всегда шесть строк — иначе панель прыгает по высоте
|
|
146
|
+
* при переключении месяцев.
|
|
147
|
+
*/
|
|
148
|
+
export function buildMonthMatrix(year: number, month: number, firstDayOfWeek: number): Date[][] {
|
|
149
|
+
const first = startOfWeek(new Date(year, month, 1), firstDayOfWeek)
|
|
150
|
+
const weeks: Date[][] = []
|
|
151
|
+
|
|
152
|
+
for (let week = 0; week < 6; week++) {
|
|
153
|
+
const days: Date[] = []
|
|
154
|
+
for (let day = 0; day < 7; day++) {
|
|
155
|
+
days.push(addDays(first, week * 7 + day))
|
|
156
|
+
}
|
|
157
|
+
weeks.push(days)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return weeks
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Номер ISO-недели — для колонки с номерами недель. */
|
|
164
|
+
export function getISOWeek(date: Date): number {
|
|
165
|
+
const target = startOfDay(date)
|
|
166
|
+
// Четверг той же недели определяет год недели по ISO 8601.
|
|
167
|
+
target.setDate(target.getDate() + 3 - ((target.getDay() + 6) % 7))
|
|
168
|
+
const firstThursday = new Date(target.getFullYear(), 0, 4)
|
|
169
|
+
firstThursday.setDate(firstThursday.getDate() + 3 - ((firstThursday.getDay() + 6) % 7))
|
|
170
|
+
return 1 + Math.round((target.getTime() - firstThursday.getTime()) / (7 * MS_IN_DAY))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getWeekdayNames(locale: string, firstDayOfWeek: number, format: 'short' | 'narrow' = 'short'): string[] {
|
|
174
|
+
const formatter = new Intl.DateTimeFormat(locale, { weekday: format })
|
|
175
|
+
return Array.from({ length: 7 }, (_, index) =>
|
|
176
|
+
formatter.format(addDays(WEEKDAY_REFERENCE, (firstDayOfWeek + index) % 7)))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function getMonthNames(locale: string, format: 'long' | 'short' = 'long'): string[] {
|
|
180
|
+
const formatter = new Intl.DateTimeFormat(locale, { month: format })
|
|
181
|
+
return Array.from({ length: 12 }, (_, month) => formatter.format(new Date(2024, month, 1)))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function formatDate(date: Date | null, locale: string, options?: Intl.DateTimeFormatOptions): string {
|
|
185
|
+
if (!date) return ''
|
|
186
|
+
return new Intl.DateTimeFormat(locale, options ?? { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Локальная дата в ГГГГ-ММ-ДД — формат, который обычно ждут бэкенды. */
|
|
190
|
+
export function toISODate(date: Date | null): string {
|
|
191
|
+
if (!date) return ''
|
|
192
|
+
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
193
|
+
const day = String(date.getDate()).padStart(2, '0')
|
|
194
|
+
return `${date.getFullYear()}-${month}-${day}`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 'дд.мм.гггг' → Date; возвращает null, если такой даты не существует. */
|
|
198
|
+
export function parseDisplayDate(value: string): Date | null {
|
|
199
|
+
const match = /^\s*(\d{1,2})[.\-/](\d{1,2})[.\-/](\d{4})\s*$/.exec(value ?? '')
|
|
200
|
+
if (!match) return null
|
|
201
|
+
|
|
202
|
+
const day = Number(match[1])
|
|
203
|
+
const month = Number(match[2])
|
|
204
|
+
const year = Number(match[3])
|
|
205
|
+
const date = new Date(year, month - 1, day)
|
|
206
|
+
|
|
207
|
+
const isReal = date.getFullYear() === year
|
|
208
|
+
&& date.getMonth() === month - 1
|
|
209
|
+
&& date.getDate() === day
|
|
210
|
+
|
|
211
|
+
return isReal ? startOfDay(date) : null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Год, с которого начинается страница сетки лет (по 12 лет на страницу). */
|
|
215
|
+
export function startOfYearPage(year: number, pageSize = 12): number {
|
|
216
|
+
return Math.floor(year / pageSize) * pageSize
|
|
217
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CalendarPreset } from './types'
|
|
2
|
+
import { addDays, addMonths, endOfMonth, endOfYear, startOfDay, startOfMonth, startOfYear } from './date-utils'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Готовый набор пресетов для режима `range`.
|
|
6
|
+
*
|
|
7
|
+
* `label` — ключи i18n; ACalendarV2 переводит их сам (`te` → `t`), а если ключа
|
|
8
|
+
* нет, покажет строку как есть. Набор можно фильтровать или дополнять:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const presets = getRangePresets().filter(p => p.key !== 'year')
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function getRangePresets(): CalendarPreset[] {
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
key: 'today',
|
|
18
|
+
label: 'calendar.presets.today',
|
|
19
|
+
value: () => ({ start: startOfDay(new Date()), end: startOfDay(new Date()) }),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'yesterday',
|
|
23
|
+
label: 'calendar.presets.yesterday',
|
|
24
|
+
value: () => {
|
|
25
|
+
const day = addDays(startOfDay(new Date()), -1)
|
|
26
|
+
return { start: day, end: day }
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'last7',
|
|
31
|
+
label: 'calendar.presets.last7',
|
|
32
|
+
value: () => ({ start: addDays(startOfDay(new Date()), -6), end: startOfDay(new Date()) }),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'last30',
|
|
36
|
+
label: 'calendar.presets.last30',
|
|
37
|
+
value: () => ({ start: addDays(startOfDay(new Date()), -29), end: startOfDay(new Date()) }),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'thisMonth',
|
|
41
|
+
label: 'calendar.presets.thisMonth',
|
|
42
|
+
value: () => ({ start: startOfMonth(new Date()), end: endOfMonth(new Date()) }),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'prevMonth',
|
|
46
|
+
label: 'calendar.presets.prevMonth',
|
|
47
|
+
value: () => {
|
|
48
|
+
const prev = addMonths(new Date(), -1)
|
|
49
|
+
return { start: startOfMonth(prev), end: endOfMonth(prev) }
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: 'thisYear',
|
|
54
|
+
label: 'calendar.presets.thisYear',
|
|
55
|
+
value: () => ({ start: startOfYear(new Date()), end: endOfYear(new Date()) }),
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Что выбирает календарь. */
|
|
2
|
+
export type CalendarMode = 'single' | 'range' | 'multiple'
|
|
3
|
+
|
|
4
|
+
/** Точность выбора: день, месяц целиком или год целиком. */
|
|
5
|
+
export type CalendarPrecision = 'day' | 'month' | 'year'
|
|
6
|
+
|
|
7
|
+
/** Какая сетка показана сейчас (внутреннее состояние навигации по заголовку). */
|
|
8
|
+
export type CalendarPanel = 'days' | 'months' | 'years'
|
|
9
|
+
|
|
10
|
+
export type CalendarSize = 'sm' | 'md'
|
|
11
|
+
|
|
12
|
+
export interface DateRange {
|
|
13
|
+
start: Date | null
|
|
14
|
+
end: Date | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CalendarModelValue = Date | DateRange | Date[] | null
|
|
18
|
+
|
|
19
|
+
/** Быстрый выбор: «Последние 7 дней», «Этот месяц» и т.п. */
|
|
20
|
+
export interface CalendarPreset {
|
|
21
|
+
key: string
|
|
22
|
+
/** Готовая строка либо ключ i18n — компонент переводит, если ключ существует. */
|
|
23
|
+
label: string
|
|
24
|
+
value: () => Date | DateRange | Date[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type DisabledDates = Date[] | ((date: Date) => boolean)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Пропсы сетки, общие для ACalendarV2 и ADatePickerV2: пикер принимает их
|
|
31
|
+
* и прокидывает в календарь как есть. Новый проп сетки добавляется здесь один
|
|
32
|
+
* раз и появляется у обоих компонентов — останется только пробросить его
|
|
33
|
+
* в шаблоне пикера.
|
|
34
|
+
*/
|
|
35
|
+
export interface CalendarBaseProps {
|
|
36
|
+
/** single — одна дата, range — период, multiple — произвольный набор дат. */
|
|
37
|
+
mode?: CalendarMode
|
|
38
|
+
/** Точность выбора. month/year открывают сразу соответствующую сетку. */
|
|
39
|
+
precision?: CalendarPrecision
|
|
40
|
+
/** BCP-47. По умолчанию — текущая локаль приложения. */
|
|
41
|
+
locale?: string
|
|
42
|
+
min?: Date | string | number | null
|
|
43
|
+
max?: Date | string | number | null
|
|
44
|
+
/** Массив запрещённых дат либо предикат. */
|
|
45
|
+
disabledDates?: DisabledDates
|
|
46
|
+
/** 1 — понедельник (по умолчанию), 0 — воскресенье. */
|
|
47
|
+
firstDayOfWeek?: number
|
|
48
|
+
/** Сколько месяцев показывать рядом (1–2). Для периода удобны два. */
|
|
49
|
+
months?: number
|
|
50
|
+
/** Быстрый выбор сбоку. Готовый набор — `getRangePresets()` из ./presets. */
|
|
51
|
+
presets?: CalendarPreset[]
|
|
52
|
+
weekNumbers?: boolean
|
|
53
|
+
/** Выбор времени. Работает для single и range (по одному контролу на конец). */
|
|
54
|
+
showTime?: boolean
|
|
55
|
+
minuteStep?: number
|
|
56
|
+
size?: CalendarSize
|
|
57
|
+
highlightWeekends?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CalendarDayCell {
|
|
61
|
+
date: Date
|
|
62
|
+
/** Стабильный ключ для v-for. */
|
|
63
|
+
key: string
|
|
64
|
+
label: number
|
|
65
|
+
isOutside: boolean
|
|
66
|
+
isToday: boolean
|
|
67
|
+
isSelected: boolean
|
|
68
|
+
isRangeStart: boolean
|
|
69
|
+
isRangeEnd: boolean
|
|
70
|
+
isInRange: boolean
|
|
71
|
+
isDisabled: boolean
|
|
72
|
+
isWeekend: boolean
|
|
73
|
+
isFocused: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CalendarUnitCell {
|
|
77
|
+
/** Номер месяца (0–11) или год. */
|
|
78
|
+
value: number
|
|
79
|
+
label: string
|
|
80
|
+
isSelected: boolean
|
|
81
|
+
isCurrent: boolean
|
|
82
|
+
isDisabled: boolean
|
|
83
|
+
isInRange: boolean
|
|
84
|
+
}
|
|
@@ -9,6 +9,8 @@ import { isEqual } from 'lodash-es'
|
|
|
9
9
|
interface Props {
|
|
10
10
|
label: string
|
|
11
11
|
required?: boolean
|
|
12
|
+
inlineLabel?: boolean
|
|
13
|
+
autoWidth?: boolean
|
|
12
14
|
options: T[]
|
|
13
15
|
hideOverflow?: boolean
|
|
14
16
|
keyToShow?: string
|
|
@@ -17,7 +19,7 @@ interface Props {
|
|
|
17
19
|
clearable?: boolean
|
|
18
20
|
hasButtons?: boolean
|
|
19
21
|
disabled?: boolean
|
|
20
|
-
size?: 'sm' | 'md'
|
|
22
|
+
size?: 'xs' | 'sm' | 'md'
|
|
21
23
|
error?: string | string[]
|
|
22
24
|
startIcon?: object
|
|
23
25
|
searchable?: boolean
|
|
@@ -46,6 +48,8 @@ interface Emits {
|
|
|
46
48
|
|
|
47
49
|
const props = withDefaults(defineProps<Props>(), {
|
|
48
50
|
required: false,
|
|
51
|
+
inlineLabel: false,
|
|
52
|
+
autoWidth: false,
|
|
49
53
|
hideOverflow: false,
|
|
50
54
|
getOnlyKey: false,
|
|
51
55
|
keyToShow: 'name',
|
|
@@ -133,10 +137,24 @@ const { x, y, strategy, update } = useFloating(positionReference, floating, {
|
|
|
133
137
|
flip({ fallbackPlacements: dropdownFallback.value }),
|
|
134
138
|
shift({ padding: 8 }),
|
|
135
139
|
floatingSize({
|
|
136
|
-
apply({ availableHeight, elements, rects }) {
|
|
140
|
+
apply({ availableHeight, availableWidth, elements, rects }) {
|
|
137
141
|
const el = elements.floating as HTMLElement
|
|
138
142
|
const widthOverride = toCssSize(props.dropdownWidth)
|
|
139
|
-
|
|
143
|
+
if (widthOverride) {
|
|
144
|
+
el.style.width = widthOverride
|
|
145
|
+
el.style.minWidth = ''
|
|
146
|
+
el.style.maxWidth = ''
|
|
147
|
+
}
|
|
148
|
+
else if (props.autoWidth) {
|
|
149
|
+
el.style.width = 'max-content'
|
|
150
|
+
el.style.minWidth = `${rects.reference.width}px`
|
|
151
|
+
el.style.maxWidth = `${availableWidth}px`
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
el.style.width = `${rects.reference.width}px`
|
|
155
|
+
el.style.minWidth = ''
|
|
156
|
+
el.style.maxWidth = ''
|
|
157
|
+
}
|
|
140
158
|
const maxHeightOverride = toCssSize(props.dropdownMaxHeight)
|
|
141
159
|
el.style.maxHeight = maxHeightOverride
|
|
142
160
|
? `min(${maxHeightOverride}, ${availableHeight}px)`
|
|
@@ -276,7 +294,7 @@ const { visibleCount: visibleChipCount, hiddenCount: hiddenChipCount } = useChip
|
|
|
276
294
|
gap: 6,
|
|
277
295
|
})
|
|
278
296
|
|
|
279
|
-
const showFloatingLabel = computed(() => !!props.label && !props.multiple)
|
|
297
|
+
const showFloatingLabel = computed(() => !!props.label && !props.multiple && !props.inlineLabel)
|
|
280
298
|
|
|
281
299
|
const isDefaultSelection = computed(() => {
|
|
282
300
|
if (props.multiple) {
|
|
@@ -287,7 +305,11 @@ const isDefaultSelection = computed(() => {
|
|
|
287
305
|
</script>
|
|
288
306
|
|
|
289
307
|
<template>
|
|
290
|
-
<div
|
|
308
|
+
<div
|
|
309
|
+
ref="wrapper"
|
|
310
|
+
:class="autoWidth ? 'w-auto' : 'w-full'"
|
|
311
|
+
class="select-v2 relative text-body-md"
|
|
312
|
+
>
|
|
291
313
|
<a-select-trigger
|
|
292
314
|
ref="triggerRef"
|
|
293
315
|
:label="label"
|
|
@@ -301,6 +323,8 @@ const isDefaultSelection = computed(() => {
|
|
|
301
323
|
:clearable="clearable"
|
|
302
324
|
:show-clear="!!valueToShow && clearable && !isDefaultSelection"
|
|
303
325
|
:floating-label="showFloatingLabel"
|
|
326
|
+
:inline-label="inlineLabel"
|
|
327
|
+
:auto-width="autoWidth"
|
|
304
328
|
:has-value="!!valueToShow"
|
|
305
329
|
:data-test-id="dataTestId"
|
|
306
330
|
@toggle="toggleOpen()"
|
|
@@ -361,7 +385,7 @@ const isDefaultSelection = computed(() => {
|
|
|
361
385
|
</span>
|
|
362
386
|
</span>
|
|
363
387
|
</template>
|
|
364
|
-
<span v-else class="text-secondary">{{ label }}</span>
|
|
388
|
+
<span v-else-if="!inlineLabel" class="text-secondary">{{ label }}</span>
|
|
365
389
|
</template>
|
|
366
390
|
</a-select-trigger>
|
|
367
391
|
|
|
@@ -389,7 +413,7 @@ const isDefaultSelection = computed(() => {
|
|
|
389
413
|
<a-input-standard
|
|
390
414
|
v-model="search"
|
|
391
415
|
:label="searchLabel"
|
|
392
|
-
:size="size"
|
|
416
|
+
:size="size === 'xs' ? 'sm' : size"
|
|
393
417
|
clearable
|
|
394
418
|
color="gray"
|
|
395
419
|
/>
|
|
@@ -399,7 +423,7 @@ const isDefaultSelection = computed(() => {
|
|
|
399
423
|
<a-input-standard
|
|
400
424
|
v-model="searchValue"
|
|
401
425
|
:label="searchLabel"
|
|
402
|
-
:size="size"
|
|
426
|
+
:size="size === 'xs' ? 'sm' : size"
|
|
403
427
|
clearable
|
|
404
428
|
color="gray"
|
|
405
429
|
/>
|
|
@@ -12,7 +12,7 @@ defineOptions({ name: 'SelectTrigger' })
|
|
|
12
12
|
const props = withDefaults(defineProps<{
|
|
13
13
|
label?: string
|
|
14
14
|
required?: boolean
|
|
15
|
-
size?: 'sm' | 'md'
|
|
15
|
+
size?: 'xs' | 'sm' | 'md'
|
|
16
16
|
disabled?: boolean
|
|
17
17
|
error?: string | string[]
|
|
18
18
|
isOpen?: boolean
|
|
@@ -26,6 +26,13 @@ const props = withDefaults(defineProps<{
|
|
|
26
26
|
floatingLabel?: boolean
|
|
27
27
|
/** Значение выбрано — лейбл поднят, иначе стоит на месте плейсхолдера. */
|
|
28
28
|
hasValue?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Лейбл уезжает в строку значения — «Лейбл: значение» одной строкой вместо
|
|
31
|
+
* floating label над ним. Двоеточие рисуется только когда значение выбрано.
|
|
32
|
+
*/
|
|
33
|
+
inlineLabel?: boolean
|
|
34
|
+
/** Кнопка сжимается по содержимому вместо w-full. */
|
|
35
|
+
autoWidth?: boolean
|
|
29
36
|
}>(), {
|
|
30
37
|
label: '',
|
|
31
38
|
required: false,
|
|
@@ -39,6 +46,8 @@ const props = withDefaults(defineProps<{
|
|
|
39
46
|
showClear: false,
|
|
40
47
|
floatingLabel: false,
|
|
41
48
|
hasValue: false,
|
|
49
|
+
inlineLabel: false,
|
|
50
|
+
autoWidth: false,
|
|
42
51
|
})
|
|
43
52
|
|
|
44
53
|
const emits = defineEmits<{
|
|
@@ -49,14 +58,30 @@ const emits = defineEmits<{
|
|
|
49
58
|
const el = ref<HTMLButtonElement | null>(null)
|
|
50
59
|
const valueEl = ref<HTMLElement | null>(null)
|
|
51
60
|
|
|
52
|
-
/* Без floating label кнопка центрирует содержимое, с ним — прижимает значение к низу. */
|
|
53
61
|
const layoutClass = computed(() => {
|
|
54
|
-
if (
|
|
62
|
+
if (props.size === 'xs') return 'h-8 py-1'
|
|
63
|
+
if (props.inlineLabel || !props.floatingLabel) {
|
|
55
64
|
return props.size === 'md' ? 'h-10 py-1.5' : 'h-9 py-1.5'
|
|
56
65
|
}
|
|
57
66
|
return props.size === 'md' ? 'h-10 pt-3 pb-0.5' : 'h-9 pt-3.5 pb-0.5'
|
|
58
67
|
})
|
|
59
68
|
|
|
69
|
+
const paddingClass = computed(() => {
|
|
70
|
+
if (props.size === 'xs') return props.clearable ? 'pl-3 pr-12' : 'pl-3 pr-8'
|
|
71
|
+
return props.clearable ? 'pl-4 pr-16' : 'pl-4 pr-9'
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const chevronPosition = computed(() => (props.size === 'xs' ? 'right-3' : 'right-3'))
|
|
75
|
+
const clearPosition = computed(() => (props.size === 'xs' ? 'right-9' : 'right-9'))
|
|
76
|
+
|
|
77
|
+
const labelMode = computed<'inline' | 'floating' | 'placeholder' | 'none'>(() => {
|
|
78
|
+
if (!props.label) return 'none'
|
|
79
|
+
if (props.inlineLabel) return 'inline'
|
|
80
|
+
if (!props.floatingLabel) return 'none'
|
|
81
|
+
if (props.size === 'xs') return props.hasValue ? 'none' : 'placeholder'
|
|
82
|
+
return 'floating'
|
|
83
|
+
})
|
|
84
|
+
|
|
60
85
|
defineExpose({ el, valueEl })
|
|
61
86
|
</script>
|
|
62
87
|
|
|
@@ -68,17 +93,19 @@ defineExpose({ el, valueEl })
|
|
|
68
93
|
:aria-expanded="isOpen"
|
|
69
94
|
:class="[
|
|
70
95
|
layoutClass,
|
|
71
|
-
|
|
96
|
+
paddingClass,
|
|
97
|
+
autoWidth ? 'w-auto' : 'w-full',
|
|
98
|
+
inlineLabel ? 'gap-1!' : '',
|
|
72
99
|
{ 'select-trigger--error': error },
|
|
73
100
|
disabled
|
|
74
101
|
? 'border-default bg-badge/70'
|
|
75
102
|
: (buttonBg || 'border-default bg-main'),
|
|
76
103
|
]"
|
|
77
|
-
class="select-trigger relative flex
|
|
104
|
+
class="select-trigger relative flex items-center gap-2 rounded-10 border border-solid 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
105
|
@click="emits('toggle')"
|
|
79
106
|
>
|
|
80
107
|
<span
|
|
81
|
-
v-if="
|
|
108
|
+
v-if="labelMode === 'floating'"
|
|
82
109
|
:data-size="size"
|
|
83
110
|
class="select-trigger__label pointer-events-none absolute top-1.5 left-4 text-[10px] leading-none text-secondary"
|
|
84
111
|
:class="hasValue ? 'select-trigger__label--floated' : 'select-trigger__label--placeholder'"
|
|
@@ -86,13 +113,28 @@ defineExpose({ el, valueEl })
|
|
|
86
113
|
{{ label }}<span v-if="required" class="text-danger"> *</span>
|
|
87
114
|
</span>
|
|
88
115
|
|
|
116
|
+
<span
|
|
117
|
+
v-else-if="labelMode === 'placeholder'"
|
|
118
|
+
class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 truncate text-secondary"
|
|
119
|
+
>
|
|
120
|
+
{{ label }}<span v-if="required" class="text-danger"> *</span>
|
|
121
|
+
</span>
|
|
122
|
+
|
|
89
123
|
<component
|
|
90
124
|
:is="startIcon"
|
|
91
125
|
v-if="startIcon"
|
|
92
126
|
class="shrink-0"
|
|
93
127
|
/>
|
|
94
128
|
|
|
95
|
-
<span
|
|
129
|
+
<span v-if="labelMode === 'inline'" class="shrink-0 font-medium whitespace-nowrap">
|
|
130
|
+
{{ label }}<span v-if="required" class="text-danger"> *</span><template v-if="hasValue">:</template>
|
|
131
|
+
</span>
|
|
132
|
+
|
|
133
|
+
<span
|
|
134
|
+
ref="valueEl"
|
|
135
|
+
:class="autoWidth ? 'w-auto' : 'w-full'"
|
|
136
|
+
class="relative min-w-0 overflow-hidden text-start leading-tight"
|
|
137
|
+
>
|
|
96
138
|
<slot />
|
|
97
139
|
</span>
|
|
98
140
|
|
|
@@ -102,7 +144,8 @@ defineExpose({ el, valueEl })
|
|
|
102
144
|
role="button"
|
|
103
145
|
tabindex="-1"
|
|
104
146
|
aria-label="Очистить"
|
|
105
|
-
class="
|
|
147
|
+
:class="clearPosition"
|
|
148
|
+
class="select-trigger__clear absolute top-1/2 flex -translate-y-1/2 items-center justify-center rounded-full p-0.5 transition-colors hover:bg-badge"
|
|
106
149
|
@click.stop="emits('clear')"
|
|
107
150
|
@keydown.enter.stop.prevent="emits('clear')"
|
|
108
151
|
@keydown.space.stop.prevent="emits('clear')"
|
|
@@ -111,8 +154,8 @@ defineExpose({ el, valueEl })
|
|
|
111
154
|
</span>
|
|
112
155
|
|
|
113
156
|
<span
|
|
114
|
-
:class="{ 'rotate-180': isOpen }"
|
|
115
|
-
class="absolute top-1/2
|
|
157
|
+
:class="[chevronPosition, { 'rotate-180': isOpen }]"
|
|
158
|
+
class="absolute top-1/2 -translate-y-1/2 transition-transform duration-200"
|
|
116
159
|
>
|
|
117
160
|
<a-icon-chevron-down class="m-0! size-4 text-icon-disabled" />
|
|
118
161
|
</span>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
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="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-download preview-icon">
|
|
8
|
+
<path d="M12 15V3"/>
|
|
9
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
10
|
+
<path d="m7 10 5 5 5-5"/>
|
|
11
|
+
</svg>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<style scoped>
|
|
15
|
+
|
|
16
|
+
</style>
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<template>
|
|
6
|
-
<svg width="
|
|
7
|
-
|
|
8
|
-
<path d="
|
|
9
|
-
<path d="
|
|
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="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-download preview-icon">
|
|
8
|
+
<path d="M12 15V3"/>
|
|
9
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
10
|
+
<path d="m7 10 5 5 5-5"/>
|
|
10
11
|
</svg>
|
|
11
12
|
</template>
|
|
12
13
|
|
package/icons/play.vue
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-play preview-icon">
|
|
8
|
+
<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/>
|
|
9
|
+
</svg>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<style scoped>
|
|
13
|
+
|
|
14
|
+
</style>
|