@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.
Files changed (116) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/index.js +105 -75
  3. package/mixins/floating-ui-content.js +17 -4
  4. package/mixins/floating-ui-item.js +31 -15
  5. package/mixins/floating-ui.js +142 -24
  6. package/mixins/form-control-icon.js +3 -3
  7. package/mixins/form-control.js +45 -20
  8. package/nuxt.js +38 -26
  9. package/package.json +17 -6
  10. package/src/components/Alert/VTAlert.vue +55 -14
  11. package/src/components/Avatar/VTAvatarImage.vue +6 -26
  12. package/src/components/Badge/VTBadge.vue +60 -0
  13. package/src/components/Badge/VTBadgeNew.vue +60 -0
  14. package/src/components/Breadcrumb/VTBreadcrumbItem.vue +11 -0
  15. package/src/components/Breadcrumb/VTBreadcrumbLink.vue +40 -0
  16. package/src/components/Breadcrumb/VTBreadcrumbList.vue +11 -0
  17. package/src/components/Breadcrumb/VTBreadcrumbRoot.vue +11 -0
  18. package/src/components/Breadcrumb/VTBreadcrumbSeparator.vue +19 -0
  19. package/src/components/Button/VTButton.vue +104 -56
  20. package/src/components/Carousel/VTCarousel.vue +69 -0
  21. package/src/components/Carousel/VTCarouselBackward.vue +36 -0
  22. package/src/components/Carousel/VTCarouselForward.vue +38 -0
  23. package/src/components/Carousel/VTCarouselTracker.vue +80 -0
  24. package/src/components/Checkbox/VTCheckbox.vue +134 -0
  25. package/src/components/Checkbox/VTCheckboxLabel.vue +3 -0
  26. package/src/components/Checkbox/VTCheckboxText.vue +20 -0
  27. package/src/components/Chip/VTChip.vue +29 -0
  28. package/src/components/Dialog/VTDialog.vue +59 -25
  29. package/src/components/Dialog/VTDialogClose.vue +3 -2
  30. package/src/components/Dialog/VTDialogContent.vue +29 -7
  31. package/src/components/Dialog/VTDialogFooter.vue +17 -2
  32. package/src/components/Dialog/VTDialogHeader.vue +2 -1
  33. package/src/components/Dialog/VTDialogMain.vue +5 -1
  34. package/src/components/Dialog/VTDialogOverlay.vue +5 -1
  35. package/src/components/Dialog/VTDialogTitle.vue +1 -1
  36. package/src/components/Disclosure/VTDisclosure.vue +2 -11
  37. package/src/components/Disclosure/VTDisclosureContent.vue +26 -52
  38. package/src/components/Disclosure/VTDisclosureDetails.vue +27 -2
  39. package/src/components/Disclosure/VTDisclosureHeader.vue +56 -89
  40. package/src/components/Disclosure/VTDisclosureIcon.vue +42 -31
  41. package/src/components/Divider/VTDivider.vue +9 -0
  42. package/src/components/Drawer/VTDrawer.vue +6 -15
  43. package/src/components/Drawer/VTDrawerClose.vue +5 -5
  44. package/src/components/Drawer/VTDrawerContent.vue +10 -10
  45. package/src/components/Drawer/VTDrawerFooter.vue +4 -4
  46. package/src/components/Drawer/VTDrawerHeader.vue +4 -4
  47. package/src/components/Drawer/VTDrawerMain.vue +5 -5
  48. package/src/components/Drawer/VTDrawerOverlay.vue +6 -6
  49. package/src/components/Drawer/VTDrawerTitle.vue +5 -5
  50. package/src/components/DropdownMenu/VTDropdownMenu.vue +0 -6
  51. package/src/components/DropdownMenu/VTDropdownMenuContent.vue +10 -1
  52. package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +7 -16
  53. package/src/components/DropdownMenu/VTDropdownMenuItem.vue +5 -1
  54. package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +1 -10
  55. package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +2 -4
  56. package/src/components/Form/VTFieldset.vue +5 -0
  57. package/src/components/Form/VTForm.vue +11 -0
  58. package/src/components/Form/VTFormCol.vue +20 -0
  59. package/src/components/Form/VTFormFeedback.vue +7 -1
  60. package/src/components/Form/VTFormGroup.vue +5 -7
  61. package/src/components/Form/VTFormLabel.vue +22 -0
  62. package/src/components/Form/VTFormLabelHelper.vue +22 -0
  63. package/src/components/Form/VTFormRow.vue +5 -0
  64. package/src/components/Form/VTInput.vue +2 -5
  65. package/src/components/Form/VTInputDate.vue +602 -0
  66. package/src/components/Form/VTInputIcon.vue +3 -9
  67. package/src/components/Form/VTInputNumber.vue +198 -0
  68. package/src/components/Form/VTInputPassword.vue +14 -5
  69. package/src/components/Form/VTInputRange.vue +92 -0
  70. package/src/components/Form/VTLegend.vue +24 -0
  71. package/src/components/Form/VTTextarea.vue +2 -2
  72. package/src/components/Image/VTImage.vue +10 -10
  73. package/src/components/Listbox/VTListbox.vue +128 -9
  74. package/src/components/Listbox/VTListboxContent.vue +14 -1
  75. package/src/components/Listbox/VTListboxDivider.vue +21 -0
  76. package/src/components/Listbox/VTListboxGroup.vue +9 -0
  77. package/src/components/Listbox/VTListboxItem.vue +57 -15
  78. package/src/components/Listbox/VTListboxLabel.vue +5 -4
  79. package/src/components/Listbox/VTListboxList.vue +1 -6
  80. package/src/components/Listbox/VTListboxPlaceholder.vue +25 -0
  81. package/src/components/Listbox/VTListboxSearch.vue +12 -8
  82. package/src/components/Listbox/VTListboxTrigger.vue +87 -6
  83. package/src/components/Listbox/VTListboxTriggerHighlight.vue +204 -0
  84. package/src/components/Listbox/VTListboxViewport.vue +33 -0
  85. package/src/components/Popover/VTPopoverContent.vue +3 -3
  86. package/src/components/Popover/VTPopoverDivider.vue +1 -1
  87. package/src/components/Popover/VTPopoverItem.vue +6 -2
  88. package/src/components/ProgressBar/VTProgressBar.vue +35 -10
  89. package/src/components/ProgressBar/VTProgressBarIndicator.vue +53 -0
  90. package/src/components/ScrollShadows/VTScrollShadows.vue +76 -0
  91. package/src/components/Separator/VTSeparator.vue +13 -0
  92. package/src/components/Switch/VTSwitch.vue +61 -0
  93. package/src/components/Tabs/VTTab.vue +6 -5
  94. package/src/components/Tabs/VTTabGroup.vue +88 -9
  95. package/src/components/Tabs/VTTabPanel.vue +4 -5
  96. package/src/components/Toast/README.md +263 -0
  97. package/src/components/Toast/VTToast.vue +145 -0
  98. package/src/components/Toast/VTToastAction.vue +25 -0
  99. package/src/components/Toast/VTToastClose.vue +52 -0
  100. package/src/components/Toast/VTToastContent.vue +25 -0
  101. package/src/components/Toast/VTToastDescription.vue +36 -0
  102. package/src/components/Toast/VTToastIcon.vue +72 -0
  103. package/src/components/Toast/VTToastItem.vue +180 -0
  104. package/src/components/Toast/VTToastTitle.vue +34 -0
  105. package/src/components/Tooltip/VTTooltipTrigger.vue +3 -5
  106. package/src/components/Transitions/FadeInOut.vue +2 -2
  107. package/src/components/Utils/FloatingUi.vue +31 -13
  108. package/src/helpers/currency.js +21 -0
  109. package/src/utils/components.js +18 -0
  110. package/src/utils/images.js +31 -12
  111. package/src/components/Input/VTInput.vue +0 -82
  112. package/src/components/Input/VTInputDate.vue +0 -36
  113. package/src/components/Input/VTInputFile.vue +0 -60
  114. package/src/components/Input/VTInputUpload.vue +0 -54
  115. package/src/components/Modal/VTModal.vue +0 -69
  116. 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="value"
