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.
@@ -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
- el.style.width = widthOverride ?? `${rects.reference.width}px`
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 ref="wrapper" class="select-v2 relative w-full text-body-md">
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 (!props.floatingLabel) {
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
- clearable ? 'pr-16' : 'pr-9',
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 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"
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="floatingLabel && label"
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">&nbsp;*</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">&nbsp;*</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 ref="valueEl" class="relative w-full min-w-0 overflow-hidden text-start leading-tight">
129
+ <span v-if="labelMode === 'inline'" class="shrink-0 font-medium whitespace-nowrap">
130
+ {{ label }}<span v-if="required" class="text-danger">&nbsp;*</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="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"
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 right-3 -translate-y-1/2 transition-transform duration-200"
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="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
7
- <path d="M17.5 12.5V15.8333C17.5 16.2754 17.3244 16.6993 17.0118 17.0118C16.6993 17.3244 16.2754 17.5 15.8333 17.5H4.16667C3.72464 17.5 3.30072 17.3244 2.98816 17.0118C2.67559 16.6993 2.5 16.2754 2.5 15.8333V12.5" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
8
- <path d="M5.83334 8.33337L10 12.5L14.1667 8.33337" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
9
- <path d="M10 12.5V2.5" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
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>