@saasmakers/ui 2.0.60 → 2.0.61
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
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { BaseRing } from '../../types/bases'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<BaseRing>(), {
|
|
5
|
+
clickable: false,
|
|
6
|
+
completed: 0,
|
|
7
|
+
dashed: false,
|
|
8
|
+
format: 'count',
|
|
9
|
+
size: 'base',
|
|
10
|
+
total: 0,
|
|
11
|
+
transitionColors: false,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
click: [event: MouseEvent]
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const sizes = {
|
|
19
|
+
base: {
|
|
20
|
+
inset: 1,
|
|
21
|
+
size: 28,
|
|
22
|
+
strokeWidth: 3,
|
|
23
|
+
},
|
|
24
|
+
lg: {
|
|
25
|
+
inset: 0,
|
|
26
|
+
size: 36,
|
|
27
|
+
strokeWidth: 4,
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
const geometry = computed(() => {
|
|
32
|
+
const { inset, size, strokeWidth } = sizes[props.size]
|
|
33
|
+
const center = size / 2
|
|
34
|
+
const radius = center - strokeWidth / 2 - inset
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
center,
|
|
38
|
+
circumference: 2 * Math.PI * radius,
|
|
39
|
+
radius,
|
|
40
|
+
size,
|
|
41
|
+
strokeWidth,
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const clampedCompleted = computed(() => {
|
|
46
|
+
return Math.max(0, Math.min(props.completed, props.total))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const percentage = computed(() => {
|
|
50
|
+
if (props.total <= 0) {
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return Math.round((clampedCompleted.value / props.total) * 100)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const centerLabel = computed(() => {
|
|
58
|
+
if (props.format === 'none' || props.total <= 0) {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (props.format === 'percent') {
|
|
63
|
+
return `${percentage.value}%`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return String(clampedCompleted.value)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const progressColor = computed(() => {
|
|
70
|
+
const progressPercentage = percentage.value
|
|
71
|
+
|
|
72
|
+
if (progressPercentage === 100) {
|
|
73
|
+
return 'var(--indigo-800)'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (progressPercentage >= 75) {
|
|
77
|
+
return 'var(--indigo-700)'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (progressPercentage >= 50) {
|
|
81
|
+
return 'var(--indigo-500)'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (progressPercentage >= 25) {
|
|
85
|
+
return 'var(--indigo-400)'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return 'var(--indigo-300)'
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Visual gap (in viewBox units) between segments, widened for fewer steps.
|
|
92
|
+
function gapForTotal(total: number) {
|
|
93
|
+
if (total <= 4) {
|
|
94
|
+
return 3
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (total <= 6) {
|
|
98
|
+
return 2.5
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return 2
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ringArcs = computed(() => {
|
|
105
|
+
const arcs: BaseRingArc[] = []
|
|
106
|
+
const { circumference } = geometry.value
|
|
107
|
+
const completed = clampedCompleted.value
|
|
108
|
+
const { total } = props
|
|
109
|
+
|
|
110
|
+
if (total <= 0) {
|
|
111
|
+
arcs.push({
|
|
112
|
+
dashArray: props.dashed ? '3 3' : undefined,
|
|
113
|
+
filled: false,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return arcs
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (completed === total) {
|
|
120
|
+
arcs.push({ filled: true })
|
|
121
|
+
|
|
122
|
+
return arcs
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Beyond 8 steps individual segments become too thin, so we collapse them
|
|
126
|
+
// into a single continuous progress arc.
|
|
127
|
+
if (total > 8) {
|
|
128
|
+
const progressLength = (circumference * completed) / total
|
|
129
|
+
|
|
130
|
+
arcs.push(
|
|
131
|
+
{ filled: false },
|
|
132
|
+
{
|
|
133
|
+
dashArray: `${progressLength} ${circumference}`,
|
|
134
|
+
dashOffset: '0',
|
|
135
|
+
filled: true,
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return arcs
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const gap = gapForTotal(total)
|
|
143
|
+
const arcPerSegment = circumference / total
|
|
144
|
+
const segmentLength = Math.max(arcPerSegment - gap, 0)
|
|
145
|
+
|
|
146
|
+
for (let index = 0; index < total; index += 1) {
|
|
147
|
+
arcs.push({
|
|
148
|
+
dashArray: `${segmentLength} ${circumference - segmentLength}`,
|
|
149
|
+
dashOffset: `${-index * arcPerSegment}`,
|
|
150
|
+
filled: index < completed,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return arcs
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
function onClick(event: MouseEvent) {
|
|
158
|
+
if (props.clickable) {
|
|
159
|
+
emit('click', event)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<template>
|
|
165
|
+
<component
|
|
166
|
+
:is="clickable ? 'button' : 'div'"
|
|
167
|
+
class="inline-flex"
|
|
168
|
+
:aria-expanded="expanded"
|
|
169
|
+
:aria-label="label"
|
|
170
|
+
:class="{ 'cursor-pointer': clickable }"
|
|
171
|
+
:type="clickable ? 'button' : undefined"
|
|
172
|
+
@click="onClick"
|
|
173
|
+
>
|
|
174
|
+
<svg
|
|
175
|
+
class="size-full"
|
|
176
|
+
:height="geometry.size"
|
|
177
|
+
:viewBox="`0 0 ${geometry.size} ${geometry.size}`"
|
|
178
|
+
:width="geometry.size"
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
focusable="false"
|
|
181
|
+
>
|
|
182
|
+
<circle
|
|
183
|
+
v-for="(arc, index) in ringArcs"
|
|
184
|
+
:key="index"
|
|
185
|
+
:class="{
|
|
186
|
+
'stroke-gray-300 dark:stroke-gray-700': !arc.filled,
|
|
187
|
+
'transition-[stroke] duration-150 motion-reduce:transition-none': transitionColors,
|
|
188
|
+
}"
|
|
189
|
+
:cx="geometry.center"
|
|
190
|
+
:cy="geometry.center"
|
|
191
|
+
fill="none"
|
|
192
|
+
:r="geometry.radius"
|
|
193
|
+
:stroke="arc.filled ? progressColor : undefined"
|
|
194
|
+
:stroke-dasharray="arc.dashArray"
|
|
195
|
+
:stroke-dashoffset="arc.dashOffset"
|
|
196
|
+
stroke-linecap="butt"
|
|
197
|
+
:stroke-width="geometry.strokeWidth"
|
|
198
|
+
:transform="`rotate(-90 ${geometry.center} ${geometry.center})`"
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
<text
|
|
202
|
+
v-if="centerLabel"
|
|
203
|
+
class="fill-gray-600 font-bold tabular-nums dark:fill-gray-400"
|
|
204
|
+
:class="{
|
|
205
|
+
'text-2xs': size === 'base',
|
|
206
|
+
'text-3xs': size === 'lg',
|
|
207
|
+
}"
|
|
208
|
+
dominant-baseline="central"
|
|
209
|
+
text-anchor="middle"
|
|
210
|
+
:x="geometry.center"
|
|
211
|
+
:y="geometry.center"
|
|
212
|
+
>
|
|
213
|
+
{{ centerLabel }}
|
|
214
|
+
</text>
|
|
215
|
+
</svg>
|
|
216
|
+
</component>
|
|
217
|
+
</template>
|
|
@@ -28,17 +28,18 @@ const id = useId()
|
|
|
28
28
|
const listId = `${id}-list`
|
|
29
29
|
const modelValue = defineModel<Item[]>({ default: () => [] })
|
|
30
30
|
const inputRef = ref<HTMLInputElement>()
|
|
31
|
+
const listRef = ref<HTMLElement>()
|
|
31
32
|
const opened = ref(false)
|
|
32
33
|
const searchRaw = ref('')
|
|
33
34
|
const highlightedIndex = ref(0)
|
|
34
35
|
|
|
35
36
|
const filteredItems = computed(() => {
|
|
36
|
-
const selectedKeys = new Set(modelValue.value.map(item =>
|
|
37
|
-
const availableItems = props.items.filter(item => !selectedKeys.has(
|
|
37
|
+
const selectedKeys = new Set(modelValue.value.map(item => getItemKey(item)))
|
|
38
|
+
const availableItems = props.items.filter(item => !selectedKeys.has(getItemKey(item)))
|
|
38
39
|
const searchQuery = normalizeText(searchRaw.value.trim())
|
|
39
40
|
|
|
40
41
|
return searchQuery
|
|
41
|
-
? availableItems.filter(item => normalizeText(
|
|
42
|
+
? availableItems.filter(item => normalizeText(getItemText(item)).includes(searchQuery))
|
|
42
43
|
: availableItems
|
|
43
44
|
})
|
|
44
45
|
|
|
@@ -48,13 +49,13 @@ const canCreate = computed(() => {
|
|
|
48
49
|
if (props.allowCreate && trimmed) {
|
|
49
50
|
const normalizedInput = normalizeText(trimmed)
|
|
50
51
|
|
|
51
|
-
return !props.items.some(item => normalizeText(
|
|
52
|
+
return !props.items.some(item => normalizeText(getItemText(item)) === normalizedInput)
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
return false
|
|
55
56
|
})
|
|
56
57
|
|
|
57
|
-
const
|
|
58
|
+
const createOptionIndex = computed(() => {
|
|
58
59
|
return canCreate.value ? filteredItems.value.length : -1
|
|
59
60
|
})
|
|
60
61
|
|
|
@@ -70,22 +71,164 @@ watch([filteredItems, canCreate], () => {
|
|
|
70
71
|
}
|
|
71
72
|
})
|
|
72
73
|
|
|
73
|
-
function
|
|
74
|
+
function getItemKey(item: Item) {
|
|
74
75
|
return typeof props.itemKey === 'function' ? props.itemKey(item) : String(readField(item, props.itemKey))
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function
|
|
78
|
+
function getItemText(item: Item) {
|
|
78
79
|
return typeof props.itemLabel === 'function'
|
|
79
80
|
? props.itemLabel(item)
|
|
80
81
|
: String(readField(item, props.itemLabel) ?? '')
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
function
|
|
84
|
-
|
|
84
|
+
function getOptionElements() {
|
|
85
|
+
if (!listRef.value) {
|
|
86
|
+
return []
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return Array.from(listRef.value.querySelectorAll<HTMLElement>('[role="option"]'))
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
function
|
|
88
|
-
|
|
92
|
+
function getOptionRects() {
|
|
93
|
+
return getOptionElements().map((element, index) => ({
|
|
94
|
+
index,
|
|
95
|
+
rect: element.getBoundingClientRect(),
|
|
96
|
+
}))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function moveHighlight(direction: 'down' | 'left' | 'right' | 'up') {
|
|
100
|
+
const optionRects = getOptionRects()
|
|
101
|
+
|
|
102
|
+
if (optionRects.length === 0) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const currentIndex = Math.min(highlightedIndex.value, optionRects.length - 1)
|
|
107
|
+
const current = optionRects[currentIndex]!
|
|
108
|
+
|
|
109
|
+
if (direction === 'left' || direction === 'right') {
|
|
110
|
+
const sameRowOptions = optionRects.filter((option) => {
|
|
111
|
+
return Math.abs(option.rect.top - current.rect.top) <= Math.min(option.rect.height, current.rect.height) / 2
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (direction === 'right') {
|
|
115
|
+
const rightOptions = sameRowOptions
|
|
116
|
+
.filter(option => option.rect.left > current.rect.left + 1)
|
|
117
|
+
.sort((first, second) => first.rect.left - second.rect.left)
|
|
118
|
+
|
|
119
|
+
if (rightOptions.length > 0) {
|
|
120
|
+
highlightedIndex.value = rightOptions[0]!.index
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const nextRowOptions = optionRects
|
|
124
|
+
.filter(option => option.rect.top > current.rect.bottom - 1)
|
|
125
|
+
.sort((first, second) => first.rect.top - second.rect.top || first.rect.left - second.rect.left)
|
|
126
|
+
|
|
127
|
+
if (nextRowOptions.length > 0) {
|
|
128
|
+
const nextRowTop = nextRowOptions[0]!.rect.top
|
|
129
|
+
const firstInNextRow = nextRowOptions.find(option => Math.abs(option.rect.top - nextRowTop) <= 4)
|
|
130
|
+
|
|
131
|
+
if (firstInNextRow) {
|
|
132
|
+
highlightedIndex.value = firstInNextRow.index
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const leftOptions = sameRowOptions
|
|
139
|
+
.filter(option => option.rect.right < current.rect.right - 1)
|
|
140
|
+
.sort((first, second) => second.rect.left - first.rect.left)
|
|
141
|
+
|
|
142
|
+
if (leftOptions.length > 0) {
|
|
143
|
+
highlightedIndex.value = leftOptions[0]!.index
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const previousRowOptions = optionRects
|
|
147
|
+
.filter(option => option.rect.bottom < current.rect.top + 1)
|
|
148
|
+
.sort((first, second) => second.rect.top - first.rect.top || second.rect.left - first.rect.left)
|
|
149
|
+
|
|
150
|
+
if (previousRowOptions.length > 0) {
|
|
151
|
+
const previousRowTop = previousRowOptions[0]!.rect.top
|
|
152
|
+
|
|
153
|
+
const lastInPreviousRow = previousRowOptions
|
|
154
|
+
.filter(option => Math.abs(option.rect.top - previousRowTop) <= 4)
|
|
155
|
+
.sort((first, second) => second.rect.left - first.rect.left)[0]
|
|
156
|
+
|
|
157
|
+
if (lastInPreviousRow) {
|
|
158
|
+
highlightedIndex.value = lastInPreviousRow.index
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const currentCenterX = current.rect.left + current.rect.width / 2
|
|
166
|
+
|
|
167
|
+
const rowOptions = optionRects.filter((option) => {
|
|
168
|
+
if (direction === 'down') {
|
|
169
|
+
return option.rect.top > current.rect.top + 1
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return option.rect.bottom < current.rect.bottom - 1
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
if (rowOptions.length === 0) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const targetRowTop = direction === 'down'
|
|
180
|
+
? Math.min(...rowOptions.map(option => option.rect.top))
|
|
181
|
+
: Math.max(...rowOptions.map(option => option.rect.top))
|
|
182
|
+
|
|
183
|
+
const targetRowOptions = rowOptions.filter(option => Math.abs(option.rect.top - targetRowTop) <= 4)
|
|
184
|
+
|
|
185
|
+
let closestIndex: number | undefined
|
|
186
|
+
let closestDist: number | undefined
|
|
187
|
+
|
|
188
|
+
for (const option of targetRowOptions) {
|
|
189
|
+
const centerX = option.rect.left + option.rect.width / 2
|
|
190
|
+
const dist = Math.abs(centerX - currentCenterX)
|
|
191
|
+
|
|
192
|
+
if (closestDist === undefined || dist < closestDist) {
|
|
193
|
+
closestDist = dist
|
|
194
|
+
closestIndex = option.index
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (closestIndex !== undefined) {
|
|
199
|
+
highlightedIndex.value = closestIndex
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await nextTick()
|
|
204
|
+
|
|
205
|
+
getOptionElements()[highlightedIndex.value]?.scrollIntoView({
|
|
206
|
+
block: 'nearest',
|
|
207
|
+
inline: 'nearest',
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function onArrowKey(event: KeyboardEvent, direction: 'down' | 'left' | 'right' | 'up') {
|
|
212
|
+
if (optionsCount.value === 0) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (direction === 'left' || direction === 'right') {
|
|
217
|
+
const input = inputRef.value
|
|
218
|
+
const cursorAtStart = input ? input.selectionStart === 0 && input.selectionEnd === 0 : true
|
|
219
|
+
|
|
220
|
+
const cursorAtEnd = input
|
|
221
|
+
? input.selectionStart === input.value.length && input.selectionEnd === input.value.length
|
|
222
|
+
: true
|
|
223
|
+
|
|
224
|
+
if (searchRaw.value.length > 0 && ((direction === 'left' && !cursorAtStart) || (direction === 'right' && !cursorAtEnd))) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
event.preventDefault()
|
|
230
|
+
|
|
231
|
+
await moveHighlight(direction)
|
|
89
232
|
}
|
|
90
233
|
|
|
91
234
|
function onBackspace() {
|
|
@@ -102,7 +245,7 @@ function onClose() {
|
|
|
102
245
|
}
|
|
103
246
|
}
|
|
104
247
|
|
|
105
|
-
function onCreate() {
|
|
248
|
+
async function onCreate() {
|
|
106
249
|
const label = searchRaw.value.trim()
|
|
107
250
|
|
|
108
251
|
if (label) {
|
|
@@ -112,13 +255,15 @@ function onCreate() {
|
|
|
112
255
|
searchRaw.value = ''
|
|
113
256
|
highlightedIndex.value = 0
|
|
114
257
|
|
|
115
|
-
nextTick(
|
|
258
|
+
await nextTick()
|
|
259
|
+
|
|
260
|
+
inputRef.value?.focus()
|
|
116
261
|
}
|
|
117
262
|
}
|
|
118
263
|
|
|
119
264
|
function onEnter() {
|
|
120
265
|
if (optionsCount.value > 0) {
|
|
121
|
-
if (highlightedIndex.value ===
|
|
266
|
+
if (highlightedIndex.value === createOptionIndex.value) {
|
|
122
267
|
onCreate()
|
|
123
268
|
}
|
|
124
269
|
else {
|
|
@@ -135,30 +280,32 @@ function onHighlight(index: number) {
|
|
|
135
280
|
highlightedIndex.value = index
|
|
136
281
|
}
|
|
137
282
|
|
|
138
|
-
function onOpen() {
|
|
283
|
+
async function onOpen() {
|
|
139
284
|
if (!props.disabled && !opened.value) {
|
|
140
285
|
opened.value = true
|
|
141
286
|
highlightedIndex.value = 0
|
|
142
287
|
|
|
143
|
-
nextTick(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
})
|
|
288
|
+
await nextTick()
|
|
289
|
+
|
|
290
|
+
inputRef.value?.focus()
|
|
291
|
+
inputRef.value?.scrollIntoView({ block: 'nearest' })
|
|
147
292
|
}
|
|
148
293
|
}
|
|
149
294
|
|
|
150
295
|
function onRemove(item: Item) {
|
|
151
|
-
const removedKey =
|
|
296
|
+
const removedKey = getItemKey(item)
|
|
152
297
|
|
|
153
|
-
modelValue.value = modelValue.value.filter(current =>
|
|
298
|
+
modelValue.value = modelValue.value.filter(current => getItemKey(current) !== removedKey)
|
|
154
299
|
}
|
|
155
300
|
|
|
156
|
-
function onSelect(item: Item) {
|
|
301
|
+
async function onSelect(item: Item) {
|
|
157
302
|
modelValue.value = [...modelValue.value, item]
|
|
158
303
|
searchRaw.value = ''
|
|
159
304
|
highlightedIndex.value = 0
|
|
160
305
|
|
|
161
|
-
nextTick(
|
|
306
|
+
await nextTick()
|
|
307
|
+
|
|
308
|
+
inputRef.value?.focus()
|
|
162
309
|
}
|
|
163
310
|
|
|
164
311
|
function readField(item: Item, field: string) {
|
|
@@ -204,26 +351,26 @@ function readField(item: Item, field: string) {
|
|
|
204
351
|
>
|
|
205
352
|
<button
|
|
206
353
|
v-for="item in modelValue"
|
|
207
|
-
:key="
|
|
208
|
-
:aria-label="t('remove', { label:
|
|
209
|
-
class="min-h-8 inline-flex items-center gap-1 border base-border rounded-md bg-gray-100 py-1 pl-2 pr-1.5 text-gray-
|
|
354
|
+
:key="getItemKey(item)"
|
|
355
|
+
:aria-label="t('remove', { label: getItemText(item) })"
|
|
356
|
+
class="min-h-8 inline-flex items-center gap-1 border base-border rounded-md bg-gray-100 py-1 pl-2 pr-1.5 text-gray-700 font-normal tracking-tight normal-case transition-colors dark:bg-gray-800 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
210
357
|
type="button"
|
|
211
358
|
@mousedown.prevent
|
|
212
359
|
@click="onRemove(item)"
|
|
213
360
|
>
|
|
214
|
-
<span class="truncate">{{
|
|
361
|
+
<span class="truncate">{{ getItemText(item) }}</span>
|
|
215
362
|
|
|
216
363
|
<BaseIcon
|
|
217
364
|
color="gray"
|
|
218
365
|
:icon="getLayerIcon('close')"
|
|
219
|
-
size="
|
|
366
|
+
size="xs"
|
|
220
367
|
/>
|
|
221
368
|
</button>
|
|
222
369
|
|
|
223
370
|
<button
|
|
224
371
|
v-if="!opened"
|
|
225
372
|
:aria-label="t('add')"
|
|
226
|
-
class="min-h-8 inline-flex items-center gap-1 border base-border base-border-hover rounded-md border-dashed py-1 pl-1.5 pr-2 text-gray-600 font-
|
|
373
|
+
class="min-h-8 inline-flex items-center gap-1 border base-border base-border-hover rounded-md border-dashed py-1 pl-1.5 pr-2 text-gray-600 font-normal tracking-tight normal-case dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
|
|
227
374
|
:disabled="disabled"
|
|
228
375
|
type="button"
|
|
229
376
|
@click="onOpen"
|
|
@@ -241,7 +388,7 @@ function readField(item: Item, field: string) {
|
|
|
241
388
|
:id="id"
|
|
242
389
|
ref="inputRef"
|
|
243
390
|
v-model="searchRaw"
|
|
244
|
-
:aria-activedescendant="opened && optionsCount > 0 ? highlightedIndex ===
|
|
391
|
+
:aria-activedescendant="opened && optionsCount > 0 ? highlightedIndex === createOptionIndex ? `${id}-create` : `${id}-option-${highlightedIndex}` : undefined"
|
|
245
392
|
aria-autocomplete="list"
|
|
246
393
|
:aria-controls="optionsCount > 0 ? listId : undefined"
|
|
247
394
|
:aria-expanded="opened"
|
|
@@ -252,10 +399,12 @@ function readField(item: Item, field: string) {
|
|
|
252
399
|
spellcheck="false"
|
|
253
400
|
@blur="onClose"
|
|
254
401
|
@keydown.backspace="onBackspace"
|
|
255
|
-
@keydown.down
|
|
402
|
+
@keydown.down="onArrowKey($event, 'down')"
|
|
256
403
|
@keydown.enter.prevent="onEnter"
|
|
257
404
|
@keydown.escape.stop="onClose"
|
|
258
|
-
@keydown.
|
|
405
|
+
@keydown.left="onArrowKey($event, 'left')"
|
|
406
|
+
@keydown.right="onArrowKey($event, 'right')"
|
|
407
|
+
@keydown.up="onArrowKey($event, 'up')"
|
|
259
408
|
>
|
|
260
409
|
</div>
|
|
261
410
|
|
|
@@ -265,51 +414,40 @@ function readField(item: Item, field: string) {
|
|
|
265
414
|
>
|
|
266
415
|
<div
|
|
267
416
|
:id="listId"
|
|
268
|
-
|
|
417
|
+
ref="listRef"
|
|
418
|
+
class="max-h-[150px] flex flex-wrap gap-1.5 overflow-y-auto"
|
|
269
419
|
role="listbox"
|
|
270
420
|
>
|
|
271
421
|
<div
|
|
272
422
|
v-for="(item, index) in filteredItems"
|
|
273
423
|
:id="`${id}-option-${index}`"
|
|
274
|
-
:key="
|
|
424
|
+
:key="getItemKey(item)"
|
|
275
425
|
:aria-selected="index === highlightedIndex"
|
|
276
|
-
class="flex cursor-pointer items-center rounded-md px-2 font-
|
|
277
|
-
:class="{
|
|
278
|
-
'min-h-8': size === 'sm',
|
|
279
|
-
'min-h-10': size === 'base',
|
|
280
|
-
'min-h-12': size === 'lg',
|
|
281
|
-
'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100': index === highlightedIndex,
|
|
282
|
-
'text-gray-600 dark:text-gray-400': index !== highlightedIndex,
|
|
283
|
-
}"
|
|
426
|
+
class="max-w-full min-h-8 inline-flex cursor-pointer items-center border base-border rounded-md px-2 py-1 text-gray-600 font-normal tracking-tight normal-case outline-none dark:text-gray-400"
|
|
427
|
+
:class="{ 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300': index === highlightedIndex }"
|
|
284
428
|
role="option"
|
|
285
429
|
@click="onSelect(item)"
|
|
286
430
|
@mousedown.prevent
|
|
287
431
|
@mouseenter="onHighlight(index)"
|
|
288
432
|
>
|
|
289
433
|
<span class="truncate">
|
|
290
|
-
{{
|
|
434
|
+
{{ getItemText(item) }}
|
|
291
435
|
</span>
|
|
292
436
|
</div>
|
|
293
437
|
|
|
294
438
|
<div
|
|
295
439
|
v-if="canCreate"
|
|
296
440
|
:id="`${id}-create`"
|
|
297
|
-
:aria-selected="highlightedIndex ===
|
|
298
|
-
class="flex cursor-pointer items-center rounded-md px-2 font-
|
|
299
|
-
:class="{
|
|
300
|
-
'min-h-8': size === 'sm',
|
|
301
|
-
'min-h-10': size === 'base',
|
|
302
|
-
'min-h-12': size === 'lg',
|
|
303
|
-
'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100': highlightedIndex === createIndex,
|
|
304
|
-
'text-gray-600 dark:text-gray-400': highlightedIndex !== createIndex,
|
|
305
|
-
}"
|
|
441
|
+
:aria-selected="highlightedIndex === createOptionIndex"
|
|
442
|
+
class="max-w-full min-h-8 inline-flex cursor-pointer items-center gap-1 border base-border rounded-md border-dashed px-2 py-1 text-gray-600 font-normal tracking-tight normal-case outline-none dark:text-gray-400"
|
|
443
|
+
:class="{ 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300': highlightedIndex === createOptionIndex }"
|
|
306
444
|
role="option"
|
|
307
445
|
@click="onCreate"
|
|
308
446
|
@mousedown.prevent
|
|
309
|
-
@mouseenter="onHighlight(
|
|
447
|
+
@mouseenter="onHighlight(createOptionIndex)"
|
|
310
448
|
>
|
|
311
449
|
<BaseIcon
|
|
312
|
-
class="
|
|
450
|
+
class="shrink-0"
|
|
313
451
|
color="gray"
|
|
314
452
|
:icon="getLayerIcon('plus')"
|
|
315
453
|
size="2xs"
|
|
@@ -113,11 +113,11 @@ const textSize = computed<BaseSize>(() => {
|
|
|
113
113
|
}
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
watch(opened, (isOpened) => {
|
|
116
|
+
watch(opened, async (isOpened) => {
|
|
117
117
|
if (isOpened && props.searchable) {
|
|
118
|
-
nextTick(
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
await nextTick()
|
|
119
|
+
|
|
120
|
+
searchInput.value?.focus()
|
|
121
121
|
}
|
|
122
122
|
})
|
|
123
123
|
|
package/app/types/bases.d.ts
CHANGED
|
@@ -299,6 +299,28 @@ export type BaseQuoteBackground = 'gray' | 'gray-light' | 'white'
|
|
|
299
299
|
|
|
300
300
|
export type BaseQuoteSize = 'base' | 'sm' | 'xs'
|
|
301
301
|
|
|
302
|
+
export interface BaseRing {
|
|
303
|
+
clickable?: boolean
|
|
304
|
+
completed?: number
|
|
305
|
+
dashed?: boolean
|
|
306
|
+
expanded?: boolean
|
|
307
|
+
format?: BaseRingFormat
|
|
308
|
+
label?: string
|
|
309
|
+
size?: BaseRingSize
|
|
310
|
+
total?: number
|
|
311
|
+
transitionColors?: boolean
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface BaseRingArc {
|
|
315
|
+
dashArray?: string
|
|
316
|
+
dashOffset?: string
|
|
317
|
+
filled: boolean
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export type BaseRingFormat = 'count' | 'none' | 'percent'
|
|
321
|
+
|
|
322
|
+
export type BaseRingSize = 'base' | 'lg'
|
|
323
|
+
|
|
302
324
|
export interface BaseShortcut {
|
|
303
325
|
active?: boolean
|
|
304
326
|
shortcut?: string
|
package/app/types/global.d.ts
CHANGED
|
@@ -64,6 +64,10 @@ declare global {
|
|
|
64
64
|
type BaseQuote = import('./bases').BaseQuote
|
|
65
65
|
type BaseQuoteBackground = import('./bases').BaseQuoteBackground
|
|
66
66
|
type BaseQuoteSize = import('./bases').BaseQuoteSize
|
|
67
|
+
type BaseRing = import('./bases').BaseRing
|
|
68
|
+
type BaseRingArc = import('./bases').BaseRingArc
|
|
69
|
+
type BaseRingFormat = import('./bases').BaseRingFormat
|
|
70
|
+
type BaseRingSize = import('./bases').BaseRingSize
|
|
67
71
|
type BaseShortcut = import('./bases').BaseShortcut
|
|
68
72
|
type BaseSize = import('./bases').BaseSize
|
|
69
73
|
type BaseSpinner = import('./bases').BaseSpinner
|