pgo-uiux2 1.0.0

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 (180) hide show
  1. package/.env +1 -0
  2. package/.env.production +1 -0
  3. package/.prettierrc +13 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/BUTTON_GUIDE.md +257 -0
  6. package/README.md +49 -0
  7. package/THEME_REFERENCE.md +310 -0
  8. package/eslint.config.ts +27 -0
  9. package/index.html +13 -0
  10. package/package.json +85 -0
  11. package/public/favicon.ico +0 -0
  12. package/src/App.vue +368 -0
  13. package/src/assets/fonts/Faruma.ttf +0 -0
  14. package/src/components/examples/AppBarExample.vue +101 -0
  15. package/src/components/examples/AvatarExample.vue +47 -0
  16. package/src/components/examples/BannerExample.vue +287 -0
  17. package/src/components/examples/BaseInputExample.vue +25 -0
  18. package/src/components/examples/BreadcrumbExample.vue +53 -0
  19. package/src/components/examples/CardExample.vue +77 -0
  20. package/src/components/examples/ChipExample.vue +225 -0
  21. package/src/components/examples/DatePickerExample.vue +31 -0
  22. package/src/components/examples/DropdownExample.vue +84 -0
  23. package/src/components/examples/EditorExample.vue +200 -0
  24. package/src/components/examples/ExpansionPanelExample.vue +42 -0
  25. package/src/components/examples/FileUploadExample.vue +40 -0
  26. package/src/components/examples/FormExample.vue +121 -0
  27. package/src/components/examples/HugeTest.vue +8 -0
  28. package/src/components/examples/LayoutContainerExample.vue +80 -0
  29. package/src/components/examples/ModalExample.vue +82 -0
  30. package/src/components/examples/NavDrawerExample.vue +170 -0
  31. package/src/components/examples/NumberFieldExample.vue +145 -0
  32. package/src/components/examples/RadioButtonExample.vue +161 -0
  33. package/src/components/examples/SearchExample.vue +322 -0
  34. package/src/components/examples/SelectExample.vue +121 -0
  35. package/src/components/examples/StackedTableViewExample.vue +53 -0
  36. package/src/components/examples/TabExample.vue +336 -0
  37. package/src/components/examples/TableExample.vue +228 -0
  38. package/src/components/examples/TextFieldExample.vue +181 -0
  39. package/src/components/examples/TextareaExample.vue +173 -0
  40. package/src/components/examples/ThemeToggle.vue +50 -0
  41. package/src/components/examples/TimelineExample.vue +66 -0
  42. package/src/components/examples/TipTapEditorExample.vue +20 -0
  43. package/src/components/examples/TooltipExample.vue +53 -0
  44. package/src/components/examples/VueDatePickerShowcase.vue +214 -0
  45. package/src/components/examples/_DatePickerExample.vue +33 -0
  46. package/src/components/examples/__FormExample.vue +77 -0
  47. package/src/components/index.ts +25 -0
  48. package/src/components/pgo/AppBar.vue +347 -0
  49. package/src/components/pgo/Avatar.vue +139 -0
  50. package/src/components/pgo/Banner.vue +300 -0
  51. package/src/components/pgo/Breadcrumb.vue +101 -0
  52. package/src/components/pgo/Button.vue +171 -0
  53. package/src/components/pgo/Card.vue +178 -0
  54. package/src/components/pgo/ConfirmationModel.vue +32 -0
  55. package/src/components/pgo/DataTable.vue +845 -0
  56. package/src/components/pgo/DatePicker/CalendarPanel.vue +43 -0
  57. package/src/components/pgo/DatePicker/__DatePicker.vue +122 -0
  58. package/src/components/pgo/DatePicker/types.ts +11 -0
  59. package/src/components/pgo/DatePicker/useCalendar.ts +39 -0
  60. package/src/components/pgo/DatePicker/useDatePicker.ts +31 -0
  61. package/src/components/pgo/Deprecated/ToastContainer.vue +51 -0
  62. package/src/components/pgo/Deprecated/ToastItem.vue +55 -0
  63. package/src/components/pgo/Dropdown.vue +296 -0
  64. package/src/components/pgo/DropdownItem.vue +40 -0
  65. package/src/components/pgo/Editor.vue +511 -0
  66. package/src/components/pgo/ExpansionPanel.vue +185 -0
  67. package/src/components/pgo/Footer.vue +39 -0
  68. package/src/components/pgo/HeroIcon.vue +124 -0
  69. package/src/components/pgo/InputSearch.vue +194 -0
  70. package/src/components/pgo/LayoutContainer.vue +104 -0
  71. package/src/components/pgo/Main.vue +37 -0
  72. package/src/components/pgo/Modal.vue +273 -0
  73. package/src/components/pgo/NavDrawer.vue +127 -0
  74. package/src/components/pgo/NavDrawerItem.vue +161 -0
  75. package/src/components/pgo/NavigationDrawer.vue +849 -0
  76. package/src/components/pgo/OLDNavDrawer.vue +661 -0
  77. package/src/components/pgo/OldAppBar.vue +223 -0
  78. package/src/components/pgo/PApp.vue +102 -0
  79. package/src/components/pgo/Pagination.vue +242 -0
  80. package/src/components/pgo/Search copy.vue +310 -0
  81. package/src/components/pgo/Search.vue +411 -0
  82. package/src/components/pgo/StackedTableView.vue +167 -0
  83. package/src/components/pgo/Tab.vue +617 -0
  84. package/src/components/pgo/TestInput.vue +395 -0
  85. package/src/components/pgo/Timeline.vue +367 -0
  86. package/src/components/pgo/TimelineItem.vue +80 -0
  87. package/src/components/pgo/TipTapEditor.vue +315 -0
  88. package/src/components/pgo/Tooltip.NOTES.md +12 -0
  89. package/src/components/pgo/Tooltip.PROPS.md +21 -0
  90. package/src/components/pgo/Tooltip.vue +281 -0
  91. package/src/components/pgo/base/Base.vue +444 -0
  92. package/src/components/pgo/buttons/Chip.vue +324 -0
  93. package/src/components/pgo/buttons/ChipGroup.vue +224 -0
  94. package/src/components/pgo/buttons/Radio.vue +424 -0
  95. package/src/components/pgo/filters/FilterSection.vue +188 -0
  96. package/src/components/pgo/filters/Searchbar.vue +216 -0
  97. package/src/components/pgo/forms/DynamicForm.vue +45 -0
  98. package/src/components/pgo/forms/Form.vue +132 -0
  99. package/src/components/pgo/index.ts +15 -0
  100. package/src/components/pgo/inputs/Checkbox.vue +320 -0
  101. package/src/components/pgo/inputs/DatePicker.vue +395 -0
  102. package/src/components/pgo/inputs/FileUpload.vue +326 -0
  103. package/src/components/pgo/inputs/NumberField.vue +243 -0
  104. package/src/components/pgo/inputs/Radio.vue +162 -0
  105. package/src/components/pgo/inputs/RadioGroup.vue +188 -0
  106. package/src/components/pgo/inputs/Select.vue +535 -0
  107. package/src/components/pgo/inputs/TextField.vue +194 -0
  108. package/src/components/pgo/inputs/Textarea.vue +181 -0
  109. package/src/main.js +12 -0
  110. package/src/pgo-components/_index.js +31 -0
  111. package/src/pgo-components/assets/fonts/Faruma.ttf +0 -0
  112. package/src/pgo-components/assets/fonts/logo.png +0 -0
  113. package/src/pgo-components/composables/useTheme.js +10 -0
  114. package/src/pgo-components/directives/tooltip-directive.ts +393 -0
  115. package/src/pgo-components/index.js +96 -0
  116. package/src/pgo-components/lib/componentConfig.js +147 -0
  117. package/src/pgo-components/lib/core/composables/_useCalendar.ts +127 -0
  118. package/src/pgo-components/lib/core/composables/useDefaults.ts +15 -0
  119. package/src/pgo-components/lib/core/composables/useLanguageSelect.js +0 -0
  120. package/src/pgo-components/lib/core/composables/useRtl.ts +12 -0
  121. package/src/pgo-components/lib/core/defaults/createDefaults.ts +5 -0
  122. package/src/pgo-components/lib/core/defaults/defaults.ts +7 -0
  123. package/src/pgo-components/lib/core/rtl/rtl.ts +3 -0
  124. package/src/pgo-components/lib/core/rtl/setRtl.ts +19 -0
  125. package/src/pgo-components/lib/drawerState.ts +3 -0
  126. package/src/pgo-components/lib/i18n/defaultLables.js +71 -0
  127. package/src/pgo-components/lib/i18n/i18nPlugin.js +52 -0
  128. package/src/pgo-components/lib/i18n/useI18n.js +35 -0
  129. package/src/pgo-components/lib/index.ts +38 -0
  130. package/src/pgo-components/pages/Component.vue +7 -0
  131. package/src/pgo-components/pages/ComponentRenderer.vue +85 -0
  132. package/src/pgo-components/pages/Home.vue +130 -0
  133. package/src/pgo-components/pages/ListView.vue +370 -0
  134. package/src/pgo-components/pages/Page1.vue +296 -0
  135. package/src/pgo-components/pages/_Page1.vue +180 -0
  136. package/src/pgo-components/plugins/SnackBar.vue +251 -0
  137. package/src/pgo-components/plugins/SnackBarContainer.vue +53 -0
  138. package/src/pgo-components/plugins/SnackBarPlugin.ts +136 -0
  139. package/src/pgo-components/plugins/theme-plugin.js +114 -0
  140. package/src/pgo-components/plugins/types.ts +46 -0
  141. package/src/pgo-components/plugins/useSnackBar.js +11 -0
  142. package/src/pgo-components/plugins/useSnackBar.ts +21 -0
  143. package/src/pgo-components/plugins/validation-plugin.js +11 -0
  144. package/src/pgo-components/services/Entry.json +813 -0
  145. package/src/pgo-components/services/axios.js +54 -0
  146. package/src/pgo-components/services/data.json +90 -0
  147. package/src/pgo-components/services/person.json +260 -0
  148. package/src/pgo-components/services/toast.ts +44 -0
  149. package/src/pgo-components/styles/global.css +234 -0
  150. package/src/pgo-components/styles/reset.css +96 -0
  151. package/src/pgo-components/styles/tokens.css +18 -0
  152. package/src/pgo-components/styles/utilities/border-radius.css +57 -0
  153. package/src/pgo-components/styles/utilities/borders.css +85 -0
  154. package/src/pgo-components/styles/utilities/colors.css +38 -0
  155. package/src/pgo-components/styles/utilities/cursor.css +19 -0
  156. package/src/pgo-components/styles/utilities/display.css +78 -0
  157. package/src/pgo-components/styles/utilities/elevation.css +33 -0
  158. package/src/pgo-components/styles/utilities/flex.css +403 -0
  159. package/src/pgo-components/styles/utilities/float.css +41 -0
  160. package/src/pgo-components/styles/utilities/hover.css +9 -0
  161. package/src/pgo-components/styles/utilities/index.css +18 -0
  162. package/src/pgo-components/styles/utilities/opacity.css +27 -0
  163. package/src/pgo-components/styles/utilities/overflow.css +26 -0
  164. package/src/pgo-components/styles/utilities/palette.css +515 -0
  165. package/src/pgo-components/styles/utilities/position.css +14 -0
  166. package/src/pgo-components/styles/utilities/sizing.css +70 -0
  167. package/src/pgo-components/styles/utilities/spacing.css +578 -0
  168. package/src/pgo-components/styles/utilities/transitions.css +58 -0
  169. package/src/pgo-components/styles/utilities/typography.css +91 -0
  170. package/src/pgo-components/styles/utilities/z-index.css +11 -0
  171. package/src/pgo-components/tokens/index.js +337 -0
  172. package/src/router/index.js +88 -0
  173. package/src/shims-vue.d.ts +14 -0
  174. package/src/validations/validationRules.js +50 -0
  175. package/tailwind.config.js +73 -0
  176. package/test.php +5 -0
  177. package/tsconfig.json +25 -0
  178. package/ui +31 -0
  179. package/ui.pgo.mv.conf +18 -0
  180. package/vite.config.js +42 -0
