adata-ui 2.1.40-beta.1 → 2.1.40-beta.3
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/a-select-row/ASelectRowV2.vue +213 -0
- package/components/elements/button/AButtonV2.vue +89 -0
- package/components/elements/segmented/ASegmentedV2.vue +58 -0
- package/components/elements/select/ASelectV2.vue +581 -0
- package/components/elements/show-more/AShowMoreV2.vue +26 -0
- package/components/forms/checkbox/ACheckboxV2.vue +229 -0
- package/components/forms/input/AInputV2.vue +542 -0
- package/components/forms/toggle/AToggleV2.vue +71 -0
- package/components/navigation/header/AHeader.vue +5 -5
- package/components/navigation/pill-tabs/APillTabsV2.vue +118 -0
- package/components/overlays/modal/AModalV2.vue +388 -0
- package/composables/useChipOverflow.ts +82 -0
- package/package.json +1 -1
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
<script generic="T, K extends keyof T" lang="ts" setup>
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
import { autoUpdate, flip, size as floatingSize, offset, shift, useFloating } from '@floating-ui/vue'
|
|
4
|
+
import { onClickOutside } from '@vueuse/core'
|
|
5
|
+
import { MaskInput } from 'maska'
|
|
6
|
+
|
|
7
|
+
defineOptions({ inheritAttrs: false })
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
10
|
+
type: 'text',
|
|
11
|
+
required: false,
|
|
12
|
+
disabled: false,
|
|
13
|
+
readonly: false,
|
|
14
|
+
clearable: false,
|
|
15
|
+
size: 'sm',
|
|
16
|
+
error: undefined,
|
|
17
|
+
placeholder: '',
|
|
18
|
+
autocomplete: 'off',
|
|
19
|
+
loading: false,
|
|
20
|
+
variant: 'default',
|
|
21
|
+
mask: undefined,
|
|
22
|
+
autocompleteList: () => [] as T[],
|
|
23
|
+
autocompleteKey: undefined,
|
|
24
|
+
autocompleteOption: undefined,
|
|
25
|
+
autocompleteFn: undefined,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
(e: 'clear'): void
|
|
30
|
+
(e: 'focus', ev: FocusEvent): void
|
|
31
|
+
(e: 'blur', ev: FocusEvent): void
|
|
32
|
+
(e: 'keydown', ev: KeyboardEvent): void
|
|
33
|
+
(e: 'enter', value: string | AutocompleteValue): void
|
|
34
|
+
(e: 'selectOption', value: AutocompleteValue): void
|
|
35
|
+
(e: 'updateValue', value: string | number): void
|
|
36
|
+
}>()
|
|
37
|
+
|
|
38
|
+
interface Props {
|
|
39
|
+
label?: string
|
|
40
|
+
type?: 'text' | 'search' | 'email' | 'password' | 'tel' | 'url' | 'number'
|
|
41
|
+
placeholder?: string
|
|
42
|
+
required?: boolean
|
|
43
|
+
disabled?: boolean
|
|
44
|
+
readonly?: boolean
|
|
45
|
+
clearable?: boolean
|
|
46
|
+
size?: 'sm' | 'md'
|
|
47
|
+
error?: string | string[]
|
|
48
|
+
startIcon?: Component
|
|
49
|
+
endIcon?: Component
|
|
50
|
+
loading?: boolean
|
|
51
|
+
variant?: 'default' | 'gray' | 'blue'
|
|
52
|
+
mask?: string
|
|
53
|
+
inputId?: string
|
|
54
|
+
autocomplete?: string
|
|
55
|
+
autocompleteList?: T[]
|
|
56
|
+
autocompleteKey?: K
|
|
57
|
+
autocompleteOption?: K
|
|
58
|
+
autocompleteFn?: (item: T) => string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type AutocompleteValue = T | T[keyof T]
|
|
62
|
+
|
|
63
|
+
function customTrim(input: string | number | null | undefined): string | number {
|
|
64
|
+
if (input == null) return ''
|
|
65
|
+
if (typeof input !== 'string') return input
|
|
66
|
+
if (input === ' ' || /\d/.test(input)) return input.trim()
|
|
67
|
+
return input
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const [modelValue, modelModifiers] = defineModel<string | number | AutocompleteValue>({
|
|
71
|
+
default: '',
|
|
72
|
+
set(value) {
|
|
73
|
+
if (modelModifiers['custom-trim']) return customTrim(value as string)
|
|
74
|
+
return value
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
79
|
+
const isFocused = ref(false)
|
|
80
|
+
|
|
81
|
+
let maskInput: MaskInput | null = null
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
if (props.mask && inputRef.value) {
|
|
84
|
+
maskInput = new MaskInput(inputRef.value, { mask: props.mask })
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
onUnmounted(() => {
|
|
88
|
+
maskInput?.destroy()
|
|
89
|
+
maskInput = null
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const internalId = useId()
|
|
93
|
+
const inputId = computed(() => props.inputId ?? `input-v2-${internalId}`)
|
|
94
|
+
|
|
95
|
+
const hasValue = computed(() => modelValue.value != null && modelValue.value !== '')
|
|
96
|
+
const hasError = computed(() => Array.isArray(props.error) ? props.error.length > 0 : !!props.error)
|
|
97
|
+
const isLabelFloated = computed(() => hasValue.value || isFocused.value)
|
|
98
|
+
|
|
99
|
+
const hasAutocomplete = computed(() => props.autocompleteList && props.autocompleteList.length > 0)
|
|
100
|
+
const isDropdownOpen = computed(() => hasAutocomplete.value && isFocused.value)
|
|
101
|
+
const currentItemIndex = ref(-1)
|
|
102
|
+
|
|
103
|
+
const fieldRef = ref<HTMLElement | null>(null)
|
|
104
|
+
const floatingRef = ref<HTMLElement | null>(null)
|
|
105
|
+
const { x, y, strategy, update } = useFloating(fieldRef, floatingRef, {
|
|
106
|
+
placement: 'bottom-start',
|
|
107
|
+
strategy: 'absolute',
|
|
108
|
+
middleware: [
|
|
109
|
+
offset(6),
|
|
110
|
+
flip({ fallbackPlacements: ['top-start'] }),
|
|
111
|
+
shift({ padding: 8 }),
|
|
112
|
+
floatingSize({
|
|
113
|
+
apply({ availableHeight, elements, rects }) {
|
|
114
|
+
const el = elements.floating as HTMLElement
|
|
115
|
+
el.style.width = `${rects.reference.width}px`
|
|
116
|
+
el.style.maxHeight = `${Math.min(availableHeight, 320)}px`
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
let cleanup: (() => void) | null = null
|
|
123
|
+
watch(isDropdownOpen, (open) => {
|
|
124
|
+
if (open && fieldRef.value && floatingRef.value) {
|
|
125
|
+
cleanup = autoUpdate(fieldRef.value, floatingRef.value, update)
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
cleanup?.()
|
|
129
|
+
cleanup = null
|
|
130
|
+
currentItemIndex.value = -1
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
onUnmounted(() => cleanup?.())
|
|
134
|
+
|
|
135
|
+
const wrapperRef = ref<HTMLDivElement | null>(null)
|
|
136
|
+
onClickOutside(wrapperRef, () => {
|
|
137
|
+
if (isFocused.value) inputRef.value?.blur()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
function resolveSelectedValue(item: T): AutocompleteValue {
|
|
141
|
+
if (props.autocompleteFn) return props.autocompleteFn(item) as AutocompleteValue
|
|
142
|
+
if (props.autocompleteKey) return item[props.autocompleteKey] as AutocompleteValue
|
|
143
|
+
return item
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyCurrentItem() {
|
|
147
|
+
const item = props.autocompleteList[currentItemIndex.value]
|
|
148
|
+
if (item == null) return
|
|
149
|
+
modelValue.value = resolveSelectedValue(item)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function onSelectOption(item: T) {
|
|
153
|
+
const value = resolveSelectedValue(item)
|
|
154
|
+
modelValue.value = value
|
|
155
|
+
emit('selectOption', value)
|
|
156
|
+
inputRef.value?.blur()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isTouchScrolling = ref(false)
|
|
160
|
+
function onTouchEndOption(item: T) {
|
|
161
|
+
if (!isTouchScrolling.value) onSelectOption(item)
|
|
162
|
+
isTouchScrolling.value = false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function onInput(ev: Event) {
|
|
166
|
+
const value = (ev.target as HTMLInputElement).value
|
|
167
|
+
modelValue.value = value
|
|
168
|
+
emit('updateValue', value)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onFocus(ev: FocusEvent) {
|
|
172
|
+
isFocused.value = true
|
|
173
|
+
emit('focus', ev)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function onBlur(ev: FocusEvent) {
|
|
177
|
+
isFocused.value = false
|
|
178
|
+
emit('blur', ev)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const autocompleteListEl = ref<HTMLUListElement | null>(null)
|
|
182
|
+
function scrollIntoView(idx: number) {
|
|
183
|
+
const container = autocompleteListEl.value
|
|
184
|
+
if (!container) return
|
|
185
|
+
const item = container.children[idx] as HTMLElement | undefined
|
|
186
|
+
if (!item) return
|
|
187
|
+
const itemTop = item.offsetTop
|
|
188
|
+
const itemBottom = itemTop + item.clientHeight
|
|
189
|
+
if (itemTop < container.scrollTop) container.scrollTop = itemTop
|
|
190
|
+
else if (itemBottom > container.scrollTop + container.clientHeight)
|
|
191
|
+
container.scrollTop = itemBottom - container.clientHeight
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function onKeydown(ev: KeyboardEvent) {
|
|
195
|
+
emit('keydown', ev)
|
|
196
|
+
|
|
197
|
+
if (hasAutocomplete.value) {
|
|
198
|
+
const last = props.autocompleteList.length - 1
|
|
199
|
+
if (ev.key === 'ArrowDown' && currentItemIndex.value < last) {
|
|
200
|
+
ev.preventDefault()
|
|
201
|
+
currentItemIndex.value++
|
|
202
|
+
applyCurrentItem()
|
|
203
|
+
nextTick(() => scrollIntoView(currentItemIndex.value))
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (ev.key === 'ArrowUp' && currentItemIndex.value > 0) {
|
|
207
|
+
ev.preventDefault()
|
|
208
|
+
currentItemIndex.value--
|
|
209
|
+
applyCurrentItem()
|
|
210
|
+
nextTick(() => scrollIntoView(currentItemIndex.value))
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
if (ev.key === 'Enter') {
|
|
214
|
+
if (currentItemIndex.value !== -1) {
|
|
215
|
+
applyCurrentItem()
|
|
216
|
+
emit('enter', props.autocompleteList[currentItemIndex.value])
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
emit('enter', (ev.target as HTMLInputElement).value)
|
|
220
|
+
}
|
|
221
|
+
inputRef.value?.blur()
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
if (ev.key === 'Escape') {
|
|
225
|
+
inputRef.value?.blur()
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (ev.key === 'Enter') emit('enter', (ev.target as HTMLInputElement).value)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function onClear() {
|
|
234
|
+
modelValue.value = ''
|
|
235
|
+
emit('clear')
|
|
236
|
+
emit('updateValue', '')
|
|
237
|
+
inputRef.value?.focus()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function focus() {
|
|
241
|
+
inputRef.value?.focus()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function blur() {
|
|
245
|
+
inputRef.value?.blur()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function onFieldMousedown(ev: MouseEvent) {
|
|
249
|
+
if (ev.target === inputRef.value) return
|
|
250
|
+
ev.preventDefault()
|
|
251
|
+
inputRef.value?.focus()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
defineExpose({ focus, blur, inputRef })
|
|
255
|
+
|
|
256
|
+
const layoutClass = computed(() => {
|
|
257
|
+
if (!props.label) {
|
|
258
|
+
return props.size === 'md' ? 'h-10 py-1.5' : 'h-9 py-1.5'
|
|
259
|
+
}
|
|
260
|
+
return props.size === 'md' ? 'h-10 pt-3 pb-1' : 'h-9 pt-2 pb-1'
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const iconOffsetClass = computed(() => {
|
|
264
|
+
if (!props.label) return ''
|
|
265
|
+
return props.size === 'md' ? '-translate-y-[4px]' : '-translate-y-[2px]'
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const showClearButton = computed(() =>
|
|
269
|
+
props.clearable && hasValue.value && !props.disabled && !props.readonly && !props.loading,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
const errorMessages = computed(() => {
|
|
273
|
+
if (!props.error) return [] as string[]
|
|
274
|
+
return Array.isArray(props.error) ? props.error : [props.error]
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const attrs = useAttrs()
|
|
278
|
+
const wrapperAttrs = computed(() => ({ class: attrs.class, style: attrs.style }))
|
|
279
|
+
const inputAttrs = computed(() => {
|
|
280
|
+
const { class: _c, style: _s, ...rest } = attrs as Record<string, unknown>
|
|
281
|
+
return rest
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const fieldClass = computed(() => {
|
|
285
|
+
if (props.disabled) {
|
|
286
|
+
return 'border-gray-200 bg-gray-100/70 dark:border-gray-700 dark:bg-white/[0.03]'
|
|
287
|
+
}
|
|
288
|
+
if (props.variant === 'gray') {
|
|
289
|
+
return 'border-transparent bg-gray-50 dark:border-transparent dark:bg-white/[0.04]'
|
|
290
|
+
}
|
|
291
|
+
if (props.variant === 'blue') {
|
|
292
|
+
return 'border-blue-200 bg-blue-50 dark:border-blue-900/50 dark:bg-blue-900/20'
|
|
293
|
+
}
|
|
294
|
+
return 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900'
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
function rowDisplay(item: T): unknown {
|
|
298
|
+
if (props.autocompleteOption) return item[props.autocompleteOption]
|
|
299
|
+
return item
|
|
300
|
+
}
|
|
301
|
+
</script>
|
|
302
|
+
|
|
303
|
+
<template>
|
|
304
|
+
<div
|
|
305
|
+
ref="wrapperRef"
|
|
306
|
+
class="input-v2 relative w-full text-sm"
|
|
307
|
+
v-bind="wrapperAttrs"
|
|
308
|
+
>
|
|
309
|
+
<div
|
|
310
|
+
ref="fieldRef"
|
|
311
|
+
class="input-v2__field text-deepblue-900 relative flex w-full cursor-text items-center gap-2 rounded-[10px] border border-solid pl-4 transition-colors duration-200 focus-within:border-blue-600 focus-within:ring-2 focus-within:ring-blue-600/20 hover:border-blue-500 dark:text-gray-200 dark:focus-within:border-blue-400 dark:focus-within:ring-blue-400/20 dark:hover:border-blue-400"
|
|
312
|
+
:class="[
|
|
313
|
+
layoutClass,
|
|
314
|
+
fieldClass,
|
|
315
|
+
startIcon ? 'pl-3' : '',
|
|
316
|
+
clearable || endIcon || loading || $slots.endButton ? 'pr-3' : 'pr-4',
|
|
317
|
+
{ 'input-v2__field--error': hasError, 'input-v2__field--disabled': disabled },
|
|
318
|
+
]"
|
|
319
|
+
@mousedown="onFieldMousedown"
|
|
320
|
+
>
|
|
321
|
+
<label
|
|
322
|
+
v-if="label"
|
|
323
|
+
:for="inputId"
|
|
324
|
+
class="input-v2__label pointer-events-none absolute top-1/2 max-w-[calc(100%-32px)] truncate text-gray-500 dark:text-gray-400"
|
|
325
|
+
:data-size="size"
|
|
326
|
+
:class="[
|
|
327
|
+
startIcon ? 'left-9' : 'left-4',
|
|
328
|
+
{ 'input-v2__label--floated': isLabelFloated },
|
|
329
|
+
]"
|
|
330
|
+
>
|
|
331
|
+
{{ label }}<span v-if="required" class="text-red-500 dark:text-red-400"> *</span>
|
|
332
|
+
</label>
|
|
333
|
+
|
|
334
|
+
<component
|
|
335
|
+
:is="startIcon"
|
|
336
|
+
v-if="startIcon"
|
|
337
|
+
class="size-4 shrink-0 text-gray-400 dark:text-gray-500"
|
|
338
|
+
:class="iconOffsetClass"
|
|
339
|
+
/>
|
|
340
|
+
|
|
341
|
+
<input
|
|
342
|
+
:id="inputId"
|
|
343
|
+
ref="inputRef"
|
|
344
|
+
:type="type"
|
|
345
|
+
:value="modelValue ?? ''"
|
|
346
|
+
:placeholder="(isLabelFloated || !label) ? placeholder : ''"
|
|
347
|
+
:disabled="disabled"
|
|
348
|
+
:readonly="readonly"
|
|
349
|
+
:autocomplete="autocomplete"
|
|
350
|
+
:tabindex="disabled ? -1 : 0"
|
|
351
|
+
class="input-v2__input text-deepblue-900 w-full min-w-0 bg-transparent leading-tight outline-none placeholder:text-gray-500 disabled:cursor-not-allowed dark:text-gray-200 dark:placeholder:text-gray-400"
|
|
352
|
+
v-bind="inputAttrs"
|
|
353
|
+
@input="onInput"
|
|
354
|
+
@focus="onFocus"
|
|
355
|
+
@blur="onBlur"
|
|
356
|
+
@keydown="onKeydown"
|
|
357
|
+
>
|
|
358
|
+
|
|
359
|
+
<a-icon-loader-circle
|
|
360
|
+
v-if="loading"
|
|
361
|
+
class="size-4 shrink-0 animate-spin text-gray-400 dark:text-gray-500"
|
|
362
|
+
:class="iconOffsetClass"
|
|
363
|
+
/>
|
|
364
|
+
|
|
365
|
+
<button
|
|
366
|
+
v-else-if="showClearButton"
|
|
367
|
+
type="button"
|
|
368
|
+
tabindex="-1"
|
|
369
|
+
class="flex shrink-0 items-center justify-center rounded-full p-0.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-white/[0.08] dark:hover:text-gray-300"
|
|
370
|
+
:class="iconOffsetClass"
|
|
371
|
+
@mousedown.prevent
|
|
372
|
+
@click.stop="onClear"
|
|
373
|
+
>
|
|
374
|
+
<a-icon-x-mark class="!m-0 size-3.5" />
|
|
375
|
+
</button>
|
|
376
|
+
|
|
377
|
+
<component
|
|
378
|
+
:is="endIcon"
|
|
379
|
+
v-else-if="endIcon"
|
|
380
|
+
class="size-4 shrink-0 text-gray-400 dark:text-gray-500"
|
|
381
|
+
:class="iconOffsetClass"
|
|
382
|
+
/>
|
|
383
|
+
|
|
384
|
+
<slot name="endButton" />
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<transition
|
|
388
|
+
enter-active-class="input-v2__dropdown-enter-active"
|
|
389
|
+
enter-from-class="input-v2__dropdown-enter-from"
|
|
390
|
+
enter-to-class="input-v2__dropdown-enter-to"
|
|
391
|
+
leave-active-class="input-v2__dropdown-leave-active"
|
|
392
|
+
leave-from-class="input-v2__dropdown-leave-from"
|
|
393
|
+
leave-to-class="input-v2__dropdown-leave-to"
|
|
394
|
+
>
|
|
395
|
+
<div
|
|
396
|
+
v-if="isDropdownOpen"
|
|
397
|
+
ref="floatingRef"
|
|
398
|
+
class="input-v2__dropdown z-[10000] flex flex-col gap-2 rounded-xl border border-gray-200 bg-white p-1.5 shadow-lg shadow-gray-900/10 dark:border-gray-700 dark:bg-gray-900 dark:shadow-black/30"
|
|
399
|
+
:style="{
|
|
400
|
+
position: strategy,
|
|
401
|
+
left: x != null ? `${x}px` : '',
|
|
402
|
+
top: y != null ? `${y}px` : '',
|
|
403
|
+
}"
|
|
404
|
+
>
|
|
405
|
+
<ul ref="autocompleteListEl" class="input-v2__list flex flex-col">
|
|
406
|
+
<li
|
|
407
|
+
v-for="(item, idx) in autocompleteList"
|
|
408
|
+
:key="idx"
|
|
409
|
+
class="cursor-pointer rounded-lg px-3 py-2 text-sm transition-colors duration-100 hover:bg-gray-50 dark:hover:bg-gray-800/60"
|
|
410
|
+
:class="{ 'input-v2__option--active': currentItemIndex === idx }"
|
|
411
|
+
@mousedown.prevent="onSelectOption(item)"
|
|
412
|
+
@touchend.prevent="onTouchEndOption(item)"
|
|
413
|
+
@touchmove="isTouchScrolling = true"
|
|
414
|
+
>
|
|
415
|
+
<slot name="autocomplete-option" :item="item">
|
|
416
|
+
{{ rowDisplay(item) }}
|
|
417
|
+
</slot>
|
|
418
|
+
</li>
|
|
419
|
+
</ul>
|
|
420
|
+
|
|
421
|
+
<slot name="autocomplete-buttons" :value="modelValue" />
|
|
422
|
+
</div>
|
|
423
|
+
</transition>
|
|
424
|
+
|
|
425
|
+
<transition
|
|
426
|
+
enter-active-class="input-v2__error-enter-active"
|
|
427
|
+
enter-from-class="input-v2__error-enter-from"
|
|
428
|
+
enter-to-class="input-v2__error-enter-to"
|
|
429
|
+
leave-active-class="input-v2__error-leave-active"
|
|
430
|
+
leave-from-class="input-v2__error-leave-from"
|
|
431
|
+
leave-to-class="input-v2__error-leave-to"
|
|
432
|
+
>
|
|
433
|
+
<p v-if="hasError" class="mt-1 flex flex-col text-xs text-red-500 dark:text-red-400">
|
|
434
|
+
<span v-for="(err, idx) in errorMessages" :key="idx">{{ err }}</span>
|
|
435
|
+
</p>
|
|
436
|
+
</transition>
|
|
437
|
+
</div>
|
|
438
|
+
</template>
|
|
439
|
+
|
|
440
|
+
<style scoped>
|
|
441
|
+
.input-v2__field--error {
|
|
442
|
+
border-color: theme('colors.red.500');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.input-v2__field--disabled {
|
|
446
|
+
pointer-events: none;
|
|
447
|
+
cursor: not-allowed;
|
|
448
|
+
opacity: 0.7;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.input-v2__field--disabled .input-v2__input {
|
|
452
|
+
cursor: not-allowed;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.input-v2__label {
|
|
456
|
+
font-size: 14px;
|
|
457
|
+
line-height: 1.3;
|
|
458
|
+
transform: translateY(-50%);
|
|
459
|
+
transition:
|
|
460
|
+
transform 300ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
461
|
+
font-size 300ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
462
|
+
color 300ms ease-out;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.input-v2__label--floated {
|
|
466
|
+
font-size: 10px;
|
|
467
|
+
transform: translateY(-17px);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.input-v2__label--floated[data-size="sm"] {
|
|
471
|
+
transform: translateY(-14px);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.input-v2__dropdown-enter-active,
|
|
475
|
+
.input-v2__dropdown-leave-active {
|
|
476
|
+
transition:
|
|
477
|
+
opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
478
|
+
transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.input-v2__dropdown-enter-from,
|
|
482
|
+
.input-v2__dropdown-leave-to {
|
|
483
|
+
opacity: 0;
|
|
484
|
+
transform: translateY(-4px);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.input-v2__dropdown-enter-to,
|
|
488
|
+
.input-v2__dropdown-leave-from {
|
|
489
|
+
opacity: 1;
|
|
490
|
+
transform: translateY(0);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.input-v2__list {
|
|
494
|
+
max-height: 250px;
|
|
495
|
+
overflow-y: auto;
|
|
496
|
+
overflow-x: hidden;
|
|
497
|
+
scrollbar-width: none;
|
|
498
|
+
-ms-overflow-style: none;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.input-v2__list::-webkit-scrollbar {
|
|
502
|
+
width: 0;
|
|
503
|
+
height: 0;
|
|
504
|
+
display: none;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.input-v2__option--active {
|
|
508
|
+
background-color: theme('colors.blue.50');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
:is(.dark) .input-v2__option--active {
|
|
512
|
+
background-color: rgb(30 64 175 / 0.18);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.input-v2__error-enter-active {
|
|
516
|
+
transition: transform 0.3s, opacity 0.3s;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.input-v2__error-enter-from {
|
|
520
|
+
opacity: 0;
|
|
521
|
+
transform: translateY(-4px);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.input-v2__error-leave-active {
|
|
525
|
+
transition: transform 0.2s, opacity 0.2s;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.input-v2__error-leave-to {
|
|
529
|
+
opacity: 0;
|
|
530
|
+
transform: translateY(-4px);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
@media (prefers-reduced-motion: reduce) {
|
|
534
|
+
.input-v2__label,
|
|
535
|
+
.input-v2__dropdown-enter-active,
|
|
536
|
+
.input-v2__dropdown-leave-active,
|
|
537
|
+
.input-v2__error-enter-active,
|
|
538
|
+
.input-v2__error-leave-active {
|
|
539
|
+
transition: none;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
type Size = 'sm' | 'md'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
label?: string
|
|
6
|
+
labelSide?: 'left' | 'right'
|
|
7
|
+
size?: Size
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
}>(), {
|
|
10
|
+
labelSide: 'left',
|
|
11
|
+
size: 'md',
|
|
12
|
+
disabled: false,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const model = defineModel<boolean>({ default: false })
|
|
16
|
+
|
|
17
|
+
const SIZE: Record<Size, { track: string, knob: string, on: string, off: string, text: string }> = {
|
|
18
|
+
sm: { track: 'h-5 w-9', knob: 'size-4', on: 'translate-x-4', off: 'translate-x-0.5', text: 'text-xs' },
|
|
19
|
+
md: { track: 'h-6 w-11', knob: 'size-5', on: 'translate-x-5', off: 'translate-x-0.5', text: 'text-sm' },
|
|
20
|
+
}
|
|
21
|
+
const s = computed(() => SIZE[props.size])
|
|
22
|
+
|
|
23
|
+
function toggle() {
|
|
24
|
+
if (props.disabled) return
|
|
25
|
+
model.value = !model.value
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
role="switch"
|
|
33
|
+
:aria-checked="model"
|
|
34
|
+
:aria-disabled="disabled || undefined"
|
|
35
|
+
:disabled="disabled"
|
|
36
|
+
class="toggle-v2 group inline-flex select-none items-center gap-2 rounded-full focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
37
|
+
:class="labelSide === 'right' ? 'flex-row-reverse' : ''"
|
|
38
|
+
@click="toggle"
|
|
39
|
+
>
|
|
40
|
+
<span
|
|
41
|
+
v-if="label"
|
|
42
|
+
class="font-medium text-gray-600 transition-colors group-hover:text-gray-800 dark:text-gray-300 dark:group-hover:text-gray-100"
|
|
43
|
+
:class="s.text"
|
|
44
|
+
>
|
|
45
|
+
{{ label }}
|
|
46
|
+
</span>
|
|
47
|
+
<span
|
|
48
|
+
class="relative inline-flex shrink-0 items-center rounded-full ring-1 ring-inset transition-colors duration-200 group-focus-visible:ring-2 group-focus-visible:ring-blue-500/50"
|
|
49
|
+
:class="[
|
|
50
|
+
s.track,
|
|
51
|
+
model
|
|
52
|
+
? 'bg-blue-600 ring-blue-600/30 dark:bg-blue-500 dark:ring-blue-400/30'
|
|
53
|
+
: 'bg-gray-200 ring-gray-300/60 dark:bg-gray-700 dark:ring-gray-600/60',
|
|
54
|
+
]"
|
|
55
|
+
>
|
|
56
|
+
<span
|
|
57
|
+
class="toggle-v2__knob inline-block rounded-full bg-white shadow-sm transition-transform duration-200 will-change-transform dark:bg-gray-100"
|
|
58
|
+
:class="[s.knob, model ? s.on : s.off]"
|
|
59
|
+
/>
|
|
60
|
+
</span>
|
|
61
|
+
</button>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<style scoped>
|
|
65
|
+
@media (prefers-reduced-motion: reduce) {
|
|
66
|
+
.toggle-v2,
|
|
67
|
+
.toggle-v2 * {
|
|
68
|
+
transition: none !important;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
@@ -109,7 +109,7 @@ onMounted(() => {
|
|
|
109
109
|
<system-notification />
|
|
110
110
|
<header
|
|
111
111
|
ref="headerRef"
|
|
112
|
-
class="relative h-16 border-b border-deepblue-900/10 bg-gray-50/
|
|
112
|
+
class="relative h-16 border-b border-deepblue-900/10 bg-gray-50/20 backdrop-blur-lg dark:border-gray-200/10 dark:bg-gray-900/30"
|
|
113
113
|
>
|
|
114
114
|
<div class="a-container mobile-padding flex h-full items-center justify-between gap-2">
|
|
115
115
|
<!-- Desktop hidden -->
|
|
@@ -135,18 +135,18 @@ onMounted(() => {
|
|
|
135
135
|
</div>
|
|
136
136
|
<div
|
|
137
137
|
v-else-if="mobileHeaderType === 'search'"
|
|
138
|
-
class="flex w-full items-center gap-2
|
|
138
|
+
class="flex w-full items-center gap-2 lg:hidden"
|
|
139
139
|
>
|
|
140
140
|
<logo
|
|
141
141
|
class="dark:text-gray-200"
|
|
142
142
|
@click="goToAnotherModule"
|
|
143
143
|
/>
|
|
144
144
|
<button
|
|
145
|
-
class="flex w-full items-center gap-2 rounded
|
|
145
|
+
class="flex w-full items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-1.5 shadow-sm dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400"
|
|
146
146
|
@click="$emit('search')"
|
|
147
147
|
>
|
|
148
|
-
<a-icon-search />
|
|
149
|
-
<span class="body-400">Найти</span>
|
|
148
|
+
<a-icon-search class="text-gray-400 dark:text-gray-500" />
|
|
149
|
+
<span class="body-400 text-gray-500 dark:text-gray-400">Найти</span>
|
|
150
150
|
</button>
|
|
151
151
|
</div>
|
|
152
152
|
</section>
|