5
+ :value="modelValue"
6
6
  :disabled="disabled"
7
7
  :type="reveal ? 'text' : 'password'"
8
- v-bind="$attrs"
9
- v-on="listeners"
8
+ v-bind="attrsComputed"
10
9
  />
11
10
  <div :class="classComputedWrapperIcon">
12
- <VTButton v-show="value.length" variant="icon" @click="reveal = !reveal">
13
- <component :is="iconVisibility" class="h-5 w-5" />
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>
@@ -1,9 +1,9 @@
1
1
  <template>
2
2
  <textarea
3
3
  :class="classComputed"
4
- :value="value"
4
+ :value="modelValue"
5
5
  :disabled="disabled"
6
- v-on="listeners"
6
+ v-bind="attrsComputed"
7
7
  />
8
8
  </template>
9
9
 
@@ -6,8 +6,8 @@
6
6
  headless
7
7
  ? null
8
8
  : hasObjectFit
9
- ? `h-full w-full ${objectFitComputed}`
10
- : null,
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.cdnSrc) {
64
- return this.handleImageResizing(this.cdnSrc, this.$attrs.width);
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.src) {
68
- return this.src;
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
- ? 'object-contain'
85
- : null
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="{ listbox: headless }">
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.$emit('input', value);
56
- this.$emit('change', value);
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
- selectedValue: this.value,
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
- value: {
83
- type: [String, Number, Object],
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
- id() {
126
- return `listbox-${this.componentId}`;
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="$vnode.data.staticClass"
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>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div role="group"><slot></slot></div>
3
+ </template>
4
+
5
+ <script>
6
+ export default {
7
+ name: 'VTListboxGroup',
8
+ };
9
+ </script>