@@ -0,0 +1,535 @@
1
+ <template>
2
+ <div v-on-click-outside="handleClickOutside" :class="['relative', width, margin]">
3
+ <Base
4
+ v-bind="$props"
5
+ :model-value="modelValue"
6
+ @update:model-value="handleUpdate"
7
+ v-on="$attrs"
8
+ :dir="computedDir"
9
+ :lang="computedLang"
10
+ :label="label"
11
+ :prepend="prepend"
12
+ :append="append"
13
+ :isOpen="isOpen"
14
+ :size="size"
15
+ @click="toggleOpen"
16
+ :disabled="disabled"
17
+ :readonly="readonly"
18
+ :required="required"
19
+ :bg="bg"
20
+ :border="border"
21
+ :text-color="textColor"
22
+ :rounded="rounded"
23
+ :width="width"
24
+ :hint="hint"
25
+ :persistent-hint="!!hint"
26
+ :rules="rules"
27
+ @click:clear="clear"
28
+ >
29
+ <template #control="{ attrs, events }">
30
+ <div :class="['flex', 'items-center', 'vts-ga-2', 'justify-between', 'w-full', 'h-full']">
31
+
32
+ <!-- Search input -->
33
+ <input
34
+ v-model="searchQuery"
35
+ :id="attrs.id"
36
+ :disabled="attrs.disabled"
37
+ :required="attrs.required"
38
+ :dir="attrs.dir"
39
+ type="text"
40
+ :placeholder="isFocused && !searchQuery ? placeholder : ''"
41
+ :class="inputClasses"
42
+ @focus="handleFocus"
43
+ @blur="handleBlur"
44
+ @keydown.enter="handleEnter"
45
+ />
46
+
47
+ <!-- Single select label -->
48
+ <div v-if="selectedLabel && !multiple" :class="['absolute', computedLang || language === 'dv' ? 'right-0' : 'left-0', ' px-3 truncate pointer-events-none']">
49
+ {{ selectedLabel }}
50
+ </div>
51
+ <!-- Multi-select badges - Show BEFORE input -->
52
+ <div v-if="multiple && selectedValues.length > 0" class="absolute right-8 group-hover/select:right-14 flex vts-ga-2 items-center flex-wrap">
53
+ <!-- Show all badges when less than 2 items -->
54
+ <template v-if="selectedValues.length < 2">
55
+ <div v-for="value in selectedValues" :key="value">
56
+ <div
57
+ :class="[
58
+ `vts-px-2 vts-py-0.5 text-xs flex items-center vts-ga-1 border vts-border-dashed`,
59
+ textColor,
60
+ border,
61
+ rounded ? roundedMap[rounded] : ''
62
+ ]"
63
+ >
64
+ <div class="">{{ getDisplayTextForValue(value) }}</div>
65
+ <HeroIcon @click.stop="removeValue(value)" name="x-mark" type="outline" size="10" color="text-current" />
66
+ </div>
67
+ </div>
68
+ </template>
69
+
70
+ <!-- Show count when 2 or more items -->
71
+ <template v-else>
72
+ <div
73
+ :class="[
74
+ `vts-px-2 vts-py-1 text-xs flex items-center vts-ga-1 border vts-border-dashed`,
75
+ textColor,
76
+ border,
77
+ rounded ? roundedMap[rounded] : ''
78
+ ]"
79
+ >
80
+ <div class="">{{ selectedValues.length }} selected</div>
81
+ </div>
82
+ </template>
83
+ </div>
84
+ </div>
85
+ </template>
86
+
87
+ <template #append>
88
+ <HeroIcon
89
+ v-if="!append && !loading"
90
+ name="chevron-down"
91
+ type="outline"
92
+ size="16"
93
+ :class="['transition-transform duration-200', isOpen ? 'rotate-180' : '', 'text-input-border']"
94
+ />
95
+ <!-- Loading spinner -->
96
+ <svg v-else-if="loading" class="animate-spin h-4 w-4 text-input-border" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
97
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
98
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
99
+ </svg>
100
+ <HeroIcon v-else :name="append" type="outline" size="16" class="text-input-border" />
101
+ </template>
102
+ </Base>
103
+ <Transition name="fade">
104
+ <div
105
+ v-if="isOpen"
106
+ :class="[
107
+ 'absolute left-0 right-0 mt-1 shadow-lg z-50',
108
+ bg ?? 'bg-white',
109
+ border ? 'border ' + border : 'border border-input-border',
110
+ rounded ? roundedMap[rounded] : '',
111
+ menuClass
112
+ ]"
113
+ :style="menuStyle"
114
+ >
115
+ <ul class="max-h-60 overflow-auto vts-py-1">
116
+ <!-- Loading state -->
117
+ <li v-if="loading" :class="['vts-px-4 vts-py-2 text-input-text text-center text-sm']">
118
+ <slot name="loading">Loading...</slot>
119
+ </li>
120
+
121
+ <!-- Error state -->
122
+ <li v-else-if="error" :class="['vts-px-4 vts-py-2 text-error text-center text-sm']">
123
+ <slot name="error" :error="error">{{ error }}</slot>
124
+ </li>
125
+
126
+ <!-- No items found -->
127
+ <li v-else-if="filteredItems.length === 0" :class="['vts-px-4 vts-py-2 text-input-text text-center text-sm']">
128
+ <slot name="no-data">No items found</slot>
129
+ </li>
130
+
131
+ <!-- Items list -->
132
+ <li
133
+ v-for="(item, index) in filteredItems"
134
+ :key="getItemValue(item)"
135
+ @mousedown="(e) => { e.preventDefault(); handleSelect(item); }"
136
+ :class="[
137
+ 'vts-px-4 vts-py-2 cursor-pointer transition flex items-center text-sm',
138
+ isSelected(getItemValue(item)) ? selectedItemClass || 'bg-input-focus-border' : itemClass || 'hover:bg-input-hover',
139
+ itemHoverClass
140
+ ]"
141
+ >
142
+ <slot name="item" :item="item" :index="index" :selected="isSelected(getItemValue(item))">
143
+ <div class="flex items-center vts-ga-2 w-full">
144
+ <!-- Checkbox for multi-select -->
145
+ <Checkbox
146
+ v-if="multiple"
147
+ :model-value="isSelected(getItemValue(item))"
148
+ :value="index"
149
+ :label="getItemTitle(item)"
150
+ :required="false"
151
+ :error="false"
152
+ @click.stop
153
+ />
154
+ <!-- <input v-if="multiple" type="checkbox" :checked="isSelected(getItemValue(item))" class="cursor-pointer accent-input-text" /> -->
155
+
156
+ <!-- Item content -->
157
+ <span class="flex-1">{{ getItemTitle(item) }}</span>
158
+
159
+ <!-- Check icon for single select -->
160
+ <svg
161
+ v-if="!multiple && isSelected(getItemValue(item))"
162
+ xmlns="http://www.w3.org/2000/svg"
163
+ fill="none"
164
+ viewBox="0 0 24 24"
165
+ stroke-width="2"
166
+ stroke="text-input-text"
167
+ class="w-4 h-4"
168
+ >
169
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
170
+ </svg>
171
+ </div>
172
+ </slot>
173
+ </li>
174
+ </ul>
175
+ </div>
176
+ </Transition>
177
+ </div>
178
+ </template>
179
+ <script setup>
180
+ import { ref, computed, watch, nextTick, inject, onMounted } from 'vue'
181
+ import { roundedMap } from '../../../pgo-components/lib/componentConfig'
182
+ import Base from '../base/Base.vue'
183
+ import HeroIcon from '../HeroIcon.vue'
184
+ import { vOnClickOutside } from '@vueuse/components'
185
+ import Checkbox from '../inputs/Checkbox.vue'
186
+
187
+ const { language } = inject('i18n')
188
+
189
+
190
+ const emit = defineEmits(['update:modelValue', 'change', 'select', 'clear', 'open', 'close', 'focus', 'blur', 'enter', 'search'])
191
+
192
+ const searchQuery = ref('')
193
+ const searchInputRef = ref(null)
194
+ const localValue = ref('')
195
+ const isFocused = ref(false)
196
+ const loading = ref(false)
197
+ const error = ref(null)
198
+ const fetchedItems = ref([])
199
+
200
+ const props = defineProps({
201
+ modelValue: {
202
+ type: [String, Number, Array],
203
+ default: ''
204
+ },
205
+ label: { type: [Object, String], default: '' },
206
+ dir: { type: String, default: '' },
207
+ placeholder: { type: String, default: 'Search...' },
208
+ prepend: { type: String, default: '' },
209
+ append: { type: String, default: '' },
210
+ items: { type: Array, default: () => [] },
211
+ url: { type: String, default: '' },
212
+ itemTitle: { type: [String], default: 'title' },
213
+ itemValue: { type: String, default: 'value' },
214
+ multiple: { type: Boolean, default: false },
215
+ searchable: { type: Boolean, default: false },
216
+ disabled: { type: Boolean, default: false },
217
+ errorMessages: { type: Array, default: () => [] },
218
+ rounded: { type: String, default: 'sm' },
219
+ size: { type: String },
220
+ border: { type: String },
221
+ textColor: { type: String },
222
+ menuClass: { type: String, default: '' },
223
+ menuStyle: { type: Object, default: () => ({}) },
224
+ itemClass: { type: String, default: '' },
225
+ itemHoverClass: { type: String, default: '' },
226
+ selectedItemClass: { type: String, default: '' },
227
+ bg: { type: String },
228
+ returnObject: { type: Boolean, default: false },
229
+ readonly: { type: Boolean, default: false },
230
+ required: { type: Boolean, default: false },
231
+ width: { type: String, default: 'w-full' },
232
+ margin: { type: String, default: '' },
233
+ hint: { type: String, default: '' },
234
+ lang: { type: String },
235
+ rules: { type: Array, default: () => [] },
236
+ eager: { type: Boolean, default: false },
237
+ cacheItems: { type: Boolean, default: true }
238
+ })
239
+
240
+ const cardDir = inject('parentDir', '')
241
+ const cardLang = inject('parentLang', '')
242
+ const formContext = inject('formContext', null)
243
+ const api = inject('api', null)
244
+
245
+ const computedDir = computed(() => props.dir || cardDir)
246
+ const computedLang = computed(() => props.lang || cardLang)
247
+
248
+ const isOpen = ref(false)
249
+
250
+ const computedItems = computed(() => {
251
+ if (props.url && fetchedItems.value.length > 0) {
252
+ return fetchedItems.value
253
+ }
254
+ return props.items
255
+ })
256
+
257
+ const fetchItems = async () => {
258
+ if (!props.url) return
259
+ if (props.cacheItems && fetchedItems.value.length > 0) return
260
+
261
+ loading.value = true
262
+ error.value = null
263
+
264
+ try {
265
+ let response
266
+
267
+ if (api) {
268
+ response = await api.get(props.url)
269
+ } else {
270
+ const res = await fetch(props.url)
271
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
272
+ response = { data: await res.json() }
273
+ }
274
+
275
+ const data = response.data
276
+
277
+ // Auto-detect array in response
278
+ let items = null
279
+
280
+ if (Array.isArray(data)) {
281
+ items = data
282
+ } else if (data.data && Array.isArray(data.data)) {
283
+ items = data.data
284
+ } else if (data.items && Array.isArray(data.items)) {
285
+ items = data.items
286
+ } else if (data.results && Array.isArray(data.results)) {
287
+ items = data.results
288
+ } else {
289
+ // Find first array property
290
+ for (const key in data) {
291
+ if (Array.isArray(data[key])) {
292
+ items = data[key]
293
+ break
294
+ }
295
+ }
296
+ }
297
+
298
+ if (!items) {
299
+ throw new Error('No array found in response')
300
+ }
301
+
302
+ fetchedItems.value = items
303
+
304
+ } catch (err) {
305
+ console.error('Error fetching items:', err)
306
+ error.value = err.message || 'Failed to load items'
307
+ } finally {
308
+ loading.value = false
309
+ }
310
+ }
311
+
312
+ onMounted(() => {
313
+ if (props.eager && props.url) {
314
+ fetchItems()
315
+ }
316
+
317
+ })
318
+
319
+ const handleFocus = (event) => {
320
+ isFocused.value = true
321
+ emit('focus', event)
322
+ }
323
+
324
+ const handleBlur = (event) => {
325
+ isFocused.value = false
326
+ emit('blur', event)
327
+ }
328
+
329
+ const handleUpdate = val => {
330
+ emit('update:modelValue', val)
331
+ }
332
+
333
+ const handleInput = e => {
334
+ emit('update:modelValue', e.target.value)
335
+ }
336
+
337
+ const handleEnter = (event) => {
338
+ emit('enter', props.modelValue)
339
+ emit('search', props.modelValue)
340
+ }
341
+
342
+ const clear = () => {
343
+ emit('update:modelValue', '')
344
+ emit('clear')
345
+ searchInputRef.value?.focus()
346
+ }
347
+
348
+ const inputClasses = computed(() => [
349
+ 'bg-transparent outline-none border-none flex-1 min-w-0 max-w-auto',
350
+ 'placeholder:text-gray-400',
351
+ 'focus:outline-none',
352
+ props.multiple && selectedValues.value.length > 0 ? 'flex-1 min-w-0' : '',
353
+ !props.searchable ? 'no-caret invisible w-px' : '',
354
+ !props.searchable ? 'cursor-pointer' : 'cursor-text'
355
+ ])
356
+
357
+ const selectedValues = computed(() => {
358
+ if (props.multiple) {
359
+ return Array.isArray(props.modelValue) ? props.modelValue : []
360
+ }
361
+ return props.modelValue ? [props.modelValue] : []
362
+ })
363
+
364
+ const selectedLabel = computed(() => {
365
+ const selected = computedItems.value.find(item => String(getItemValue(item)) === String(props.modelValue))
366
+ return selected ? getItemTitle(selected) : ''
367
+ })
368
+
369
+ const displayValue = computed(() => {
370
+ if (props.multiple) {
371
+ return selectedValues.value.length > 0 ? `${selectedValues.value.length} selected` : ''
372
+ }
373
+ return selectedLabel.value
374
+ })
375
+
376
+ const filteredItems = computed(() => {
377
+ if (!searchQuery.value) {
378
+ return computedItems.value
379
+ }
380
+ return computedItems.value.filter(item => {
381
+ const title = getItemTitle(item)
382
+ return title.toLowerCase().includes(searchQuery.value.toLowerCase())
383
+ })
384
+ })
385
+
386
+ const getItemTitle = item => {
387
+ if (!item) return ''
388
+ if (typeof item === 'string' || typeof item === 'number') return item.toString()
389
+ if (typeof props.itemTitle === 'function') return props.itemTitle(item)
390
+ return item[props.itemTitle] || ''
391
+ }
392
+
393
+ const getItemValue = item => {
394
+ if (!item) return ''
395
+ if (typeof item === 'string' || typeof item === 'number') return item
396
+ if (typeof props.itemValue === 'function') return props.itemValue(item)
397
+ return item[props.itemValue] || item
398
+ }
399
+
400
+ const getDisplayTextForValue = value => {
401
+ const item = computedItems.value.find(item => String(getItemValue(item)) === String(value))
402
+ return item ? getItemTitle(item) : value.toString()
403
+ }
404
+
405
+ const isSelected = value => {
406
+ return selectedValues.value.some(v => String(v) === String(value))
407
+ }
408
+
409
+ const handleSelect = item => {
410
+ const value = props.returnObject ? item : getItemValue(item)
411
+
412
+ if (props.multiple) {
413
+ let newValues = [...selectedValues.value]
414
+ const itemValue = getItemValue(item)
415
+
416
+ const index = newValues.findIndex(v => String(v) === String(itemValue))
417
+
418
+ if (index > -1) {
419
+ newValues.splice(index, 1)
420
+ } else {
421
+ newValues.push(itemValue)
422
+ }
423
+
424
+ emit('update:modelValue', newValues)
425
+ } else {
426
+ emit('update:modelValue', value)
427
+ emit('change', value)
428
+ emit('select', item)
429
+ isOpen.value = false
430
+ searchQuery.value = ''
431
+ }
432
+ }
433
+
434
+ const handleClickOutside = (event) => {
435
+ if (!props.multiple) {
436
+ close()
437
+ return
438
+ }
439
+ close()
440
+ }
441
+
442
+ const removeValue = value => {
443
+ if (props.multiple) {
444
+ const newValues = selectedValues.value.filter(v => String(v) !== String(value))
445
+ emit('update:modelValue', newValues)
446
+ emit('change', newValues)
447
+ }
448
+ }
449
+
450
+ const clearSelection = () => {
451
+ const newValue = props.multiple ? [] : ''
452
+ emit('update:modelValue', newValue)
453
+ emit('change', newValue)
454
+ emit('clear')
455
+ }
456
+
457
+ const toggleOpen = async () => {
458
+ if (!props.disabled) {
459
+ isOpen.value = true
460
+
461
+ if (isOpen.value) {
462
+ if (props.url && !props.eager) {
463
+ await fetchItems()
464
+ }
465
+
466
+ emit('open')
467
+ if (props.searchable) {
468
+ nextTick(() => {
469
+ searchInputRef.value?.focus()
470
+ })
471
+ }
472
+ } else {
473
+ emit('close')
474
+ searchQuery.value = ''
475
+ }
476
+ }
477
+ }
478
+
479
+ const close = () => {
480
+ isOpen.value = false
481
+ searchQuery.value = ''
482
+
483
+ if (formContext?.validateOn?.value) {
484
+ const validateOn = formContext.validateOn.value
485
+ const shouldValidate = validateOn === 'blur' || (Array.isArray(validateOn) && validateOn.includes('blur'))
486
+
487
+ if (shouldValidate) {
488
+ nextTick(() => {})
489
+ }
490
+ }
491
+
492
+ emit('close')
493
+ }
494
+
495
+ watch(isOpen, newVal => {
496
+ if (!newVal) {
497
+ searchQuery.value = ''
498
+ }
499
+ })
500
+
501
+ defineExpose({
502
+ close,
503
+ open: () => toggleOpen(),
504
+ clear: clearSelection,
505
+ refresh: fetchItems,
506
+ focus: () => {
507
+ if (props.searchable && isOpen.value) {
508
+ searchInputRef.value?.focus()
509
+ }
510
+ }
511
+ })
512
+ </script>
513
+
514
+ <style scoped>
515
+ .fade-enter-active,
516
+ .fade-leave-active {
517
+ transition: opacity 0.2s, transform 0.2s;
518
+ }
519
+
520
+ .fade-enter-from,
521
+ .fade-leave-to {
522
+ opacity: 0;
523
+ }
524
+ .no-caret {
525
+ caret-color: transparent;
526
+ }
527
+
528
+ @keyframes spin {
529
+ to { transform: rotate(360deg); }
530
+ }
531
+
532
+ .animate-spin {
533
+ animation: spin 1s linear infinite;
534
+ }
535
+ </style>