@wishbone-media/spark 0.31.0 → 0.32.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wishbone-media/spark",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -88,7 +88,7 @@ const mergeClasses = (baseClasses, overrideClasses) => {
88
88
  }
89
89
 
90
90
  const computedButtonClass = computed(() => {
91
- let classes = ''
91
+ let classes = 'cursor-pointer'
92
92
  let roundingSize = ''
93
93
 
94
94
  switch (props.size) {
@@ -0,0 +1,275 @@
1
+ <template>
2
+ <div>
3
+ <label v-if="label" class="block text-sm font-medium text-gray-700 mb-2">{{ label }}</label>
4
+
5
+ <!-- Dashed variant -->
6
+ <div
7
+ v-if="variant === 'dashed'"
8
+ class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center transition-colors"
9
+ :class="{
10
+ 'border-blue-500 bg-blue-50': isDragging,
11
+ 'opacity-50 pointer-events-none': disabled || uploading,
12
+ }"
13
+ @dragover.prevent="onDragOver"
14
+ @dragenter.prevent="onDragOver"
15
+ @dragleave.prevent="onDragLeave"
16
+ @drop.prevent="onDrop"
17
+ >
18
+ <FormKit
19
+ ref="formkitFileRef"
20
+ type="file"
21
+ :name="name"
22
+ :accept="accept"
23
+ :disabled="disabled || uploading"
24
+ outer-class="hidden"
25
+ @input="handleFormKitInput"
26
+ />
27
+ <div v-if="!selectedFile">
28
+ <font-awesome-icon :icon="icon" class="text-4xl text-gray-400 mb-3" />
29
+ <p class="text-sm text-gray-600 mb-2">{{ dropText }}</p>
30
+ <SparkButton variant="secondary" @click="triggerFileDialog">
31
+ {{ browseText }}
32
+ </SparkButton>
33
+ </div>
34
+ <div v-else class="flex items-center justify-center gap-3">
35
+ <font-awesome-icon :icon="Icons.farFile" class="text-2xl text-gray-500" />
36
+ <span class="text-gray-700">{{ selectedFile.name }}</span>
37
+ <SparkButton variant="secondary" size="sm" @click="clear">
38
+ <font-awesome-icon :icon="Icons.farXmark" />
39
+ </SparkButton>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Minimal variant -->
44
+ <div
45
+ v-else
46
+ class="relative rounded-md transition-colors"
47
+ :class="{
48
+ 'ring-2 ring-blue-500 ring-offset-2 bg-blue-50': isDragging,
49
+ }"
50
+ @dragover.prevent="onDragOver"
51
+ @dragenter.prevent="onDragOver"
52
+ @dragleave.prevent="onDragLeave"
53
+ @drop.prevent="onDrop"
54
+ >
55
+ <FormKit
56
+ type="file"
57
+ :name="name"
58
+ :accept="accept"
59
+ :disabled="disabled || uploading"
60
+ @input="handleFormKitInput"
61
+ />
62
+ <div
63
+ v-if="isDragging"
64
+ class="absolute inset-0 flex items-center justify-center bg-blue-50/80 rounded-md pointer-events-none"
65
+ >
66
+ <span class="text-sm text-blue-600 font-medium">Drop file here</span>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Upload button -->
71
+ <div v-if="showUploadButton" class="flex justify-end mt-4">
72
+ <SparkButton size="lg" :disabled="!selectedFile || uploading" @click="upload">
73
+ <font-awesome-icon
74
+ v-if="uploading"
75
+ :icon="Icons.farSpinner"
76
+ class="mr-2 animate-spin"
77
+ />
78
+ {{ uploading ? uploadingText : uploadText }}
79
+ </SparkButton>
80
+ </div>
81
+
82
+ <p v-if="uploading && !showUploadButton" class="text-sm text-gray-500 mt-1">{{ uploadingText }}</p>
83
+ </div>
84
+ </template>
85
+
86
+ <script setup>
87
+ import { ref, inject } from 'vue'
88
+ import { Icons } from '@/plugins/fontawesome'
89
+ import { sparkNotificationService } from '@/composables/sparkNotificationService'
90
+ import SparkButton from './SparkButton.vue'
91
+
92
+ const props = defineProps({
93
+ uploadUrl: {
94
+ type: String,
95
+ required: true,
96
+ },
97
+ fieldName: {
98
+ type: String,
99
+ default: 'file',
100
+ },
101
+ extraFields: {
102
+ type: Object,
103
+ default: () => ({}),
104
+ },
105
+ extraParams: {
106
+ type: Object,
107
+ default: () => ({}),
108
+ },
109
+ accept: {
110
+ type: String,
111
+ default: '*/*',
112
+ },
113
+ disabled: {
114
+ type: Boolean,
115
+ default: false,
116
+ },
117
+ label: {
118
+ type: String,
119
+ default: '',
120
+ },
121
+ name: {
122
+ type: String,
123
+ default: 'file',
124
+ },
125
+ icon: {
126
+ type: Object,
127
+ default: () => Icons.farCloudArrowUp,
128
+ },
129
+ browseText: {
130
+ type: String,
131
+ default: 'Browse Files',
132
+ },
133
+ dropText: {
134
+ type: String,
135
+ default: 'Drag and drop your file here, or',
136
+ },
137
+ uploadText: {
138
+ type: String,
139
+ default: 'Upload',
140
+ },
141
+ uploadingText: {
142
+ type: String,
143
+ default: 'Uploading...',
144
+ },
145
+ showUploadButton: {
146
+ type: Boolean,
147
+ default: true,
148
+ },
149
+ variant: {
150
+ type: String,
151
+ default: 'dashed',
152
+ validator: (value) => ['dashed', 'minimal'].includes(value),
153
+ },
154
+ responseParser: {
155
+ type: Function,
156
+ default: (response) => response.data,
157
+ },
158
+ })
159
+
160
+ const emit = defineEmits(['upload-start', 'upload-success', 'upload-error', 'file-selected', 'file-cleared'])
161
+
162
+ const axios = inject('axios')
163
+ const selectedFile = ref(null)
164
+ const uploading = ref(false)
165
+ const isDragging = ref(false)
166
+ const formkitFileRef = ref(null)
167
+
168
+ function onDragOver() {
169
+ if (props.disabled || uploading.value) return
170
+ isDragging.value = true
171
+ }
172
+
173
+ function onDragLeave() {
174
+ isDragging.value = false
175
+ }
176
+
177
+ function onDrop(event) {
178
+ isDragging.value = false
179
+ if (props.disabled || uploading.value) return
180
+
181
+ const file = event.dataTransfer?.files?.[0]
182
+ if (!file) return
183
+
184
+ if (!isFileTypeAccepted(file)) {
185
+ sparkNotificationService.show({
186
+ type: 'danger',
187
+ message: 'File type not accepted.',
188
+ })
189
+ return
190
+ }
191
+
192
+ selectedFile.value = file
193
+ emit('file-selected', file)
194
+ }
195
+
196
+ function isFileTypeAccepted(file) {
197
+ const acceptTypes = props.accept.split(',').map((t) => t.trim())
198
+
199
+ for (const acceptType of acceptTypes) {
200
+ if (acceptType === '*/*') return true
201
+ if (acceptType.endsWith('/*')) {
202
+ const category = acceptType.slice(0, -2)
203
+ if (file.type.startsWith(category + '/')) return true
204
+ } else if (acceptType.startsWith('.')) {
205
+ if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) return true
206
+ } else {
207
+ if (file.type === acceptType) return true
208
+ }
209
+ }
210
+
211
+ return false
212
+ }
213
+
214
+ function handleFormKitInput(files) {
215
+ if (!files || files.length === 0) return
216
+ const file = files[0].file
217
+ if (!file) return
218
+ selectedFile.value = file
219
+ emit('file-selected', file)
220
+ }
221
+
222
+ function triggerFileDialog() {
223
+ const el = formkitFileRef.value?.$el
224
+ if (!el) return
225
+ const inputEl = el.querySelector('input[type="file"]')
226
+ || el.parentElement?.querySelector('input[type="file"]')
227
+ if (inputEl) {
228
+ inputEl.click()
229
+ }
230
+ }
231
+
232
+ function clear() {
233
+ selectedFile.value = null
234
+ emit('file-cleared')
235
+ }
236
+
237
+ async function upload() {
238
+ if (!selectedFile.value) return
239
+
240
+ uploading.value = true
241
+ emit('upload-start')
242
+
243
+ try {
244
+ const formData = new FormData()
245
+ formData.append(props.fieldName, selectedFile.value)
246
+
247
+ Object.entries(props.extraFields).forEach(([key, value]) => {
248
+ formData.append(key, value)
249
+ })
250
+
251
+ const response = await axios.post(props.uploadUrl, formData, {
252
+ headers: { 'Content-Type': 'multipart/form-data' },
253
+ params: props.extraParams,
254
+ })
255
+
256
+ const result = props.responseParser(response)
257
+ emit('upload-success', result)
258
+ } catch (error) {
259
+ emit('upload-error', error)
260
+ sparkNotificationService.show({
261
+ type: 'danger',
262
+ message: error.response?.data?.message || 'Failed to upload file.',
263
+ })
264
+ } finally {
265
+ uploading.value = false
266
+ }
267
+ }
268
+
269
+ defineExpose({
270
+ upload,
271
+ clear,
272
+ selectedFile,
273
+ uploading,
274
+ })
275
+ </script>
@@ -6,6 +6,7 @@ export { default as SparkBrandSelector } from './SparkBrandSelector.vue'
6
6
  export { default as SparkButton } from './SparkButton.vue'
7
7
  export { default as SparkButtonGroup } from './SparkButtonGroup.vue'
8
8
  export { default as SparkCard } from './SparkCard.vue'
9
+ export { default as SparkFileDragUpload } from './SparkFileDragUpload.vue'
9
10
  export { default as SparkImageUpload } from './SparkImageUpload.vue'
10
11
  export { default as SparkModalContainer } from './SparkModalContainer.vue'
11
12
  export { default as SparkModalDialog } from './SparkModalDialog.vue'