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.
- package/.env +1 -0
- package/.env.production +1 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +3 -0
- package/BUTTON_GUIDE.md +257 -0
- package/README.md +49 -0
- package/THEME_REFERENCE.md +310 -0
- package/eslint.config.ts +27 -0
- package/index.html +13 -0
- package/package.json +85 -0
- package/public/favicon.ico +0 -0
- package/src/App.vue +368 -0
- package/src/assets/fonts/Faruma.ttf +0 -0
- package/src/components/examples/AppBarExample.vue +101 -0
- package/src/components/examples/AvatarExample.vue +47 -0
- package/src/components/examples/BannerExample.vue +287 -0
- package/src/components/examples/BaseInputExample.vue +25 -0
- package/src/components/examples/BreadcrumbExample.vue +53 -0
- package/src/components/examples/CardExample.vue +77 -0
- package/src/components/examples/ChipExample.vue +225 -0
- package/src/components/examples/DatePickerExample.vue +31 -0
- package/src/components/examples/DropdownExample.vue +84 -0
- package/src/components/examples/EditorExample.vue +200 -0
- package/src/components/examples/ExpansionPanelExample.vue +42 -0
- package/src/components/examples/FileUploadExample.vue +40 -0
- package/src/components/examples/FormExample.vue +121 -0
- package/src/components/examples/HugeTest.vue +8 -0
- package/src/components/examples/LayoutContainerExample.vue +80 -0
- package/src/components/examples/ModalExample.vue +82 -0
- package/src/components/examples/NavDrawerExample.vue +170 -0
- package/src/components/examples/NumberFieldExample.vue +145 -0
- package/src/components/examples/RadioButtonExample.vue +161 -0
- package/src/components/examples/SearchExample.vue +322 -0
- package/src/components/examples/SelectExample.vue +121 -0
- package/src/components/examples/StackedTableViewExample.vue +53 -0
- package/src/components/examples/TabExample.vue +336 -0
- package/src/components/examples/TableExample.vue +228 -0
- package/src/components/examples/TextFieldExample.vue +181 -0
- package/src/components/examples/TextareaExample.vue +173 -0
- package/src/components/examples/ThemeToggle.vue +50 -0
- package/src/components/examples/TimelineExample.vue +66 -0
- package/src/components/examples/TipTapEditorExample.vue +20 -0
- package/src/components/examples/TooltipExample.vue +53 -0
- package/src/components/examples/VueDatePickerShowcase.vue +214 -0
- package/src/components/examples/_DatePickerExample.vue +33 -0
- package/src/components/examples/__FormExample.vue +77 -0
- package/src/components/index.ts +25 -0
- package/src/components/pgo/AppBar.vue +347 -0
- package/src/components/pgo/Avatar.vue +139 -0
- package/src/components/pgo/Banner.vue +300 -0
- package/src/components/pgo/Breadcrumb.vue +101 -0
- package/src/components/pgo/Button.vue +171 -0
- package/src/components/pgo/Card.vue +178 -0
- package/src/components/pgo/ConfirmationModel.vue +32 -0
- package/src/components/pgo/DataTable.vue +845 -0
- package/src/components/pgo/DatePicker/CalendarPanel.vue +43 -0
- package/src/components/pgo/DatePicker/__DatePicker.vue +122 -0
- package/src/components/pgo/DatePicker/types.ts +11 -0
- package/src/components/pgo/DatePicker/useCalendar.ts +39 -0
- package/src/components/pgo/DatePicker/useDatePicker.ts +31 -0
- package/src/components/pgo/Deprecated/ToastContainer.vue +51 -0
- package/src/components/pgo/Deprecated/ToastItem.vue +55 -0
- package/src/components/pgo/Dropdown.vue +296 -0
- package/src/components/pgo/DropdownItem.vue +40 -0
- package/src/components/pgo/Editor.vue +511 -0
- package/src/components/pgo/ExpansionPanel.vue +185 -0
- package/src/components/pgo/Footer.vue +39 -0
- package/src/components/pgo/HeroIcon.vue +124 -0
- package/src/components/pgo/InputSearch.vue +194 -0
- package/src/components/pgo/LayoutContainer.vue +104 -0
- package/src/components/pgo/Main.vue +37 -0
- package/src/components/pgo/Modal.vue +273 -0
- package/src/components/pgo/NavDrawer.vue +127 -0
- package/src/components/pgo/NavDrawerItem.vue +161 -0
- package/src/components/pgo/NavigationDrawer.vue +849 -0
- package/src/components/pgo/OLDNavDrawer.vue +661 -0
- package/src/components/pgo/OldAppBar.vue +223 -0
- package/src/components/pgo/PApp.vue +102 -0
- package/src/components/pgo/Pagination.vue +242 -0
- package/src/components/pgo/Search copy.vue +310 -0
- package/src/components/pgo/Search.vue +411 -0
- package/src/components/pgo/StackedTableView.vue +167 -0
- package/src/components/pgo/Tab.vue +617 -0
- package/src/components/pgo/TestInput.vue +395 -0
- package/src/components/pgo/Timeline.vue +367 -0
- package/src/components/pgo/TimelineItem.vue +80 -0
- package/src/components/pgo/TipTapEditor.vue +315 -0
- package/src/components/pgo/Tooltip.NOTES.md +12 -0
- package/src/components/pgo/Tooltip.PROPS.md +21 -0
- package/src/components/pgo/Tooltip.vue +281 -0
- package/src/components/pgo/base/Base.vue +444 -0
- package/src/components/pgo/buttons/Chip.vue +324 -0
- package/src/components/pgo/buttons/ChipGroup.vue +224 -0
- package/src/components/pgo/buttons/Radio.vue +424 -0
- package/src/components/pgo/filters/FilterSection.vue +188 -0
- package/src/components/pgo/filters/Searchbar.vue +216 -0
- package/src/components/pgo/forms/DynamicForm.vue +45 -0
- package/src/components/pgo/forms/Form.vue +132 -0
- package/src/components/pgo/index.ts +15 -0
- package/src/components/pgo/inputs/Checkbox.vue +320 -0
- package/src/components/pgo/inputs/DatePicker.vue +395 -0
- package/src/components/pgo/inputs/FileUpload.vue +326 -0
- package/src/components/pgo/inputs/NumberField.vue +243 -0
- package/src/components/pgo/inputs/Radio.vue +162 -0
- package/src/components/pgo/inputs/RadioGroup.vue +188 -0
- package/src/components/pgo/inputs/Select.vue +535 -0
- package/src/components/pgo/inputs/TextField.vue +194 -0
- package/src/components/pgo/inputs/Textarea.vue +181 -0
- package/src/main.js +12 -0
- package/src/pgo-components/_index.js +31 -0
- package/src/pgo-components/assets/fonts/Faruma.ttf +0 -0
- package/src/pgo-components/assets/fonts/logo.png +0 -0
- package/src/pgo-components/composables/useTheme.js +10 -0
- package/src/pgo-components/directives/tooltip-directive.ts +393 -0
- package/src/pgo-components/index.js +96 -0
- package/src/pgo-components/lib/componentConfig.js +147 -0
- package/src/pgo-components/lib/core/composables/_useCalendar.ts +127 -0
- package/src/pgo-components/lib/core/composables/useDefaults.ts +15 -0
- package/src/pgo-components/lib/core/composables/useLanguageSelect.js +0 -0
- package/src/pgo-components/lib/core/composables/useRtl.ts +12 -0
- package/src/pgo-components/lib/core/defaults/createDefaults.ts +5 -0
- package/src/pgo-components/lib/core/defaults/defaults.ts +7 -0
- package/src/pgo-components/lib/core/rtl/rtl.ts +3 -0
- package/src/pgo-components/lib/core/rtl/setRtl.ts +19 -0
- package/src/pgo-components/lib/drawerState.ts +3 -0
- package/src/pgo-components/lib/i18n/defaultLables.js +71 -0
- package/src/pgo-components/lib/i18n/i18nPlugin.js +52 -0
- package/src/pgo-components/lib/i18n/useI18n.js +35 -0
- package/src/pgo-components/lib/index.ts +38 -0
- package/src/pgo-components/pages/Component.vue +7 -0
- package/src/pgo-components/pages/ComponentRenderer.vue +85 -0
- package/src/pgo-components/pages/Home.vue +130 -0
- package/src/pgo-components/pages/ListView.vue +370 -0
- package/src/pgo-components/pages/Page1.vue +296 -0
- package/src/pgo-components/pages/_Page1.vue +180 -0
- package/src/pgo-components/plugins/SnackBar.vue +251 -0
- package/src/pgo-components/plugins/SnackBarContainer.vue +53 -0
- package/src/pgo-components/plugins/SnackBarPlugin.ts +136 -0
- package/src/pgo-components/plugins/theme-plugin.js +114 -0
- package/src/pgo-components/plugins/types.ts +46 -0
- package/src/pgo-components/plugins/useSnackBar.js +11 -0
- package/src/pgo-components/plugins/useSnackBar.ts +21 -0
- package/src/pgo-components/plugins/validation-plugin.js +11 -0
- package/src/pgo-components/services/Entry.json +813 -0
- package/src/pgo-components/services/axios.js +54 -0
- package/src/pgo-components/services/data.json +90 -0
- package/src/pgo-components/services/person.json +260 -0
- package/src/pgo-components/services/toast.ts +44 -0
- package/src/pgo-components/styles/global.css +234 -0
- package/src/pgo-components/styles/reset.css +96 -0
- package/src/pgo-components/styles/tokens.css +18 -0
- package/src/pgo-components/styles/utilities/border-radius.css +57 -0
- package/src/pgo-components/styles/utilities/borders.css +85 -0
- package/src/pgo-components/styles/utilities/colors.css +38 -0
- package/src/pgo-components/styles/utilities/cursor.css +19 -0
- package/src/pgo-components/styles/utilities/display.css +78 -0
- package/src/pgo-components/styles/utilities/elevation.css +33 -0
- package/src/pgo-components/styles/utilities/flex.css +403 -0
- package/src/pgo-components/styles/utilities/float.css +41 -0
- package/src/pgo-components/styles/utilities/hover.css +9 -0
- package/src/pgo-components/styles/utilities/index.css +18 -0
- package/src/pgo-components/styles/utilities/opacity.css +27 -0
- package/src/pgo-components/styles/utilities/overflow.css +26 -0
- package/src/pgo-components/styles/utilities/palette.css +515 -0
- package/src/pgo-components/styles/utilities/position.css +14 -0
- package/src/pgo-components/styles/utilities/sizing.css +70 -0
- package/src/pgo-components/styles/utilities/spacing.css +578 -0
- package/src/pgo-components/styles/utilities/transitions.css +58 -0
- package/src/pgo-components/styles/utilities/typography.css +91 -0
- package/src/pgo-components/styles/utilities/z-index.css +11 -0
- package/src/pgo-components/tokens/index.js +337 -0
- package/src/router/index.js +88 -0
- package/src/shims-vue.d.ts +14 -0
- package/src/validations/validationRules.js +50 -0
- package/tailwind.config.js +73 -0
- package/test.php +5 -0
- package/tsconfig.json +25 -0
- package/ui +31 -0
- package/ui.pgo.mv.conf +18 -0
- 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>
|