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,326 @@
1
+ <template>
2
+ <div :class="[containerClass, width]">
3
+ <Base
4
+ v-bind="$props"
5
+ :persistent-hint="!!hint"
6
+ :error="computedErrors.length > 0"
7
+ :is-open="dragActive"
8
+ :clearable="false"
9
+ >
10
+ <template #control="{ attrs }">
11
+ <div
12
+ :class="dropzoneClasses"
13
+ :aria-disabled="disabled"
14
+ role="button"
15
+ tabindex="0"
16
+ @click="openFileDialog"
17
+ @keydown.enter.prevent="openFileDialog"
18
+ @keydown.space.prevent="openFileDialog"
19
+ @dragenter.prevent="onDragEnter"
20
+ @dragover.prevent="onDragOver"
21
+ @dragleave.prevent="onDragLeave"
22
+ @drop.prevent="onDrop"
23
+ >
24
+ <input
25
+ :id="attrs.id"
26
+ ref="inputRef"
27
+ type="file"
28
+ :accept="accept"
29
+ :multiple="multiple"
30
+ class="hidden"
31
+ :disabled="disabled"
32
+ :readonly="attrs.readonly"
33
+ :required="attrs.required"
34
+ @change="onFileChange"
35
+ >
36
+
37
+ <div class="w-full flex items-center vts-ga-2 sm:gap-3 flex-wrap ">
38
+ <svg
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ viewBox="0 0 24 24"
41
+ fill="currentColor"
42
+ class="w-5 h-5 text-input-text flex-shrink-0"
43
+ >
44
+ <path d="M3 15a4 4 0 014-4h3v2H7a2 2 0 000 4h10a2 2 0 100-4h-3v-2h3a4 4 0 110 8H7a4 4 0 01-4-4z" />
45
+ <path d="M12 4a1 1 0 011 1v8a1 1 0 11-2 0V5a1 1 0 011-1z" />
46
+ <path d="M8.293 8.293a1 1 0 011.414 0L12 10.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" />
47
+ </svg>
48
+ <div class=" ">
49
+ <div class="text-sm text-input-text truncate">
50
+ <span class="font-medium">Click to upload</span> or drag and drop
51
+ </div>
52
+ <div class="text-xs text-input-text vts-mt-1 hidden sm:block">
53
+ {{ helpText }}
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <Button
58
+ type="button"
59
+ size="xs"
60
+ :disabled="disabled"
61
+ :rounded="rounded"
62
+ class="vts-px-3 py-1.5 text-sm text-primaryText "
63
+ >
64
+ Browse
65
+ </Button>
66
+ </div>
67
+ </template>
68
+ </Base>
69
+
70
+ <!-- Selected files list -->
71
+ <div
72
+ v-if="files.length"
73
+ class="vts-mt-2 vts-space-y-2"
74
+ >
75
+ <div
76
+ v-for="(f, idx) in files"
77
+ :key="f._id"
78
+ :class="['flex items-center vts-ga-3 vts-pa-2 vts-border bg-input-background', border, rounded ? roundedMap[rounded] : 'vts-rounded-md']"
79
+ >
80
+ <div
81
+ v-if="showPreview && isImage(f.file)"
82
+ class="w-10 h-10 overflow-hidden vts-rounded-md vts-border border-input-border"
83
+ >
84
+ <img
85
+ :src="f.preview"
86
+ alt="preview"
87
+ class="w-full h-full object-cover"
88
+ loading="lazy"
89
+ >
90
+ </div>
91
+ <div class="min-w-0">
92
+ <div class="text-sm text-input-text truncate">
93
+ {{ f.file.name }}
94
+ </div>
95
+ <div class="text-xs text-input-text opacity-80">
96
+ {{ formatSize(f.file.size) }}
97
+ </div>
98
+ </div>
99
+ <button
100
+ type="button"
101
+ class="text-input-text hover:text-input-focus"
102
+ @click="removeAt(idx)"
103
+ >
104
+ <svg
105
+ xmlns="http://www.w3.org/2000/svg"
106
+ fill="none"
107
+ viewBox="0 0 24 24"
108
+ stroke-width="1.5"
109
+ stroke="currentColor"
110
+ class="size-5"
111
+ >
112
+ <path
113
+ stroke-linecap="round"
114
+ stroke-linejoin="round"
115
+ d="M6 18 18 6M6 6l12 12"
116
+ />
117
+ </svg>
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </template>
123
+
124
+ <script setup>
125
+ import { ref, computed, watch } from 'vue'
126
+ import Base from '../base/Base.vue'
127
+ import { roundedMap } from '../../../pgo-components/lib/componentConfig'
128
+ import Button from '../Button.vue'
129
+
130
+ const props = defineProps({
131
+ modelValue: { type: Array, default: () => [] },
132
+ label: { type: [String, Object] },
133
+ hint: { type: String, default: '' },
134
+ disabled: { type: Boolean, default: false },
135
+ readonly: { type: Boolean, default: false },
136
+ required: { type: Boolean, default: false },
137
+ errorMessages: { type: Array, default: () => [] },
138
+ prepend: { type: String, default: '' },
139
+ append: { type: String, default: '' },
140
+ size: { type: String, },
141
+ rounded: { type: String },
142
+ border: { type: String, default: 'border-input-border' },
143
+ textColor: { type: String, default: 'text-input-text' },
144
+ bg: { type: String, default: 'bg-input-bg' },
145
+ width: { type: String, default: 'w-full' },
146
+
147
+ multiple: { type: Boolean, default: true },
148
+ accept: { type: String, default: '' },
149
+ maxSizeMB: { type: Number, default: 2 },
150
+ showPreview: { type: Boolean, default: true },
151
+ })
152
+
153
+ const emit = defineEmits(['update:modelValue', 'change', 'error', 'select', 'drop', 'remove'])
154
+
155
+ const inputRef = ref(null)
156
+ const dragActive = ref(true)
157
+ const containerClass = ref('')
158
+
159
+ const files = ref([]) // [{ file: File, preview: string|null, _id: string }]
160
+ const componentErrors = ref([])
161
+
162
+ const computedErrors = computed(() => {
163
+ const external = Array.isArray(props.errorMessages) ? props.errorMessages : []
164
+ const internal = Array.isArray(componentErrors.value) ? componentErrors.value : []
165
+ return [...external, ...internal]
166
+ })
167
+
168
+ watch(
169
+ () => props.modelValue,
170
+ (val) => {
171
+ // accept external v-model updates
172
+ files.value = toInternal(val)
173
+ },
174
+ { immediate: true }
175
+ )
176
+
177
+ function toInternal(list) {
178
+ const arr = Array.isArray(list) ? list : []
179
+ return arr.map((f) => ({ file: f.file ?? f, preview: f.preview ?? null, _id: genId() }))
180
+ }
181
+
182
+ function genId() {
183
+ return Math.random().toString(36).slice(2)
184
+ }
185
+
186
+ const dropzoneClasses = computed(() => [
187
+ 'w-full h-full flex items-center justify-between',
188
+ 'vts-ga-2 sm:vts-gap-3 vts-px-2 sm:vts-px-3',
189
+ 'cursor-pointer select-none transition',
190
+ 'bg-input-background',
191
+ ])
192
+
193
+ const helpText = computed(() => {
194
+ const acc = props.accept && props.accept.length ? props.accept : 'any file'
195
+ return `${props.multiple ? 'Upload multiple ' : 'Upload '}(${acc}) • Max ${props.maxSizeMB}MB each`
196
+ })
197
+
198
+ function openFileDialog() {
199
+ if (props.disabled || props.readonly) return
200
+ inputRef.value?.click()
201
+ }
202
+
203
+ function onFileChange(e) {
204
+ if (!e.target.files) return
205
+ const list = Array.from(e.target.files)
206
+ addFiles(list)
207
+ // reset input so same file can be selected again
208
+ e.target.value = ''
209
+ }
210
+
211
+ function onDragEnter() {
212
+ if (props.disabled) return
213
+ dragActive.value = true
214
+ }
215
+
216
+ function onDragOver() {
217
+ if (props.disabled) return
218
+ dragActive.value = true
219
+ }
220
+
221
+ function onDragLeave() {
222
+ dragActive.value = false
223
+ }
224
+
225
+ function onDrop(e) {
226
+ dragActive.value = false
227
+ if (props.disabled) return
228
+ const dt = e.dataTransfer
229
+ const list = dt ? Array.from(dt.files || []) : []
230
+ emit('drop', list)
231
+ addFiles(list)
232
+ }
233
+
234
+ function addFiles(list) {
235
+ componentErrors.value = []
236
+ const accepted = []
237
+ const maxBytes = props.maxSizeMB * 1024 * 1024
238
+ const acceptSet = parseAccept(props.accept)
239
+
240
+ for (const f of list) {
241
+ if (f.size > maxBytes) {
242
+ componentErrors.value.push(`${f.name} exceeds ${props.maxSizeMB}MB`)
243
+ emit('error', { type: 'size', file: f })
244
+ continue
245
+ }
246
+ if (acceptSet.size && !matchesAccept(f, acceptSet)) {
247
+ componentErrors.value.push(`${f.name} type not allowed`)
248
+ emit('error', { type: 'type', file: f })
249
+ continue
250
+ }
251
+ accepted.push(f)
252
+ }
253
+
254
+ if (!accepted.length) return
255
+
256
+ const prepared = accepted.map((f) => ({ file: f, preview: createPreview(f), _id: genId() }))
257
+ const next = props.multiple ? files.value.concat(prepared) : prepared.slice(0, 1)
258
+ files.value = next
259
+ emit('select', accepted)
260
+ emit('update:modelValue', next.map((x) => ({ file: x.file, preview: x.preview })))
261
+ emit('change', next.map((x) => x.file))
262
+ }
263
+
264
+ function createPreview(file) {
265
+ if (!isImage(file)) return null
266
+ try {
267
+ return URL.createObjectURL(file)
268
+ } catch {
269
+ return null
270
+ }
271
+ }
272
+
273
+ function isImage(file) {
274
+ return /^image\//.test(file.type)
275
+ }
276
+
277
+ function removeAt(index) {
278
+ const removed = files.value.splice(index, 1)
279
+ emit('remove', removed[0]?.file)
280
+ emit('update:modelValue', files.value.map((x) => ({ file: x.file, preview: x.preview })))
281
+ emit('change', files.value.map((x) => x.file))
282
+ }
283
+
284
+ function parseAccept(accept) {
285
+ const set = new Set()
286
+ if (!accept) return set
287
+ accept
288
+ .split(',')
289
+ .map((s) => s.trim())
290
+ .filter(Boolean)
291
+ .forEach((token) => set.add(token.toLowerCase()))
292
+ return set
293
+ }
294
+
295
+ function matchesAccept(file, set) {
296
+ if (!set || !set.size) return true
297
+ const type = file.type.toLowerCase()
298
+ const ext = file.name.toLowerCase().split('.').pop()
299
+ for (const token of set) {
300
+ if (token.includes('/')) {
301
+ // e.g., image/* or image/png
302
+ const [major, minor] = token.split('/')
303
+ const [ftMajor, ftMinor] = type.split('/')
304
+ if (minor === '*') {
305
+ if (major === ftMajor) return true
306
+ } else if (token === type) {
307
+ return true
308
+ }
309
+ } else if (token.startsWith('.')) {
310
+ if (ext && token.slice(1) === ext) return true
311
+ }
312
+ }
313
+ return false
314
+ }
315
+
316
+ function formatSize(bytes) {
317
+ if (bytes < 1024) return `${bytes} B`
318
+ const kb = bytes / 1024
319
+ if (kb < 1024) return `${kb.toFixed(1)} KB`
320
+ const mb = kb / 1024
321
+ return `${mb.toFixed(2)} MB`
322
+ }
323
+ </script>
324
+
325
+ <style scoped>
326
+ </style>
@@ -0,0 +1,243 @@
1
+ <template>
2
+ <div
3
+ ref="containerRef"
4
+ :class="['relative', containerClass, width]"
5
+ >
6
+ <Base
7
+ :model-value="modelValue"
8
+ :label="label"
9
+ :hint="hint"
10
+ :persistent-hint="!!hint"
11
+ :disabled="disabled"
12
+ :readonly="readonly"
13
+ :required="required"
14
+ :error="!!error || errorMessages.length > 0"
15
+ :error-messages="errorMessages"
16
+ :clearable="clearable && !loading"
17
+ :size="size"
18
+ :id="inputId"
19
+ :prepend="prepend"
20
+ :append="append"
21
+ :is-open="isFocused"
22
+ @click:clear="clear"
23
+ :bg="bg"
24
+ :border="border"
25
+ :text-color="textColor"
26
+ :rounded="rounded"
27
+ :dir="computedDir"
28
+ :lang="computedLang"
29
+ :width="width"
30
+ :rules="rules"
31
+ >
32
+ <template #control="{ attrs, events }">
33
+ <input
34
+ ref="inputRef"
35
+ v-bind="attrs"
36
+ v-on="events"
37
+ type="number"
38
+ :value="displayValue"
39
+ :placeholder="isFocused && (modelValue === '' || modelValue === null) ? placeholder : ''"
40
+ :class="inputClasses"
41
+ :min="min"
42
+ :max="max"
43
+ :step="step"
44
+ @input="handleInput"
45
+ @keydown.enter="handleEnter"
46
+ @focus="handleFocus"
47
+ @blur="handleBlur"
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 714 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
56
+ </svg>
57
+ </template>
58
+ </Base>
59
+ </div>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import { ref, computed, inject } from 'vue'
64
+ import Base from '../base/Base.vue'
65
+ import { roundedMap, sizes, iconSizes } from '../../pgo-components/lib/componentConfig'
66
+
67
+ const props = defineProps({
68
+ modelValue: { type: [String, Number, null], default: '' },
69
+ label: { type: [String, Object], default: '' },
70
+ placeholder: { type: String, default: '0' },
71
+ hint: { type: String, default: '' },
72
+ error: { type: String, default: '' },
73
+ errorMessages: { type: Array, default: () => [] },
74
+ disabled: { type: Boolean, default: false },
75
+ readonly: { type: Boolean, default: false },
76
+ required: { type: Boolean, default: false },
77
+ clearable: { type: Boolean, default: true },
78
+ loading: { type: Boolean, default: false },
79
+ prepend: { type: String, default: '' },
80
+ append: { type: String, default: '' },
81
+ rules: { type: Array, default: () => [] },
82
+
83
+ // number-specific
84
+ min: { type: [Number, String], default: undefined },
85
+ max: { type: [Number, String], default: undefined },
86
+ step: { type: [Number, String], default: '1' },
87
+ allowDecimal: { type: Boolean, default: true },
88
+
89
+ // Appearance
90
+ size: { type: String },
91
+ rounded: { type: String },
92
+ border: { type: String},
93
+ textColor: { type: String },
94
+ bg: { type: String },
95
+ containerClass: { type: String, default: '' },
96
+ id: { type: String, default: '' },
97
+ width: { type: String, default: 'w-full' },
98
+
99
+ // RTL/Lang support
100
+ dir: { type: String, default: '' },
101
+ lang: { type: String, default: '' },
102
+
103
+ // Props that might be passed but not used (to avoid warnings)
104
+ items: { type: Array, default: () => [] },
105
+ itemText: { type: String, default: 'text' },
106
+ itemValue: { type: String, default: 'value' },
107
+ })
108
+
109
+ const emit = defineEmits([
110
+ 'update:modelValue',
111
+ 'input',
112
+ 'change',
113
+ 'focus',
114
+ 'blur',
115
+ 'clear',
116
+ 'enter'
117
+ ])
118
+
119
+ // Inject dir from parent Card (if exists)
120
+ const cardDir = inject('parentDir', '')
121
+ const cardLang = inject('parentLang', '')
122
+
123
+ // Use component's dir if provided, otherwise use card's dir
124
+ const computedDir = computed(() => props.dir || cardDir)
125
+ const computedLang = computed(() => props.lang || cardLang)
126
+
127
+ const inputRef = ref<HTMLInputElement | null>(null)
128
+ const containerRef = ref(null)
129
+ const isFocused = ref(false)
130
+
131
+ // Generate unique ID
132
+ const inputId = computed(() => props.id || `input-${Math.random().toString(36).substr(2, 9)}`)
133
+
134
+ // Input classes (match TextField)
135
+ const inputClasses = computed(() => [
136
+ 'w-full bg-transparent outline-none border-none',
137
+ 'placeholder:text-gray-400',
138
+ 'focus:outline-none'
139
+ ])
140
+
141
+ // displayValue keeps caret-friendly representation
142
+ const displayValue = computed(() => {
143
+ if (props.modelValue === null || props.modelValue === undefined || props.modelValue === '') return ''
144
+ return String(props.modelValue)
145
+ })
146
+
147
+ /**
148
+ * Parse input value to number or empty string.
149
+ * - If field empty => ''
150
+ * - If allowDecimal false => parseInt
151
+ * - Otherwise parseFloat
152
+ * - If parsed is NaN => keep as string to allow user editing (do not force)
153
+ */
154
+ const parseValue = (val: string) => {
155
+ if (val === '') return ''
156
+ // Trim to avoid whitespace
157
+ const v = String(val).trim()
158
+ if (v === '') return ''
159
+ // If not allowed decimal and contains dot -> treat as parseInt of part before dot
160
+ if (!props.allowDecimal) {
161
+ const parsed = parseInt(v, 10)
162
+ return Number.isNaN(parsed) ? v : parsed
163
+ }
164
+ const parsedFloat = parseFloat(v)
165
+ return Number.isNaN(parsedFloat) ? v : parsedFloat
166
+ }
167
+
168
+ const clampValue = (num: number) => {
169
+ let n = num
170
+ if (props.min !== undefined && props.min !== null && props.min !== '') {
171
+ const minN = Number(props.min)
172
+ if (!Number.isNaN(minN)) n = Math.max(n, minN)
173
+ }
174
+ if (props.max !== undefined && props.max !== null && props.max !== '') {
175
+ const maxN = Number(props.max)
176
+ if (!Number.isNaN(maxN)) n = Math.min(n, maxN)
177
+ }
178
+ return n
179
+ }
180
+
181
+ const handleInput = (e: Event) => {
182
+ const target = e.target as HTMLInputElement
183
+ const raw = target.value
184
+
185
+ // Emit input for immediate reactive usage
186
+ emit('input', raw)
187
+
188
+ // Parse the value; if parse returns number, clamp to min/max
189
+ const parsed = parseValue(raw)
190
+
191
+ if (typeof parsed === 'number') {
192
+ const clamped = clampValue(parsed)
193
+ emit('update:modelValue', clamped)
194
+ // For consistency also emit change when user types a valid number
195
+ emit('change', clamped)
196
+ } else {
197
+ // Keep user's intermediate string (e.g. '-' or '.'), emit as-is to model so UI shows it
198
+ emit('update:modelValue', parsed)
199
+ }
200
+ }
201
+
202
+ const handleFocus = (event: Event) => {
203
+ isFocused.value = true
204
+ emit('focus', event)
205
+ }
206
+
207
+ const handleBlur = (event: Event) => {
208
+ isFocused.value = false
209
+ // On blur, if modelValue is numeric string, coerce to number and clamp
210
+ const mv = props.modelValue
211
+ if (typeof mv === 'string' && mv.trim() !== '') {
212
+ const coerced = parseValue(mv)
213
+ if (typeof coerced === 'number') {
214
+ const clamped = clampValue(coerced)
215
+ emit('update:modelValue', clamped)
216
+ emit('change', clamped)
217
+ }
218
+ }
219
+ emit('blur', event)
220
+ }
221
+
222
+ const handleEnter = (event: Event) => {
223
+ emit('enter', props.modelValue)
224
+ }
225
+
226
+ const clear = () => {
227
+ emit('update:modelValue', '')
228
+ emit('clear')
229
+ inputRef.value?.focus()
230
+ }
231
+
232
+ // Focus method
233
+ const focus = () => {
234
+ inputRef.value?.focus()
235
+ }
236
+
237
+ // Expose methods
238
+ defineExpose({ focus, clear })
239
+ </script>
240
+
241
+ <style scoped>
242
+ /* no additional styling; Base handles visual chrome */
243
+ </style>