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,310 @@
1
+ <template>
2
+ <InputSearch
3
+ v-bind="searchProps"
4
+ v-on="$attrs"
5
+ @update:modelValue="handleModelUpdate"
6
+ @update:searchInput="handleSearchInputUpdate"
7
+ @input="handleInputEvent"
8
+ @change="handleChangeEvent"
9
+ @focus="handleFocusEvent"
10
+ @blur="handleBlurEvent"
11
+ @select="handleSelectEvent"
12
+ @clear="handleClearEvent"
13
+ @search="handleSearchEvent"
14
+ >
15
+ <!-- Forward all named slots -->
16
+ <template v-for="(_, name) in $slots" :key="name" v-slot:[name]="slotProps">
17
+ <slot :name="name" v-bind="slotProps" />
18
+ </template>
19
+ </InputSearch>
20
+ </template>
21
+
22
+ <script setup>
23
+ import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, inject } from 'vue'
24
+ import { useRoute, useRouter } from 'vue-router'
25
+ import InputSearch from './InputSearch.vue'
26
+
27
+ const props = defineProps({
28
+ modelValue: { type: [String, Number, Object], default: '' },
29
+ label: { type: String, default: '' },
30
+ placeholder: { type: String, default: 'Search...' },
31
+ hint: { type: String, default: '' },
32
+ error: { type: String, default: '' },
33
+ errorMessages: { type: Array, default: () => [] },
34
+ disabled: { type: Boolean, default: false },
35
+ readonly: { type: Boolean, default: false },
36
+ required: { type: Boolean, default: false },
37
+ clearable: { type: Boolean, default: true },
38
+ loading: { type: Boolean, default: false },
39
+ hidePrependIcon: { type: Boolean, default: false },
40
+
41
+ // Items & Search
42
+ items: { type: Array, default: () => [] },
43
+ itemText: { type: [String, Function], default: 'text' },
44
+ itemValue: { type: [String, Function], default: 'value' },
45
+ returnObject: { type: Boolean, default: false },
46
+
47
+ // Search behavior
48
+ searchInput: { type: String, default: '' },
49
+ filterKeys: { type: Array, default: () => [] },
50
+ customFilter: { type: Function, default: null },
51
+ minSearchLength: { type: Number, default: 0 },
52
+ debounce: { type: Number, default: 300 },
53
+
54
+ // Appearance
55
+ variant: { type: String, default: 'outlined' },
56
+ size: { type: String, default: 'md' },
57
+ containerClass: { type: String, default: '' },
58
+ id: { type: String, default: '' },
59
+
60
+ // Menu
61
+ menuMaxHeight: { type: String, default: '300px' },
62
+ menuWidth: { type: String, default: 'auto' },
63
+
64
+ // URL Integration
65
+ syncWithUrl: { type: Boolean, default: false },
66
+ urlParam: { type: String, default: 'search' },
67
+ updateHistory: { type: Boolean, default: true }, // true = pushState, false = replaceState
68
+ preserveOtherParams: { type: Boolean, default: true },
69
+ })
70
+
71
+ const emit = defineEmits([
72
+ 'update:modelValue',
73
+ 'update:searchInput',
74
+ 'input',
75
+ 'change',
76
+ 'focus',
77
+ 'blur',
78
+ 'select',
79
+ 'clear',
80
+ 'search',
81
+ 'url-updated'
82
+ ])
83
+
84
+ const route = useRoute()
85
+ const router = useRouter()
86
+
87
+ const inputRef = ref(null)
88
+ const localValue = ref('')
89
+ let debounceTimeout = null
90
+
91
+ // Initialize local value from URL or props
92
+ const initializeValue = () => {
93
+ if (props.syncWithUrl && route.query[props.urlParam]) {
94
+ const urlValue = route.query[props.urlParam]
95
+ localValue.value = urlValue
96
+ emit('update:modelValue', urlValue)
97
+ emit('update:searchInput', urlValue)
98
+ } else if (props.modelValue) {
99
+ if (props.returnObject) {
100
+ localValue.value = getItemText(props.modelValue)
101
+ } else {
102
+ localValue.value = props.modelValue
103
+ }
104
+ } else if (props.searchInput) {
105
+ localValue.value = props.searchInput
106
+ }
107
+ }
108
+
109
+ // Watch for external modelValue changes
110
+ watch(() => props.modelValue, (newVal) => {
111
+ if (props.returnObject) {
112
+ localValue.value = newVal ? getItemText(newVal) : ''
113
+ } else {
114
+ localValue.value = newVal || ''
115
+ }
116
+ })
117
+
118
+ // Watch for searchInput changes
119
+ watch(() => props.searchInput, (newVal) => {
120
+ if (newVal !== localValue.value) {
121
+ localValue.value = newVal
122
+ }
123
+ })
124
+
125
+ // Watch URL changes if syncWithUrl is enabled
126
+ watch(() => route.query[props.urlParam], (newVal) => {
127
+ if (props.syncWithUrl && newVal !== localValue.value) {
128
+ localValue.value = newVal || ''
129
+ emit('update:modelValue', newVal || '')
130
+ emit('update:searchInput', newVal || '')
131
+ }
132
+ })
133
+
134
+ // Update URL with search value
135
+ const updateUrl = (value) => {
136
+ if (!props.syncWithUrl) return
137
+
138
+ const query = props.preserveOtherParams
139
+ ? { ...route.query }
140
+ : {}
141
+
142
+ if (value) {
143
+ query[props.urlParam] = value
144
+ } else {
145
+ delete query[props.urlParam]
146
+ }
147
+
148
+ const method = props.updateHistory ? 'push' : 'replace'
149
+
150
+ router[method]({
151
+ path: route.path,
152
+ query: query
153
+ }).catch(() => {}) // Ignore navigation duplicates
154
+
155
+ emit('url-updated', query)
156
+ }
157
+
158
+ // Get item text
159
+ const getItemText = (item) => {
160
+ if (!item) return ''
161
+ if (typeof item === 'string' || typeof item === 'number') return item.toString()
162
+ if (typeof props.itemText === 'function') return props.itemText(item)
163
+ return item[props.itemText] || ''
164
+ }
165
+
166
+ // Get item value
167
+ const getItemValue = (item) => {
168
+ if (!item) return ''
169
+ if (typeof item === 'string' || typeof item === 'number') return item
170
+ if (typeof props.itemValue === 'function') return props.itemValue(item)
171
+ return item[props.itemValue] || item
172
+ }
173
+
174
+ // Build search props to pass to InputSearch
175
+ const searchProps = computed(() => ({
176
+ modelValue: localValue.value,
177
+ label: props.label,
178
+ placeholder: props.placeholder,
179
+ hint: props.hint,
180
+ error: props.error,
181
+ errorMessages: props.errorMessages,
182
+ disabled: props.disabled,
183
+ readonly: props.readonly,
184
+ required: props.required,
185
+ clearable: props.clearable,
186
+ loading: props.loading,
187
+ hidePrependIcon: props.hidePrependIcon,
188
+ items: props.items,
189
+ itemText: props.itemText,
190
+ itemValue: props.itemValue,
191
+ returnObject: props.returnObject,
192
+ searchInput: props.searchInput,
193
+ filterKeys: props.filterKeys,
194
+ customFilter: props.customFilter,
195
+ minSearchLength: props.minSearchLength,
196
+ debounce: props.debounce,
197
+ variant: props.variant,
198
+ size: props.size,
199
+ containerClass: props.containerClass,
200
+ id: props.id,
201
+ menuMaxHeight: props.menuMaxHeight,
202
+ menuWidth: props.menuWidth,
203
+ }))
204
+
205
+ // Event handlers
206
+ const handleModelUpdate = (value) => {
207
+ localValue.value = value
208
+ emit('update:modelValue', value)
209
+
210
+ if (props.syncWithUrl) {
211
+ clearTimeout(debounceTimeout)
212
+ debounceTimeout = setTimeout(() => {
213
+ updateUrl(value)
214
+ }, props.debounce)
215
+ }
216
+ }
217
+
218
+ const handleSearchInputUpdate = (value) => {
219
+ emit('update:searchInput', value)
220
+ }
221
+
222
+ const handleInputEvent = (value) => {
223
+ emit('input', value)
224
+ }
225
+
226
+ const handleChangeEvent = (value) => {
227
+ emit('change', value)
228
+ }
229
+
230
+ const handleFocusEvent = (event) => {
231
+ emit('focus', event)
232
+ }
233
+
234
+ const handleBlurEvent = (event) => {
235
+ emit('blur', event)
236
+ }
237
+
238
+ const handleSelectEvent = (item) => {
239
+ const value = props.returnObject ? item : getItemValue(item)
240
+ const text = getItemText(item)
241
+
242
+ localValue.value = text
243
+ emit('update:modelValue', value)
244
+ emit('update:searchInput', text)
245
+ emit('select', item)
246
+
247
+ if (props.syncWithUrl) {
248
+ updateUrl(text)
249
+ }
250
+ }
251
+
252
+ const handleClearEvent = () => {
253
+ localValue.value = ''
254
+ emit('update:modelValue', '')
255
+ emit('update:searchInput', '')
256
+ emit('clear')
257
+
258
+ if (props.syncWithUrl) {
259
+ updateUrl('')
260
+ }
261
+ }
262
+
263
+ const handleSearchEvent = (value) => {
264
+ emit('search', value)
265
+ }
266
+
267
+ // Public methods
268
+ const focus = () => {
269
+ inputRef.value?.focus()
270
+ }
271
+
272
+ const clear = () => {
273
+ handleClearEvent()
274
+ }
275
+
276
+ const getUrlParams = () => {
277
+ return route.query[props.urlParam] || ''
278
+ }
279
+
280
+ const setUrlParam = (value) => {
281
+ updateUrl(value)
282
+ }
283
+
284
+ // Lifecycle
285
+ onMounted(() => {
286
+ initializeValue()
287
+ })
288
+
289
+ onBeforeUnmount(() => {
290
+ clearTimeout(debounceTimeout)
291
+ })
292
+
293
+ // Expose methods
294
+ defineExpose({
295
+ focus,
296
+ clear,
297
+ getUrlParams,
298
+ setUrlParam
299
+ })
300
+ </script>
301
+
302
+ <style scoped>
303
+ /* Remove default search input styling */
304
+ input[type="search"]::-webkit-search-decoration,
305
+ input[type="search"]::-webkit-search-cancel-button,
306
+ input[type="search"]::-webkit-search-results-button,
307
+ input[type="search"]::-webkit-search-results-decoration {
308
+ display: none;
309
+ }
310
+ </style>
@@ -0,0 +1,411 @@
1
+ <template>
2
+ <div
3
+ ref="containerRef"
4
+ :class="['relative', containerClass, width]"
5
+ v-on-click-outside="handleClickOutside"
6
+ >
7
+ <Base
8
+ :model-value="localValue"
9
+ :label="label"
10
+ v-on="$attrs"
11
+ :hint="hint"
12
+ :persistent-hint="!!hint"
13
+ :disabled="disabled"
14
+ :readonly="readonly"
15
+ :required="required"
16
+ :error="!!error || errorMessages.length > 0"
17
+ :error-messages="errorMessages"
18
+ :clearable="clearable && !loading"
19
+ :size="size"
20
+ :id="inputId"
21
+ :prepend="prepend"
22
+ :append="append"
23
+ :is-open="showResults"
24
+ @click:clear="clear"
25
+ :bg="bg"
26
+ :border="border"
27
+ :text-color="textColor"
28
+ :rounded="rounded"
29
+ :dir="computedDir"
30
+ :lang="computedLang"
31
+ :width="width"
32
+ >
33
+ <template #control="{ attrs, events }">
34
+ <input
35
+ ref="inputRef"
36
+ v-bind="attrs"
37
+ type="search"
38
+ :value="localValue"
39
+ :placeholder="isFocused && !localValue ? placeholder : ''"
40
+ :class="inputClasses"
41
+ @input="handleInput"
42
+ @focus="handleFocus"
43
+ @blur="handleBlur"
44
+ @keydown.enter="handleEnter"
45
+ @keydown.esc="handleEscape"
46
+ @keydown.down="handleArrowDown"
47
+ @keydown.up="handleArrowUp"
48
+ />
49
+ </template>
50
+
51
+ <!-- Loading spinner in append slot -->
52
+ <template v-if="loading" #append>
53
+ <svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
54
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
55
+ <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>
56
+ </svg>
57
+ </template>
58
+
59
+ <!-- Magnifying glass icon when not loading -->
60
+ <!-- <template v-else #append>
61
+ <HeroIcon name="magnifying-glass" :size="iconSizes[size]" />
62
+ </template> -->
63
+ </Base>
64
+
65
+ <!-- Results Dropdown - Styled like Select component -->
66
+ <Transition name="fade">
67
+ <div
68
+ v-if="showResults"
69
+ ref="menuRef"
70
+ :class="[
71
+ 'absolute left-0 right-0 mt-1 shadow-lg z-50',
72
+ bg,
73
+ border ? 'border ' + border : '',
74
+ rounded ? roundedMap[rounded] : '',
75
+ menuClass
76
+ ]"
77
+ :style="menuStyle"
78
+ >
79
+ <ul class="max-h-60 overflow-auto py-1">
80
+ <!-- Loading State -->
81
+ <li v-if="loading" class="px-4 py-2 text-input-text text-center text-sm">
82
+ <slot name="loading">
83
+ <div class="flex items-center gap-2 justify-center">
84
+ <svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
85
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
86
+ <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>
87
+ </svg>
88
+ Searching...
89
+ </div>
90
+ </slot>
91
+ </li>
92
+
93
+ <!-- No Data -->
94
+ <li v-else-if="filteredItems.length === 0" class="px-4 py-2 text-input-text text-center text-sm">
95
+ <slot name="noData">No results found</slot>
96
+ </li>
97
+
98
+ <!-- Results List -->
99
+ <li
100
+ v-else
101
+ v-for="(item, index) in filteredItems"
102
+ :key="getItemValue(item)"
103
+ @click="selectItem(item)"
104
+ @mouseenter="highlightedIndex = index"
105
+ :class="[
106
+ 'px-4 py-2 cursor-pointer transition flex items-center text-sm',
107
+ highlightedIndex === index
108
+ ? 'bg-gray-100 text-gray-900'
109
+ : 'hover:bg-input-hover',
110
+ itemClass,
111
+ itemHoverClass
112
+ ]"
113
+ role="option"
114
+ :aria-selected="highlightedIndex === index"
115
+ >
116
+ <slot name="item" :item="item" :index="index">
117
+ <span class="flex-1">{{ getItemText(item) }}</span>
118
+ </slot>
119
+ </li>
120
+ </ul>
121
+ </div>
122
+ </Transition>
123
+ </div>
124
+ </template>
125
+
126
+ <script setup>
127
+ import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, inject } from 'vue'
128
+ import Base from '../pgo/base/Base.vue'
129
+ import HeroIcon from '../pgo/HeroIcon.vue'
130
+ import { vOnClickOutside } from '@vueuse/components'
131
+ import { roundedMap, sizes, iconSizes } from '../../pgo-components/lib/componentConfig'
132
+
133
+ const props = defineProps({
134
+ modelValue: { type: [String, Number, Object], default: '' },
135
+ label: { type: [String, Object], default: '' },
136
+ placeholder: { type: String, default: 'Search...' },
137
+ hint: { type: String, default: '' },
138
+ error: { type: String, default: '' },
139
+ errorMessages: { type: Array, default: () => [] },
140
+ disabled: { type: Boolean, default: false },
141
+ readonly: { type: Boolean, default: false },
142
+ required: { type: Boolean, default: false },
143
+ clearable: { type: Boolean, default: true },
144
+ loading: { type: Boolean, default: false },
145
+ prepend: { type: String, default: '' },
146
+ append: { type: String, default: '' },
147
+
148
+ // Items & Search
149
+ items: { type: Array, default: () => [] },
150
+ itemText: { type: [String, Function], default: 'text' },
151
+ itemValue: { type: [String, Function], default: 'value' },
152
+ returnObject: { type: Boolean, default: false },
153
+
154
+ // Search behavior
155
+ searchInput: { type: String, default: '' },
156
+ filterKeys: { type: Array, default: () => [] },
157
+ customFilter: { type: Function, default: null },
158
+ minSearchLength: { type: Number, default: 0 },
159
+ debounce: { type: Number, default: 300 },
160
+
161
+ // Appearance
162
+ size: { type: String, default: 'md' },
163
+ rounded: { type: String, default: '' },
164
+ border: { type: String, default: 'border-input-border' },
165
+ textColor: { type: String, default: 'text-input-text' },
166
+ bg: { type: String, default: 'bg-input-background' },
167
+ containerClass: { type: String, default: '' },
168
+ menuClass: { type: String, default: '' },
169
+ menuStyle: { type: Object, default: () => ({}) },
170
+ itemClass: { type: String, default: '' },
171
+ itemHoverClass: { type: String, default: '' },
172
+ id: { type: String, default: '' },
173
+ width: { type: String, default: 'w-full' },
174
+
175
+ // RTL/Lang support
176
+ dir: { type: String, default: '' },
177
+ lang: { type: String, default: '' },
178
+ })
179
+
180
+ // Inject dir from parent Card (if exists)
181
+ const cardDir = inject('parentDir', '')
182
+ const cardLang = inject('parentLang', '')
183
+
184
+ // Use component's dir if provided, otherwise use card's dir
185
+ const computedDir = computed(() => props.dir || cardDir)
186
+ const computedLang = computed(() => props.lang || cardLang)
187
+
188
+ const emit = defineEmits([
189
+ 'update:modelValue',
190
+ 'update:searchInput',
191
+ 'input',
192
+ 'change',
193
+ 'focus',
194
+ 'blur',
195
+ 'select',
196
+ 'clear',
197
+ 'search'
198
+ ])
199
+
200
+ const containerRef = ref(null)
201
+ const inputRef = ref(null)
202
+ const menuRef = ref(null)
203
+ const localValue = ref('')
204
+ const isFocused = ref(false)
205
+ const showResults = ref(false)
206
+ const highlightedIndex = ref(-1)
207
+ let debounceTimeout = null
208
+
209
+ // Generate unique ID
210
+ const inputId = computed(() => props.id || `search-${Math.random().toString(36).substr(2, 9)}`)
211
+
212
+ // Initialize local value
213
+ watch(() => props.modelValue, (newVal) => {
214
+ if (!newVal) {
215
+ localValue.value = ''
216
+ return
217
+ }
218
+
219
+ if (props.returnObject) {
220
+ localValue.value = getItemText(newVal)
221
+ } else {
222
+ // Find the item by value and show its text
223
+ const item = props.items.find(i => getItemValue(i) === newVal)
224
+ localValue.value = item ? getItemText(item) : newVal.toString()
225
+ }
226
+ }, { immediate: true })
227
+
228
+ // Watch search input
229
+ watch(() => props.searchInput, (newVal) => {
230
+ localValue.value = newVal
231
+ })
232
+
233
+ // Filter items
234
+ const filteredItems = computed(() => {
235
+ const search = localValue.value?.toString().toLowerCase() || ''
236
+
237
+ if (search.length < props.minSearchLength) {
238
+ return []
239
+ }
240
+
241
+ if (!search && props.items.length > 0) {
242
+ return props.items
243
+ }
244
+
245
+ if (props.customFilter) {
246
+ return props.customFilter(props.items, search)
247
+ }
248
+
249
+ return props.items.filter(item => {
250
+ const searchKeys = props.filterKeys.length > 0
251
+ ? props.filterKeys
252
+ : [props.itemText]
253
+
254
+ return searchKeys.some(key => {
255
+ const value = typeof key === 'function'
256
+ ? key(item)
257
+ : item[key]
258
+ return value?.toString().toLowerCase().includes(search)
259
+ })
260
+ })
261
+ })
262
+
263
+ // Get item text
264
+ const getItemText = (item) => {
265
+ if (!item) return ''
266
+ if (typeof item === 'string' || typeof item === 'number') return item.toString()
267
+ if (typeof props.itemText === 'function') return props.itemText(item)
268
+ return item[props.itemText] || ''
269
+ }
270
+
271
+ // Get item value
272
+ const getItemValue = (item) => {
273
+ if (!item) return ''
274
+ if (typeof item === 'string' || typeof item === 'number') return item
275
+ if (typeof props.itemValue === 'function') return props.itemValue(item)
276
+ return item[props.itemValue] || item
277
+ }
278
+
279
+ // Input classes
280
+ const inputClasses = computed(() => [
281
+ 'w-full bg-transparent outline-none border-none',
282
+ 'placeholder:text-gray-400',
283
+ 'focus:outline-none'
284
+ ])
285
+
286
+ // Handlers
287
+ const handleInput = (e) => {
288
+ const value = e.target.value
289
+ localValue.value = value
290
+
291
+ clearTimeout(debounceTimeout)
292
+ debounceTimeout = setTimeout(() => {
293
+ emit('update:searchInput', value)
294
+ emit('input', value)
295
+ emit('search', value)
296
+ emit('update:modelValue', value)
297
+
298
+ if (value.length >= props.minSearchLength) {
299
+ showResults.value = true
300
+ } else {
301
+ showResults.value = false
302
+ }
303
+ }, props.debounce)
304
+ }
305
+
306
+ const handleFocus = (event) => {
307
+ isFocused.value = true
308
+ emit('focus', event)
309
+
310
+ if (localValue.value.length >= props.minSearchLength) {
311
+ showResults.value = true
312
+ }
313
+ }
314
+
315
+ const handleBlur = (event) => {
316
+ // Delay to allow click on menu items
317
+ setTimeout(() => {
318
+ isFocused.value = false
319
+ emit('blur', event)
320
+ }, 200)
321
+ }
322
+
323
+ const handleClickOutside = () => {
324
+ showResults.value = false
325
+ }
326
+
327
+ const handleEnter = (event) => {
328
+ if (highlightedIndex.value >= 0 && highlightedIndex.value < filteredItems.value.length) {
329
+ event.preventDefault()
330
+ selectItem(filteredItems.value[highlightedIndex.value])
331
+ }
332
+ }
333
+
334
+ const handleEscape = () => {
335
+ showResults.value = false
336
+ inputRef.value?.blur()
337
+ }
338
+
339
+ const handleArrowDown = (event) => {
340
+ event.preventDefault()
341
+ if (highlightedIndex.value < filteredItems.value.length - 1) {
342
+ highlightedIndex.value++
343
+ }
344
+ }
345
+
346
+ const handleArrowUp = (event) => {
347
+ event.preventDefault()
348
+ if (highlightedIndex.value > 0) {
349
+ highlightedIndex.value--
350
+ }
351
+ }
352
+
353
+ const selectItem = (item) => {
354
+ const value = props.returnObject ? item : getItemValue(item)
355
+ const text = getItemText(item)
356
+
357
+ localValue.value = text
358
+ emit('update:modelValue', value)
359
+ emit('update:searchInput', text)
360
+ emit('select', item)
361
+
362
+ showResults.value = false
363
+ highlightedIndex.value = -1
364
+ }
365
+
366
+ const clear = () => {
367
+ localValue.value = ''
368
+ emit('update:modelValue', '')
369
+ emit('update:searchInput', '')
370
+ emit('clear')
371
+ inputRef.value?.focus()
372
+ }
373
+
374
+ // Focus method
375
+ const focus = () => {
376
+ inputRef.value?.focus()
377
+ }
378
+
379
+ // Lifecycle
380
+ onMounted(() => {
381
+ // No need for resize/scroll listeners with absolute positioning
382
+ })
383
+
384
+ onBeforeUnmount(() => {
385
+ clearTimeout(debounceTimeout)
386
+ })
387
+
388
+ // Expose methods
389
+ defineExpose({ focus, clear })
390
+ </script>
391
+
392
+ <style scoped>
393
+ /* Remove default search input styling */
394
+ input[type="search"]::-webkit-search-decoration,
395
+ input[type="search"]::-webkit-search-cancel-button,
396
+ input[type="search"]::-webkit-search-results-button,
397
+ input[type="search"]::-webkit-search-results-decoration {
398
+ display: none;
399
+ }
400
+
401
+ /* Transition styles matching Select component */
402
+ .fade-enter-active,
403
+ .fade-leave-active {
404
+ transition: opacity 0.2s, transform 0.2s;
405
+ }
406
+
407
+ .fade-enter-from,
408
+ .fade-leave-to {
409
+ opacity: 0;
410
+ }
411
+ </style>