@veritree/ui 0.27.0 → 0.28.0-1
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/.claude/settings.local.json +10 -0
- package/index.js +105 -75
- package/mixins/floating-ui-content.js +17 -4
- package/mixins/floating-ui-item.js +31 -15
- package/mixins/floating-ui.js +142 -24
- package/mixins/form-control-icon.js +3 -3
- package/mixins/form-control.js +45 -20
- package/nuxt.js +38 -26
- package/package.json +17 -6
- package/src/components/Alert/VTAlert.vue +55 -14
- package/src/components/Avatar/VTAvatarImage.vue +6 -26
- package/src/components/Badge/VTBadge.vue +60 -0
- package/src/components/Badge/VTBadgeNew.vue +60 -0
- package/src/components/Breadcrumb/VTBreadcrumbItem.vue +11 -0
- package/src/components/Breadcrumb/VTBreadcrumbLink.vue +40 -0
- package/src/components/Breadcrumb/VTBreadcrumbList.vue +11 -0
- package/src/components/Breadcrumb/VTBreadcrumbRoot.vue +11 -0
- package/src/components/Breadcrumb/VTBreadcrumbSeparator.vue +19 -0
- package/src/components/Button/VTButton.vue +104 -56
- package/src/components/Carousel/VTCarousel.vue +69 -0
- package/src/components/Carousel/VTCarouselBackward.vue +36 -0
- package/src/components/Carousel/VTCarouselForward.vue +38 -0
- package/src/components/Carousel/VTCarouselTracker.vue +80 -0
- package/src/components/Checkbox/VTCheckbox.vue +134 -0
- package/src/components/Checkbox/VTCheckboxLabel.vue +3 -0
- package/src/components/Checkbox/VTCheckboxText.vue +20 -0
- package/src/components/Chip/VTChip.vue +29 -0
- package/src/components/Dialog/VTDialog.vue +59 -25
- package/src/components/Dialog/VTDialogClose.vue +3 -2
- package/src/components/Dialog/VTDialogContent.vue +29 -7
- package/src/components/Dialog/VTDialogFooter.vue +17 -2
- package/src/components/Dialog/VTDialogHeader.vue +2 -1
- package/src/components/Dialog/VTDialogMain.vue +5 -1
- package/src/components/Dialog/VTDialogOverlay.vue +5 -1
- package/src/components/Dialog/VTDialogTitle.vue +1 -1
- package/src/components/Disclosure/VTDisclosure.vue +2 -11
- package/src/components/Disclosure/VTDisclosureContent.vue +26 -52
- package/src/components/Disclosure/VTDisclosureDetails.vue +27 -2
- package/src/components/Disclosure/VTDisclosureHeader.vue +56 -89
- package/src/components/Disclosure/VTDisclosureIcon.vue +42 -31
- package/src/components/Divider/VTDivider.vue +9 -0
- package/src/components/Drawer/VTDrawer.vue +6 -15
- package/src/components/Drawer/VTDrawerClose.vue +5 -5
- package/src/components/Drawer/VTDrawerContent.vue +10 -10
- package/src/components/Drawer/VTDrawerFooter.vue +4 -4
- package/src/components/Drawer/VTDrawerHeader.vue +4 -4
- package/src/components/Drawer/VTDrawerMain.vue +5 -5
- package/src/components/Drawer/VTDrawerOverlay.vue +6 -6
- package/src/components/Drawer/VTDrawerTitle.vue +5 -5
- package/src/components/DropdownMenu/VTDropdownMenu.vue +0 -6
- package/src/components/DropdownMenu/VTDropdownMenuContent.vue +10 -1
- package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +7 -16
- package/src/components/DropdownMenu/VTDropdownMenuItem.vue +5 -1
- package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +1 -10
- package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +2 -4
- package/src/components/Form/VTFieldset.vue +5 -0
- package/src/components/Form/VTForm.vue +11 -0
- package/src/components/Form/VTFormCol.vue +20 -0
- package/src/components/Form/VTFormFeedback.vue +7 -1
- package/src/components/Form/VTFormGroup.vue +5 -7
- package/src/components/Form/VTFormLabel.vue +22 -0
- package/src/components/Form/VTFormLabelHelper.vue +22 -0
- package/src/components/Form/VTFormRow.vue +5 -0
- package/src/components/Form/VTInput.vue +2 -5
- package/src/components/Form/VTInputDate.vue +602 -0
- package/src/components/Form/VTInputIcon.vue +3 -9
- package/src/components/Form/VTInputNumber.vue +198 -0
- package/src/components/Form/VTInputPassword.vue +14 -5
- package/src/components/Form/VTInputRange.vue +92 -0
- package/src/components/Form/VTLegend.vue +24 -0
- package/src/components/Form/VTTextarea.vue +2 -2
- package/src/components/Image/VTImage.vue +10 -10
- package/src/components/Listbox/VTListbox.vue +128 -9
- package/src/components/Listbox/VTListboxContent.vue +14 -1
- package/src/components/Listbox/VTListboxDivider.vue +21 -0
- package/src/components/Listbox/VTListboxGroup.vue +9 -0
- package/src/components/Listbox/VTListboxItem.vue +57 -15
- package/src/components/Listbox/VTListboxLabel.vue +5 -4
- package/src/components/Listbox/VTListboxList.vue +1 -6
- package/src/components/Listbox/VTListboxPlaceholder.vue +25 -0
- package/src/components/Listbox/VTListboxSearch.vue +12 -8
- package/src/components/Listbox/VTListboxTrigger.vue +87 -6
- package/src/components/Listbox/VTListboxTriggerHighlight.vue +204 -0
- package/src/components/Listbox/VTListboxViewport.vue +33 -0
- package/src/components/Popover/VTPopoverContent.vue +3 -3
- package/src/components/Popover/VTPopoverDivider.vue +1 -1
- package/src/components/Popover/VTPopoverItem.vue +6 -2
- package/src/components/ProgressBar/VTProgressBar.vue +35 -10
- package/src/components/ProgressBar/VTProgressBarIndicator.vue +53 -0
- package/src/components/ScrollShadows/VTScrollShadows.vue +76 -0
- package/src/components/Separator/VTSeparator.vue +13 -0
- package/src/components/Switch/VTSwitch.vue +61 -0
- package/src/components/Tabs/VTTab.vue +6 -5
- package/src/components/Tabs/VTTabGroup.vue +88 -9
- package/src/components/Tabs/VTTabPanel.vue +4 -5
- package/src/components/Toast/README.md +263 -0
- package/src/components/Toast/VTToast.vue +145 -0
- package/src/components/Toast/VTToastAction.vue +25 -0
- package/src/components/Toast/VTToastClose.vue +52 -0
- package/src/components/Toast/VTToastContent.vue +25 -0
- package/src/components/Toast/VTToastDescription.vue +36 -0
- package/src/components/Toast/VTToastIcon.vue +72 -0
- package/src/components/Toast/VTToastItem.vue +180 -0
- package/src/components/Toast/VTToastTitle.vue +34 -0
- package/src/components/Tooltip/VTTooltipTrigger.vue +3 -5
- package/src/components/Transitions/FadeInOut.vue +2 -2
- package/src/components/Utils/FloatingUi.vue +31 -13
- package/src/helpers/currency.js +21 -0
- package/src/utils/components.js +18 -0
- package/src/utils/images.js +31 -12
- package/src/components/Input/VTInput.vue +0 -82
- package/src/components/Input/VTInputDate.vue +0 -36
- package/src/components/Input/VTInputFile.vue +0 -60
- package/src/components/Input/VTInputUpload.vue +0 -54
- package/src/components/Modal/VTModal.vue +0 -69
- package/src/utils/genId.js +0 -13
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="inputNumberClassComputed">
|
|
3
|
+
<span v-if="format === 'currency'" class="grow-0 mr-1">{{
|
|
4
|
+
currencySymbol
|
|
5
|
+
}}</span>
|
|
6
|
+
<input
|
|
7
|
+
ref="inputEl"
|
|
8
|
+
:id="id"
|
|
9
|
+
:value="displayValue"
|
|
10
|
+
:data-testid="dataTestid"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
:inputmode="inputmode"
|
|
13
|
+
:placeholder="placeholder"
|
|
14
|
+
class="w-full bg-transparent outline-none"
|
|
15
|
+
@input="onInput"
|
|
16
|
+
@blur="onBlur"
|
|
17
|
+
@keydown="onKeyDown"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
import {
|
|
24
|
+
formControlMixin,
|
|
25
|
+
formControlStyleMixin,
|
|
26
|
+
} from '../../../mixins/form-control';
|
|
27
|
+
import { getCurrencySymbol } from '../../helpers/currency';
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
name: 'VTInputNumber',
|
|
31
|
+
|
|
32
|
+
mixins: [formControlMixin, formControlStyleMixin],
|
|
33
|
+
|
|
34
|
+
emits: ['update:modelValue', 'change', 'blur'],
|
|
35
|
+
|
|
36
|
+
props: {
|
|
37
|
+
id: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: null,
|
|
40
|
+
},
|
|
41
|
+
dataTestid: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: null,
|
|
44
|
+
},
|
|
45
|
+
currency: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: 'USD',
|
|
48
|
+
},
|
|
49
|
+
format: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: 'decimal',
|
|
52
|
+
},
|
|
53
|
+
locale: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: 'en-US',
|
|
56
|
+
},
|
|
57
|
+
placeholder: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: null,
|
|
60
|
+
},
|
|
61
|
+
inputmode: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: 'decimal',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
data() {
|
|
68
|
+
return {
|
|
69
|
+
isFocused: false,
|
|
70
|
+
localDisplay: null,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
computed: {
|
|
75
|
+
displayValue() {
|
|
76
|
+
// While typing, show the local display (with comma grouping but no fraction forcing)
|
|
77
|
+
if (this.isFocused && this.localDisplay !== null) {
|
|
78
|
+
return this.localDisplay;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// When not focused, show full formatting
|
|
82
|
+
if (this.modelValue === null || this.modelValue === undefined || this.modelValue === '') {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const num = Number(this.modelValue);
|
|
87
|
+
if (isNaN(num)) return '';
|
|
88
|
+
|
|
89
|
+
const options = this.format === 'currency'
|
|
90
|
+
? { minimumFractionDigits: 2, maximumFractionDigits: 3 }
|
|
91
|
+
: {};
|
|
92
|
+
|
|
93
|
+
return num.toLocaleString(this.locale, options);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
currencySymbol() {
|
|
97
|
+
return getCurrencySymbol({
|
|
98
|
+
locale: this.locale,
|
|
99
|
+
currency: this.currency,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
inputNumberClassComputed() {
|
|
104
|
+
if (this.format === 'currency') {
|
|
105
|
+
return [this.classComputed, 'flex justify-start'];
|
|
106
|
+
}
|
|
107
|
+
return this.classComputed;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
methods: {
|
|
112
|
+
unformat(str) {
|
|
113
|
+
if (!str || typeof str !== 'string') return null;
|
|
114
|
+
const cleaned = str.replace(/,/g, '');
|
|
115
|
+
if (cleaned === '' || cleaned === '.' || cleaned === '-') return null;
|
|
116
|
+
const num = this.format === 'integer'
|
|
117
|
+
? parseInt(cleaned, 10)
|
|
118
|
+
: parseFloat(cleaned);
|
|
119
|
+
return isNaN(num) ? null : num;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
formatWhileTyping(str) {
|
|
123
|
+
if (!str || typeof str !== 'string') return '';
|
|
124
|
+
const cleaned = str.replace(/,/g, '');
|
|
125
|
+
|
|
126
|
+
// Keep trailing dot or trailing decimal digits as-is while typing
|
|
127
|
+
const hasTrailingDot = cleaned.endsWith('.');
|
|
128
|
+
const parts = cleaned.split('.');
|
|
129
|
+
const intPart = parts[0];
|
|
130
|
+
const decPart = parts.length > 1 ? parts[1] : null;
|
|
131
|
+
|
|
132
|
+
// Format integer part with commas
|
|
133
|
+
const intNum = parseInt(intPart, 10);
|
|
134
|
+
if (isNaN(intNum)) return str;
|
|
135
|
+
const formatted = intNum.toLocaleString(this.locale);
|
|
136
|
+
|
|
137
|
+
if (hasTrailingDot) return formatted + '.';
|
|
138
|
+
if (decPart !== null) return formatted + '.' + decPart;
|
|
139
|
+
return formatted;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
onInput(e) {
|
|
143
|
+
const raw = e.target.value;
|
|
144
|
+
const cursorPos = e.target.selectionStart;
|
|
145
|
+
const oldLength = raw.length;
|
|
146
|
+
|
|
147
|
+
// Format with commas while typing
|
|
148
|
+
const formatted = this.formatWhileTyping(raw);
|
|
149
|
+
this.localDisplay = formatted;
|
|
150
|
+
|
|
151
|
+
// Emit the raw number
|
|
152
|
+
const num = this.unformat(raw);
|
|
153
|
+
this.$emit('update:modelValue', num);
|
|
154
|
+
this.$emit('change', num);
|
|
155
|
+
|
|
156
|
+
// Restore cursor position (adjust for added/removed commas)
|
|
157
|
+
this.$nextTick(() => {
|
|
158
|
+
const newLength = formatted.length;
|
|
159
|
+
const diff = newLength - oldLength;
|
|
160
|
+
const newPos = cursorPos + diff;
|
|
161
|
+
this.$refs.inputEl?.setSelectionRange(newPos, newPos);
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
onBlur() {
|
|
166
|
+
this.isFocused = false;
|
|
167
|
+
this.localDisplay = null;
|
|
168
|
+
this.$emit('blur');
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
onKeyDown(e) {
|
|
172
|
+
if (e.key === 'a' && (e.ctrlKey || e.metaKey)) return;
|
|
173
|
+
|
|
174
|
+
const allowed = /^[0-9.,\-]$/;
|
|
175
|
+
const navKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'];
|
|
176
|
+
|
|
177
|
+
if (!allowed.test(e.key) && !navKeys.includes(e.key)) {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Set focused state on first keydown
|
|
182
|
+
if (!this.isFocused) {
|
|
183
|
+
this.isFocused = true;
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
mounted() {
|
|
189
|
+
this.$refs.inputEl?.addEventListener('focus', () => {
|
|
190
|
+
this.isFocused = true;
|
|
191
|
+
// Initialize localDisplay from current formatted value
|
|
192
|
+
if (this.modelValue !== null && this.modelValue !== undefined && this.modelValue !== '') {
|
|
193
|
+
this.localDisplay = this.formatWhileTyping(String(this.modelValue));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
</script>
|
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
<div :class="classComputedWrapper">
|
|
3
3
|
<input
|
|
4
4
|
:class="classComputed"
|
|
5
|
-
:value="
|
|
5
|
+
:value="modelValue"
|
|
6
6
|
:disabled="disabled"
|
|
7
7
|
:type="reveal ? 'text' : 'password'"
|
|
8
|
-
v-bind="
|
|
9
|
-
v-on="listeners"
|
|
8
|
+
v-bind="attrsComputed"
|
|
10
9
|
/>
|
|
11
10
|
<div :class="classComputedWrapperIcon">
|
|
12
|
-
<VTButton
|
|
13
|
-
|
|
11
|
+
<VTButton
|
|
12
|
+
v-show="modelValue.length"
|
|
13
|
+
variant="icon"
|
|
14
|
+
@click="reveal = !reveal"
|
|
15
|
+
>
|
|
16
|
+
<component :is="iconVisibility" class="h-5 w-5 text-gray-500" />
|
|
14
17
|
</VTButton>
|
|
15
18
|
</div>
|
|
16
19
|
</div>
|
|
17
20
|
</template>
|
|
18
21
|
|
|
19
22
|
<script>
|
|
23
|
+
import { IconVisibilityOn, IconVisibilityOff } from '@veritree/icons';
|
|
20
24
|
import { formControlIconMixin } from '../../../mixins/form-control-icon';
|
|
21
25
|
|
|
22
26
|
export default {
|
|
@@ -24,6 +28,11 @@ export default {
|
|
|
24
28
|
|
|
25
29
|
mixins: [formControlIconMixin],
|
|
26
30
|
|
|
31
|
+
components: {
|
|
32
|
+
IconVisibilityOn,
|
|
33
|
+
IconVisibilityOff,
|
|
34
|
+
},
|
|
35
|
+
|
|
27
36
|
props: {
|
|
28
37
|
iconPlacement: {
|
|
29
38
|
type: String,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="range-slider" :style="style">
|
|
3
|
+
<input
|
|
4
|
+
:id="id"
|
|
5
|
+
v-model.number="computedValue"
|
|
6
|
+
:data-testid="dataTestid"
|
|
7
|
+
:min="min"
|
|
8
|
+
:max="max"
|
|
9
|
+
:step="step"
|
|
10
|
+
type="range"
|
|
11
|
+
@input="onInput"
|
|
12
|
+
/>
|
|
13
|
+
<div class="range-slider-track" aria-hidden />
|
|
14
|
+
<div ref="thumb" class="range-slider-thumb" aria-hidden />
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
export default {
|
|
20
|
+
name: 'VTInputRange',
|
|
21
|
+
|
|
22
|
+
model: {
|
|
23
|
+
prop: 'value',
|
|
24
|
+
event: 'input',
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
props: {
|
|
28
|
+
id: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: null,
|
|
31
|
+
},
|
|
32
|
+
dataTestid: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: null,
|
|
35
|
+
},
|
|
36
|
+
value: {
|
|
37
|
+
type: [Number, String],
|
|
38
|
+
default: 0,
|
|
39
|
+
},
|
|
40
|
+
min: {
|
|
41
|
+
type: Number,
|
|
42
|
+
default: 0,
|
|
43
|
+
},
|
|
44
|
+
max: {
|
|
45
|
+
type: Number,
|
|
46
|
+
default: 100,
|
|
47
|
+
},
|
|
48
|
+
step: {
|
|
49
|
+
type: [String, Number],
|
|
50
|
+
default: 1,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
data() {
|
|
55
|
+
return {
|
|
56
|
+
thumbSize: null,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
computed: {
|
|
61
|
+
computedValue: {
|
|
62
|
+
get() {
|
|
63
|
+
return Number(this.value);
|
|
64
|
+
},
|
|
65
|
+
set(value) {
|
|
66
|
+
this.$emit('input', value);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
percentage() {
|
|
71
|
+
return ((this.computedValue - this.min) / (this.max - this.min)) * 100;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
style() {
|
|
75
|
+
return {
|
|
76
|
+
'--value': `${this.percentage}%`,
|
|
77
|
+
'--thumb-left-position': `calc(${this.percentage}% - ${this.thumbSize * (this.percentage / 100)}px)`,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
mounted() {
|
|
83
|
+
this.thumbSize = 20;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
methods: {
|
|
87
|
+
onInput(e) {
|
|
88
|
+
this.computedValue = e.target.value;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
</script>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<legend
|
|
3
|
+
:class="[
|
|
4
|
+
headless
|
|
5
|
+
? 'legend'
|
|
6
|
+
: 'mb-4 block w-full text-sm font-medium text-gray-600',
|
|
7
|
+
]"
|
|
8
|
+
>
|
|
9
|
+
<slot />
|
|
10
|
+
</legend>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
export default {
|
|
15
|
+
name: 'VTLegend',
|
|
16
|
+
|
|
17
|
+
props: {
|
|
18
|
+
headless: {
|
|
19
|
+
type: Boolean,
|
|
20
|
+
default: false,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
</script>
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
headless
|
|
7
7
|
? null
|
|
8
8
|
: hasObjectFit
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
? `h-full w-full ${objectFitComputed}`
|
|
10
|
+
: null,
|
|
11
11
|
]"
|
|
12
12
|
v-bind="$attrs"
|
|
13
13
|
@load="onLoad"
|
|
@@ -60,12 +60,14 @@ export default {
|
|
|
60
60
|
return this.placeholder;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
if (this.
|
|
64
|
-
return this.
|
|
63
|
+
if (this.src) {
|
|
64
|
+
return this.src.includes('cloudfront.net/')
|
|
65
|
+
? handleImageResizing(this.src, this.$attrs.width)
|
|
66
|
+
: this.src;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
if (this.
|
|
68
|
-
return this.
|
|
69
|
+
if (this.cdnSrc) {
|
|
70
|
+
return handleImageResizing(this.cdnSrc, this.$attrs.width);
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -81,15 +83,13 @@ export default {
|
|
|
81
83
|
? this.objectFit === 'cover'
|
|
82
84
|
? 'object-cover'
|
|
83
85
|
: this.objectFit === 'contain'
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
? 'object-contain'
|
|
87
|
+
: null
|
|
86
88
|
: null;
|
|
87
89
|
},
|
|
88
90
|
},
|
|
89
91
|
|
|
90
92
|
methods: {
|
|
91
|
-
handleImageResizing,
|
|
92
|
-
|
|
93
93
|
onLoad() {
|
|
94
94
|
this.isLoaded = true;
|
|
95
95
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :class="
|
|
2
|
+
<div :class="classList">
|
|
3
3
|
<slot></slot>
|
|
4
4
|
</div>
|
|
5
5
|
</template>
|
|
@@ -16,16 +16,28 @@ export default {
|
|
|
16
16
|
provide() {
|
|
17
17
|
return {
|
|
18
18
|
apiListbox: () => {
|
|
19
|
+
/**
|
|
20
|
+
* This function registers a trigger by setting its value to the componentTrigger property of the current object.
|
|
21
|
+
* @param {VueComponent} trigger - The trigger to be registered.
|
|
22
|
+
*/
|
|
19
23
|
const registerTrigger = (trigger) => {
|
|
20
24
|
if (!trigger) return;
|
|
21
25
|
this.componentTrigger = trigger;
|
|
22
26
|
};
|
|
23
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Registers content to be used in the children components by setting its value to the componentContent property of the current object.
|
|
30
|
+
* @param {any} content - The content to be registered.
|
|
31
|
+
*/
|
|
24
32
|
const registerContent = (content) => {
|
|
25
33
|
if (!content) return;
|
|
26
34
|
this.componentContent = content;
|
|
27
35
|
};
|
|
28
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Registers search to be used in the children components by setting its value to the componentSearch property of the current object.
|
|
39
|
+
* @param {any} search - The search to be registered.
|
|
40
|
+
*/
|
|
29
41
|
const registerSearch = (search) => {
|
|
30
42
|
if (!search) return;
|
|
31
43
|
this.componentSearch = search;
|
|
@@ -52,8 +64,32 @@ export default {
|
|
|
52
64
|
};
|
|
53
65
|
|
|
54
66
|
const emit = (value) => {
|
|
55
|
-
this
|
|
56
|
-
|
|
67
|
+
this.valueComputed = value;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* pushValueToValueComputed handles pushing new selected values
|
|
72
|
+
* to an array that will replace the valueComputed. It is
|
|
73
|
+
* only used when the prop multiple is set to true.
|
|
74
|
+
*/
|
|
75
|
+
const pushValueToValueComputed = (value) => {
|
|
76
|
+
const newArr = Array.isArray(value) ? value : [value];
|
|
77
|
+
this.valueComputed = [...this.valueComputed, ...newArr];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* removeValueFromValueComputed handles removing aselected value
|
|
82
|
+
* from the valueComputed array by just filtering it out. It
|
|
83
|
+
* is also only used when the prop multiple is true.
|
|
84
|
+
*/
|
|
85
|
+
const removeValueFromValueComputed = (value) => {
|
|
86
|
+
this.valueComputed = this.valueComputed.filter((valueComputed) => {
|
|
87
|
+
if (typeof valueComputed === 'object') {
|
|
88
|
+
return JSON.stringify(valueComputed) !== JSON.stringify(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return valueComputed !== value;
|
|
92
|
+
});
|
|
57
93
|
};
|
|
58
94
|
|
|
59
95
|
return {
|
|
@@ -63,8 +99,10 @@ export default {
|
|
|
63
99
|
componentContent: this.componentContent,
|
|
64
100
|
componentSearch: this.componentSearch,
|
|
65
101
|
items: this.items,
|
|
102
|
+
multiple: this.multiple,
|
|
66
103
|
search: this.search,
|
|
67
|
-
|
|
104
|
+
version: this.version,
|
|
105
|
+
valueComputed: this.valueComputed,
|
|
68
106
|
registerTrigger,
|
|
69
107
|
registerContent,
|
|
70
108
|
registerSearch,
|
|
@@ -72,37 +110,87 @@ export default {
|
|
|
72
110
|
unregisterItem,
|
|
73
111
|
pushSearch,
|
|
74
112
|
clearSearch,
|
|
113
|
+
pushValueToValueComputed,
|
|
114
|
+
removeValueFromValueComputed,
|
|
75
115
|
emit,
|
|
76
116
|
};
|
|
77
117
|
},
|
|
78
118
|
};
|
|
79
119
|
},
|
|
80
120
|
|
|
121
|
+
emits: ['update:modelValue', 'change'],
|
|
122
|
+
|
|
81
123
|
props: {
|
|
82
|
-
|
|
83
|
-
type:
|
|
124
|
+
id: {
|
|
125
|
+
type: String,
|
|
84
126
|
default: null,
|
|
85
127
|
},
|
|
128
|
+
/**
|
|
129
|
+
* The value of the component. Can be a string, number, object, or array.
|
|
130
|
+
* @type {string|number|object|array}
|
|
131
|
+
* @default []
|
|
132
|
+
*/
|
|
133
|
+
modelValue: {
|
|
134
|
+
type: [String, Number, Object, Array],
|
|
135
|
+
default: () => [],
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* Determines whether the button will use its default atomic style (tailwind) or its default class
|
|
139
|
+
* @type {boolean}
|
|
140
|
+
* @values
|
|
141
|
+
* - true: The button will have no default style and can be fully customized with a custom class
|
|
142
|
+
* - false: The button will use its default atomic style (tailwind) and can be further customized with additional classes
|
|
143
|
+
* @default null
|
|
144
|
+
*/
|
|
86
145
|
headless: {
|
|
87
146
|
type: Boolean,
|
|
88
147
|
default: false,
|
|
89
148
|
},
|
|
149
|
+
/**
|
|
150
|
+
* The placement of the component relative to its trigger element.
|
|
151
|
+
* @type {string}
|
|
152
|
+
* @values 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end'
|
|
153
|
+
* @default 'bottom-start'
|
|
154
|
+
*/
|
|
90
155
|
placement: {
|
|
91
156
|
type: String,
|
|
92
157
|
default: 'bottom-start',
|
|
93
158
|
},
|
|
159
|
+
/**
|
|
160
|
+
* Determines whether the component should be aligned to the right of its trigger element.
|
|
161
|
+
* @type {boolean}
|
|
162
|
+
* @default false
|
|
163
|
+
*/
|
|
94
164
|
right: {
|
|
95
165
|
type: Boolean,
|
|
96
166
|
default: false,
|
|
97
167
|
},
|
|
168
|
+
/**
|
|
169
|
+
* Determines whether the component should allow multiple selections.
|
|
170
|
+
* @type {boolean}
|
|
171
|
+
* @default false
|
|
172
|
+
*/
|
|
173
|
+
multiple: {
|
|
174
|
+
type: Boolean,
|
|
175
|
+
default: false,
|
|
176
|
+
},
|
|
177
|
+
/**
|
|
178
|
+
* The version of the component to use.
|
|
179
|
+
* @type {number|string}
|
|
180
|
+
* @default '1'
|
|
181
|
+
*/
|
|
182
|
+
version: {
|
|
183
|
+
type: [Number, String],
|
|
184
|
+
default: '1',
|
|
185
|
+
},
|
|
98
186
|
},
|
|
99
187
|
|
|
100
188
|
data() {
|
|
101
189
|
return {
|
|
102
|
-
componentId: genId(),
|
|
103
190
|
componentSearch: null,
|
|
104
191
|
search: '',
|
|
105
192
|
items: [],
|
|
193
|
+
itemsChecked: [],
|
|
106
194
|
/**
|
|
107
195
|
* Explaining the need for the floatingUiMinWidth data
|
|
108
196
|
*
|
|
@@ -122,8 +210,39 @@ export default {
|
|
|
122
210
|
},
|
|
123
211
|
|
|
124
212
|
computed: {
|
|
125
|
-
|
|
126
|
-
return
|
|
213
|
+
componentId() {
|
|
214
|
+
return this.id || genId();
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
valueComputed: {
|
|
218
|
+
get() {
|
|
219
|
+
return this.modelValue;
|
|
220
|
+
},
|
|
221
|
+
set(newValue) {
|
|
222
|
+
this.$emit('update:modelValue', newValue);
|
|
223
|
+
this.$emit('change', newValue);
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
classList() {
|
|
228
|
+
const list = [];
|
|
229
|
+
|
|
230
|
+
// This will be refactor by moving the inline tailwind styling to its own file
|
|
231
|
+
if (this.headless || this.version === '2') {
|
|
232
|
+
// use semantic list
|
|
233
|
+
list.push('listbox');
|
|
234
|
+
list.push(`listbox--version-${this.version}`);
|
|
235
|
+
|
|
236
|
+
if (this.variant) {
|
|
237
|
+
list.push(`listbox--${this.variant}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (this.size) {
|
|
241
|
+
list.push(`listbox--${this.size}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return list;
|
|
127
246
|
},
|
|
128
247
|
},
|
|
129
248
|
};
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
:visible="visible"
|
|
5
5
|
:headless="headless"
|
|
6
6
|
:aria-activedescendant="activeDescedant"
|
|
7
|
-
:portal-class="$
|
|
7
|
+
:portal-class="`${$attrs.class || ''} ${portalClassList}`"
|
|
8
8
|
component="listbox"
|
|
9
9
|
role="listbox"
|
|
10
|
+
@shown="$emit('shown')"
|
|
11
|
+
@hidden="$emit('hidden')"
|
|
12
|
+
@before-show="$emit('before-show')"
|
|
10
13
|
>
|
|
11
14
|
<slot></slot>
|
|
12
15
|
</FloatingUi>
|
|
@@ -40,6 +43,16 @@ export default {
|
|
|
40
43
|
componentTrigger() {
|
|
41
44
|
return this.apiListbox().componentTrigger;
|
|
42
45
|
},
|
|
46
|
+
|
|
47
|
+
portalClassList() {
|
|
48
|
+
const staticClass = this.$attrs.class || this.$attrs.staticClass || '';
|
|
49
|
+
|
|
50
|
+
if (this.apiListbox().version === '2') {
|
|
51
|
+
return `${staticClass} listbox-content--version-2`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return staticClass;
|
|
55
|
+
},
|
|
43
56
|
},
|
|
44
57
|
|
|
45
58
|
mounted() {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="{
|
|
4
|
+
'listbox-divider': headless,
|
|
5
|
+
'mx-3 my-0.5 h-[1px] bg-gray-200': !headless,
|
|
6
|
+
}"
|
|
7
|
+
></div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
export default {
|
|
12
|
+
name: 'VTListboxDivider',
|
|
13
|
+
|
|
14
|
+
props: {
|
|
15
|
+
headless: {
|
|
16
|
+
type: Boolean,
|
|
17
|
+
default: false,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
</script>
|