adata-ui 2.1.40-beta.1 → 2.1.40-beta.2
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/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,213 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useChipOverflow } from '#adata-ui/composables/useChipOverflow'
|
|
3
|
+
|
|
4
|
+
interface Item {
|
|
5
|
+
id: number | string
|
|
6
|
+
name: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
label: string
|
|
11
|
+
selectedItems: Item[]
|
|
12
|
+
clearable?: boolean
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
deletable?: boolean
|
|
15
|
+
btnClass?: string
|
|
16
|
+
size?: 'sm' | 'md'
|
|
17
|
+
required?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
clearable: true,
|
|
22
|
+
disabled: false,
|
|
23
|
+
deletable: false,
|
|
24
|
+
btnClass: '',
|
|
25
|
+
size: 'md',
|
|
26
|
+
required: false,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits<{
|
|
30
|
+
(e: 'delete', item: Item): void
|
|
31
|
+
(e: 'clear'): void
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
|
35
|
+
|
|
36
|
+
const chipsRow = ref<HTMLElement | null>(null)
|
|
37
|
+
const measureRow = ref<HTMLElement | null>(null)
|
|
38
|
+
|
|
39
|
+
const itemCount = computed(() => props.selectedItems?.length ?? 0)
|
|
40
|
+
|
|
41
|
+
const { visibleCount, hiddenCount } = useChipOverflow({
|
|
42
|
+
container: chipsRow,
|
|
43
|
+
measure: measureRow,
|
|
44
|
+
count: itemCount,
|
|
45
|
+
gap: 6,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const visibleItems = computed(() => props.selectedItems.slice(0, visibleCount.value))
|
|
49
|
+
|
|
50
|
+
const showFloatingLabel = computed(() => !!props.label && !visibleItems.value?.length)
|
|
51
|
+
|
|
52
|
+
const hasValue = computed(() => itemCount.value > 0)
|
|
53
|
+
|
|
54
|
+
const isLabelFloated = computed(() => hasValue.value || isOpen.value)
|
|
55
|
+
|
|
56
|
+
function deleteItem(item: Item) {
|
|
57
|
+
emit('delete', item)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onClear() {
|
|
61
|
+
emit('clear')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const triggerLayoutClass = computed(() => {
|
|
65
|
+
if (hasValue.value || !props.label) {
|
|
66
|
+
return props.size === 'md' ? 'h-10 py-1.5' : 'h-9 py-1.5'
|
|
67
|
+
}
|
|
68
|
+
return props.size === 'md' ? 'h-10 pt-3.5 pb-1' : 'h-9 pt-[14px] pb-1'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
defineExpose({ onClear })
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<div class="select-row-v2 relative w-full text-sm">
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
:disabled="disabled"
|
|
79
|
+
:class="[
|
|
80
|
+
triggerLayoutClass,
|
|
81
|
+
clearable ? 'pr-16' : 'pr-9',
|
|
82
|
+
disabled
|
|
83
|
+
? 'border-gray-200 bg-gray-100/70 dark:border-gray-700 dark:bg-white/[0.03]'
|
|
84
|
+
: (btnClass || 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900'),
|
|
85
|
+
{ 'select-row-v2__trigger--open': isOpen },
|
|
86
|
+
]"
|
|
87
|
+
class="select-row-v2__trigger text-deepblue-900 relative flex w-full items-center gap-2 rounded-[10px] border border-solid pl-4 transition-colors duration-200 hover:border-blue-500 focus-visible:border-blue-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/20 dark:text-gray-200 dark:hover:border-blue-400 dark:focus-visible:border-blue-400 dark:focus-visible:ring-blue-400/20"
|
|
88
|
+
@click="isOpen = !isOpen"
|
|
89
|
+
>
|
|
90
|
+
<span
|
|
91
|
+
v-if="showFloatingLabel"
|
|
92
|
+
:data-size="size"
|
|
93
|
+
class="select-row-v2__label pointer-events-none absolute left-4 top-1/2 max-w-[calc(100%-48px)] truncate text-gray-500 dark:text-gray-400"
|
|
94
|
+
:class="{ 'select-row-v2__label--floated': isLabelFloated }"
|
|
95
|
+
>
|
|
96
|
+
{{ label }}<span v-if="required" class="text-red-500 dark:text-red-400"> *</span>
|
|
97
|
+
</span>
|
|
98
|
+
|
|
99
|
+
<span ref="chipsRow" class="relative flex w-full min-w-0 items-center gap-1.5 overflow-hidden text-start leading-none">
|
|
100
|
+
<template v-if="hasValue">
|
|
101
|
+
<span
|
|
102
|
+
v-for="item in visibleItems"
|
|
103
|
+
:key="item.id"
|
|
104
|
+
class="select-row-v2__chip inline-flex min-w-0 max-w-[180px] items-center gap-1 rounded-md border border-gray-200 bg-gray-50 px-2 py-0.5 text-xs dark:border-gray-700 dark:bg-white/[0.06]"
|
|
105
|
+
@click.stop="deletable ? deleteItem(item) : (isOpen = true)"
|
|
106
|
+
>
|
|
107
|
+
<span class="truncate">{{ item.name }}</span>
|
|
108
|
+
<button
|
|
109
|
+
v-if="deletable"
|
|
110
|
+
type="button"
|
|
111
|
+
class="flex shrink-0 items-center justify-center rounded-full p-0.5 transition-colors hover:bg-gray-200 dark:hover:bg-white/[0.12]"
|
|
112
|
+
>
|
|
113
|
+
<a-icon-x-mark class="size-2.5" />
|
|
114
|
+
</button>
|
|
115
|
+
</span>
|
|
116
|
+
<span
|
|
117
|
+
v-if="hiddenCount > 0"
|
|
118
|
+
class="inline-flex shrink-0 items-center whitespace-nowrap rounded-md border border-blue-200 bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
|
119
|
+
>
|
|
120
|
+
+{{ hiddenCount }}
|
|
121
|
+
</span>
|
|
122
|
+
|
|
123
|
+
<span
|
|
124
|
+
ref="measureRow"
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
class="pointer-events-none invisible absolute left-0 top-0 flex h-0 items-center gap-1.5"
|
|
127
|
+
>
|
|
128
|
+
<span
|
|
129
|
+
v-for="item in selectedItems"
|
|
130
|
+
:key="`measure-${item.id}`"
|
|
131
|
+
class="inline-flex max-w-[180px] items-center gap-1 rounded-md border border-gray-200 bg-gray-50 px-2 py-0.5 text-xs"
|
|
132
|
+
>
|
|
133
|
+
<span class="truncate">{{ item.name }}</span>
|
|
134
|
+
<span
|
|
135
|
+
v-if="deletable"
|
|
136
|
+
class="flex shrink-0 items-center justify-center rounded-full p-0.5"
|
|
137
|
+
>
|
|
138
|
+
<a-icon-x-mark class="size-2.5" />
|
|
139
|
+
</span>
|
|
140
|
+
</span>
|
|
141
|
+
<span class="inline-flex shrink-0 items-center whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium">
|
|
142
|
+
+{{ itemCount }}
|
|
143
|
+
</span>
|
|
144
|
+
</span>
|
|
145
|
+
</template>
|
|
146
|
+
<span v-else class="text-gray-500 dark:text-gray-400">​</span>
|
|
147
|
+
</span>
|
|
148
|
+
|
|
149
|
+
<button
|
|
150
|
+
v-if="clearable && hasValue && !disabled"
|
|
151
|
+
type="button"
|
|
152
|
+
class="absolute right-9 top-1/2 flex -translate-y-1/2 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"
|
|
153
|
+
@click.stop="onClear"
|
|
154
|
+
>
|
|
155
|
+
<a-icon-x-mark class="!m-0 size-3.5" />
|
|
156
|
+
</button>
|
|
157
|
+
|
|
158
|
+
<span
|
|
159
|
+
:class="{ 'rotate-180': isOpen }"
|
|
160
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 transition-transform duration-200"
|
|
161
|
+
>
|
|
162
|
+
<a-icon-chevron-down class="!m-0 size-4 text-gray-400 dark:text-gray-500" />
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
167
|
+
|
|
168
|
+
<style scoped>
|
|
169
|
+
.select-row-v2__trigger:disabled {
|
|
170
|
+
pointer-events: none;
|
|
171
|
+
cursor: not-allowed;
|
|
172
|
+
opacity: 0.7;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.select-row-v2__chip {
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
transition: background-color 150ms;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.select-row-v2__chip:hover {
|
|
181
|
+
background-color: theme('colors.gray.100');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
:is(.dark) .select-row-v2__chip:hover {
|
|
185
|
+
background-color: rgb(255 255 255 / 0.08);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.select-row-v2__label {
|
|
189
|
+
font-size: 14px;
|
|
190
|
+
line-height: 1.3;
|
|
191
|
+
transform: translateY(-50%);
|
|
192
|
+
transition:
|
|
193
|
+
transform 300ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
194
|
+
font-size 300ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
195
|
+
color 300ms ease-out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.select-row-v2__label--floated {
|
|
199
|
+
font-size: 10px;
|
|
200
|
+
transform: translateY(-17px);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.select-row-v2__label--floated[data-size="sm"] {
|
|
204
|
+
transform: translateY(-14px);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@media (prefers-reduced-motion: reduce) {
|
|
208
|
+
.select-row-v2__label,
|
|
209
|
+
.select-row-v2__trigger {
|
|
210
|
+
transition: none;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
type View = 'solid' | 'outline' | 'ghost' | 'soft'
|
|
5
|
+
type Size = 'sm' | 'md' | 'lg'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<{
|
|
8
|
+
view?: View
|
|
9
|
+
size?: Size
|
|
10
|
+
icon?: Component
|
|
11
|
+
iconRight?: boolean
|
|
12
|
+
loading?: boolean
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
block?: boolean
|
|
15
|
+
type?: 'button' | 'submit' | 'reset'
|
|
16
|
+
}>(), {
|
|
17
|
+
view: 'solid',
|
|
18
|
+
size: 'md',
|
|
19
|
+
iconRight: false,
|
|
20
|
+
loading: false,
|
|
21
|
+
disabled: false,
|
|
22
|
+
block: false,
|
|
23
|
+
type: 'button',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{ click: [event: MouseEvent] }>()
|
|
27
|
+
|
|
28
|
+
const VIEW_CLASS: Record<View, string> = {
|
|
29
|
+
solid: 'bg-blue-600 text-white shadow-sm hover:bg-blue-700 hover:shadow-md active:bg-blue-800 dark:bg-blue-500 dark:hover:bg-blue-400',
|
|
30
|
+
outline: 'border border-gray-300 bg-white text-deepblue-900 hover:border-gray-400 hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-gray-600 dark:hover:bg-white/[0.06]',
|
|
31
|
+
ghost: 'text-deepblue-900 hover:bg-gray-100 active:bg-gray-200 dark:text-gray-100 dark:hover:bg-white/[0.06] dark:active:bg-white/[0.1]',
|
|
32
|
+
soft: 'bg-blue-50 text-blue-700 hover:bg-blue-100 active:bg-blue-200/70 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/40',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SIZE_CLASS: Record<Size, string> = {
|
|
36
|
+
sm: 'h-8 gap-1.5 px-3 text-xs',
|
|
37
|
+
md: 'h-10 gap-2 px-4 text-sm',
|
|
38
|
+
lg: 'h-12 gap-2.5 px-5 text-base',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ICON_SIZE: Record<Size, string> = {
|
|
42
|
+
sm: 'size-4',
|
|
43
|
+
md: 'size-5',
|
|
44
|
+
lg: 'size-5',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function onClick(event: MouseEvent) {
|
|
48
|
+
if (props.disabled || props.loading) return
|
|
49
|
+
emit('click', event)
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<button
|
|
55
|
+
:type="type"
|
|
56
|
+
:disabled="disabled || loading"
|
|
57
|
+
class="button-v2 inline-flex select-none items-center justify-center rounded-xl font-semibold transition-all duration-150 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
58
|
+
:class="[
|
|
59
|
+
VIEW_CLASS[view],
|
|
60
|
+
SIZE_CLASS[size],
|
|
61
|
+
block ? 'w-full' : '',
|
|
62
|
+
iconRight ? 'flex-row-reverse' : '',
|
|
63
|
+
]"
|
|
64
|
+
@click="onClick"
|
|
65
|
+
>
|
|
66
|
+
<a-icon-loader-circle
|
|
67
|
+
v-if="loading"
|
|
68
|
+
class="shrink-0 animate-spin"
|
|
69
|
+
:class="ICON_SIZE[size]"
|
|
70
|
+
/>
|
|
71
|
+
<component
|
|
72
|
+
:is="icon"
|
|
73
|
+
v-else-if="icon"
|
|
74
|
+
class="shrink-0"
|
|
75
|
+
:class="ICON_SIZE[size]"
|
|
76
|
+
/>
|
|
77
|
+
<span v-if="$slots.default" class="truncate">
|
|
78
|
+
<slot />
|
|
79
|
+
</span>
|
|
80
|
+
</button>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<style scoped>
|
|
84
|
+
@media (prefers-reduced-motion: reduce) {
|
|
85
|
+
.button-v2 {
|
|
86
|
+
transition: none;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script generic="T extends string | number" lang="ts" setup>
|
|
2
|
+
interface Option {
|
|
3
|
+
value: T
|
|
4
|
+
label: string
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
count?: number
|
|
7
|
+
dotColor?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
options: Option[]
|
|
12
|
+
size?: 'sm' | 'md'
|
|
13
|
+
deselectable?: boolean
|
|
14
|
+
}>(), {
|
|
15
|
+
size: 'md',
|
|
16
|
+
deselectable: true,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const modelValue = defineModel<T | undefined | null>()
|
|
20
|
+
|
|
21
|
+
function select(option: Option) {
|
|
22
|
+
if (option.disabled) return
|
|
23
|
+
if (props.deselectable && modelValue.value === option.value) {
|
|
24
|
+
modelValue.value = undefined
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
modelValue.value = option.value
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="flex flex-wrap gap-2">
|
|
33
|
+
<button
|
|
34
|
+
v-for="option in options"
|
|
35
|
+
:key="String(option.value)"
|
|
36
|
+
type="button"
|
|
37
|
+
:disabled="option.disabled"
|
|
38
|
+
class="inline-flex items-center gap-1.5 rounded-full border transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
39
|
+
:class="[
|
|
40
|
+
size === 'sm' ? 'px-3 py-1 text-xs' : 'px-3.5 py-1.5 text-sm',
|
|
41
|
+
modelValue === option.value
|
|
42
|
+
? 'border-transparent bg-blue-700 font-medium text-white dark:bg-blue-500 dark:text-gray-900'
|
|
43
|
+
: 'border-gray-300 bg-transparent text-gray-700 hover:border-gray-400 dark:border-gray-600 dark:text-gray-300 dark:hover:border-gray-500',
|
|
44
|
+
]"
|
|
45
|
+
@click="select(option)"
|
|
46
|
+
>
|
|
47
|
+
<span
|
|
48
|
+
v-if="option.dotColor"
|
|
49
|
+
class="size-2 shrink-0 rounded-full"
|
|
50
|
+
:style="{ backgroundColor: option.dotColor }"
|
|
51
|
+
/>
|
|
52
|
+
<span>{{ option.label }}</span>
|
|
53
|
+
<span v-if="option.count != null" class="tabular-nums opacity-70">
|
|
54
|
+
{{ option.count }}
|
|
55
|
+
</span>
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|