@pyreweb/fabric 1.2.6

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 (210) hide show
  1. package/README.md +119 -0
  2. package/dist/fabric.cjs.js +18109 -0
  3. package/dist/fabric.css +2180 -0
  4. package/dist/fabric.esm.js +18062 -0
  5. package/dist/fabric.min.js +18112 -0
  6. package/dist/types/components/atoms/FAvatar/FAvatar.test.d.ts +1 -0
  7. package/dist/types/components/atoms/FBadge/FBadge.test.d.ts +1 -0
  8. package/dist/types/components/atoms/FButton/FButton.test.d.ts +1 -0
  9. package/dist/types/components/atoms/FCheckbox/FCheckbox.test.d.ts +1 -0
  10. package/dist/types/components/atoms/FDivider/FDivider.test.d.ts +1 -0
  11. package/dist/types/components/atoms/FIcon/FIcon.test.d.ts +1 -0
  12. package/dist/types/components/atoms/FInput/FInput.test.d.ts +1 -0
  13. package/dist/types/components/atoms/FLoader/FLoader.test.d.ts +1 -0
  14. package/dist/types/components/atoms/FRadio/FRadio.test.d.ts +1 -0
  15. package/dist/types/components/atoms/FTextarea/FTextarea.test.d.ts +1 -0
  16. package/dist/types/components/atoms/FToggle/FToggle.test.d.ts +1 -0
  17. package/dist/types/components/atoms/FTypography/FTypography.test.d.ts +1 -0
  18. package/dist/types/components/atoms/index.d.ts +13 -0
  19. package/dist/types/components/molecules/FAccordionItem/FAccordionItem.test.d.ts +1 -0
  20. package/dist/types/components/molecules/FAlert/FAlert.test.d.ts +1 -0
  21. package/dist/types/components/molecules/FBreadcrumb/FBreadcrumb.test.d.ts +1 -0
  22. package/dist/types/components/molecules/FButtonGroup/FButtonGroup.test.d.ts +1 -0
  23. package/dist/types/components/molecules/FCard/FCard.test.d.ts +1 -0
  24. package/dist/types/components/molecules/FDatePicker/FDatePicker.test.d.ts +1 -0
  25. package/dist/types/components/molecules/FEmptyState/FEmptyState.test.d.ts +1 -0
  26. package/dist/types/components/molecules/FFilePreview/FFilePreview.test.d.ts +1 -0
  27. package/dist/types/components/molecules/FFormField/FFormField.test.d.ts +1 -0
  28. package/dist/types/components/molecules/FListItem/FListItem.test.d.ts +1 -0
  29. package/dist/types/components/molecules/FPagination/FPagination.test.d.ts +1 -0
  30. package/dist/types/components/molecules/FSearchBar/FSearchBar.test.d.ts +1 -0
  31. package/dist/types/components/molecules/FSelect/FSelect.test.d.ts +1 -0
  32. package/dist/types/components/molecules/FStatCard/FStatCard.test.d.ts +1 -0
  33. package/dist/types/components/molecules/FTabs/FTabs.test.d.ts +1 -0
  34. package/dist/types/components/molecules/FToast/FToast.test.d.ts +1 -0
  35. package/dist/types/components/molecules/index.d.ts +18 -0
  36. package/dist/types/components/organisms/FActivityFeed/FActivityFeed.test.d.ts +1 -0
  37. package/dist/types/components/organisms/FDataTable/FDataTable.test.d.ts +1 -0
  38. package/dist/types/components/organisms/FDrawer/FDrawer.test.d.ts +1 -0
  39. package/dist/types/components/organisms/FFileUpload/FFileUpload.test.d.ts +1 -0
  40. package/dist/types/components/organisms/FFilterSidebar/FFilterSidebar.test.d.ts +1 -0
  41. package/dist/types/components/organisms/FForm/FForm.test.d.ts +1 -0
  42. package/dist/types/components/organisms/FModal/FModal.test.d.ts +1 -0
  43. package/dist/types/components/organisms/FNavigationSidebar/FNavigationSidebar.test.d.ts +1 -0
  44. package/dist/types/components/organisms/FOnboardingStepper/FOnboardingStepper.test.d.ts +1 -0
  45. package/dist/types/components/organisms/FOnboardingStepper/FStepperProgress.test.d.ts +1 -0
  46. package/dist/types/components/organisms/FPageHeader/FPageHeader.test.d.ts +1 -0
  47. package/dist/types/components/organisms/FProfileSection/FProfileSection.test.d.ts +1 -0
  48. package/dist/types/components/organisms/FToastProvider/FToastProvider.test.d.ts +1 -0
  49. package/dist/types/components/organisms/FUserMenu/FUserMenu.test.d.ts +1 -0
  50. package/dist/types/components/organisms/index.d.ts +14 -0
  51. package/dist/types/components/utils/FThemeProvider.test.d.ts +1 -0
  52. package/dist/types/components/utils/index.d.ts +2 -0
  53. package/dist/types/components.d.ts +602 -0
  54. package/dist/types/composables/index.d.ts +12 -0
  55. package/dist/types/composables/useDataTableState.d.ts +106 -0
  56. package/dist/types/composables/useDataTableState.test.d.ts +1 -0
  57. package/dist/types/composables/useFormValidation.d.ts +49 -0
  58. package/dist/types/composables/useFormValidation.test.d.ts +1 -0
  59. package/dist/types/composables/useSidebarState.d.ts +65 -0
  60. package/dist/types/composables/useSidebarState.test.d.ts +1 -0
  61. package/dist/types/index.d.ts +19 -0
  62. package/dist/types/types.d.ts +529 -0
  63. package/package.json +100 -0
  64. package/src/components/atoms/FAvatar/FAvatar.stories.js +100 -0
  65. package/src/components/atoms/FAvatar/FAvatar.test.ts +95 -0
  66. package/src/components/atoms/FAvatar/FAvatar.vue +190 -0
  67. package/src/components/atoms/FBadge/FBadge.stories.js +129 -0
  68. package/src/components/atoms/FBadge/FBadge.test.ts +93 -0
  69. package/src/components/atoms/FBadge/FBadge.vue +103 -0
  70. package/src/components/atoms/FButton/FButton.stories.js +122 -0
  71. package/src/components/atoms/FButton/FButton.test.ts +98 -0
  72. package/src/components/atoms/FButton/FButton.vue +147 -0
  73. package/src/components/atoms/FCheckbox/FCheckbox.stories.js +96 -0
  74. package/src/components/atoms/FCheckbox/FCheckbox.test.ts +64 -0
  75. package/src/components/atoms/FCheckbox/FCheckbox.vue +76 -0
  76. package/src/components/atoms/FDivider/FDivider.stories.js +104 -0
  77. package/src/components/atoms/FDivider/FDivider.test.ts +80 -0
  78. package/src/components/atoms/FDivider/FDivider.vue +117 -0
  79. package/src/components/atoms/FIcon/FIcon.stories.js +189 -0
  80. package/src/components/atoms/FIcon/FIcon.test.ts +99 -0
  81. package/src/components/atoms/FIcon/FIcon.vue +192 -0
  82. package/src/components/atoms/FInput/FInput.stories.js +119 -0
  83. package/src/components/atoms/FInput/FInput.test.ts +79 -0
  84. package/src/components/atoms/FInput/FInput.vue +88 -0
  85. package/src/components/atoms/FLoader/FLoader.stories.js +109 -0
  86. package/src/components/atoms/FLoader/FLoader.test.ts +66 -0
  87. package/src/components/atoms/FLoader/FLoader.vue +97 -0
  88. package/src/components/atoms/FRadio/FRadio.stories.js +105 -0
  89. package/src/components/atoms/FRadio/FRadio.test.ts +75 -0
  90. package/src/components/atoms/FRadio/FRadio.vue +119 -0
  91. package/src/components/atoms/FTextarea/FTextarea.stories.js +126 -0
  92. package/src/components/atoms/FTextarea/FTextarea.test.ts +94 -0
  93. package/src/components/atoms/FTextarea/FTextarea.vue +156 -0
  94. package/src/components/atoms/FToggle/FToggle.stories.js +108 -0
  95. package/src/components/atoms/FToggle/FToggle.test.ts +96 -0
  96. package/src/components/atoms/FToggle/FToggle.vue +123 -0
  97. package/src/components/atoms/FTypography/FTypography.stories.js +127 -0
  98. package/src/components/atoms/FTypography/FTypography.test.ts +93 -0
  99. package/src/components/atoms/FTypography/FTypography.vue +78 -0
  100. package/src/components/atoms/index.ts +27 -0
  101. package/src/components/molecules/FAccordionItem/FAccordionItem.stories.js +71 -0
  102. package/src/components/molecules/FAccordionItem/FAccordionItem.test.ts +61 -0
  103. package/src/components/molecules/FAccordionItem/FAccordionItem.vue +105 -0
  104. package/src/components/molecules/FAlert/FAlert.stories.js +87 -0
  105. package/src/components/molecules/FAlert/FAlert.test.ts +59 -0
  106. package/src/components/molecules/FAlert/FAlert.vue +108 -0
  107. package/src/components/molecules/FBreadcrumb/FBreadcrumb.stories.js +90 -0
  108. package/src/components/molecules/FBreadcrumb/FBreadcrumb.test.ts +76 -0
  109. package/src/components/molecules/FBreadcrumb/FBreadcrumb.vue +117 -0
  110. package/src/components/molecules/FButtonGroup/FButtonGroup.stories.js +82 -0
  111. package/src/components/molecules/FButtonGroup/FButtonGroup.test.ts +44 -0
  112. package/src/components/molecules/FButtonGroup/FButtonGroup.vue +31 -0
  113. package/src/components/molecules/FCard/FCard.stories.js +136 -0
  114. package/src/components/molecules/FCard/FCard.test.ts +87 -0
  115. package/src/components/molecules/FCard/FCard.vue +75 -0
  116. package/src/components/molecules/FDatePicker/FDatePicker.stories.js +305 -0
  117. package/src/components/molecules/FDatePicker/FDatePicker.test.ts +282 -0
  118. package/src/components/molecules/FDatePicker/FDatePicker.vue +750 -0
  119. package/src/components/molecules/FEmptyState/FEmptyState.stories.js +98 -0
  120. package/src/components/molecules/FEmptyState/FEmptyState.test.ts +82 -0
  121. package/src/components/molecules/FEmptyState/FEmptyState.vue +89 -0
  122. package/src/components/molecules/FFilePreview/FFilePreview.stories.js +130 -0
  123. package/src/components/molecules/FFilePreview/FFilePreview.test.ts +70 -0
  124. package/src/components/molecules/FFilePreview/FFilePreview.vue +125 -0
  125. package/src/components/molecules/FFormField/FFormField.stories.js +149 -0
  126. package/src/components/molecules/FFormField/FFormField.test.ts +85 -0
  127. package/src/components/molecules/FFormField/FFormField.vue +107 -0
  128. package/src/components/molecules/FListItem/FListItem.stories.js +158 -0
  129. package/src/components/molecules/FListItem/FListItem.test.ts +93 -0
  130. package/src/components/molecules/FListItem/FListItem.vue +113 -0
  131. package/src/components/molecules/FPagination/FPagination.stories.js +132 -0
  132. package/src/components/molecules/FPagination/FPagination.test.ts +79 -0
  133. package/src/components/molecules/FPagination/FPagination.vue +206 -0
  134. package/src/components/molecules/FSearchBar/FSearchBar.stories.js +129 -0
  135. package/src/components/molecules/FSearchBar/FSearchBar.test.ts +81 -0
  136. package/src/components/molecules/FSearchBar/FSearchBar.vue +180 -0
  137. package/src/components/molecules/FSelect/FSelect.stories.js +333 -0
  138. package/src/components/molecules/FSelect/FSelect.test.ts +478 -0
  139. package/src/components/molecules/FSelect/FSelect.vue +551 -0
  140. package/src/components/molecules/FStatCard/FStatCard.stories.js +144 -0
  141. package/src/components/molecules/FStatCard/FStatCard.test.ts +78 -0
  142. package/src/components/molecules/FStatCard/FStatCard.vue +106 -0
  143. package/src/components/molecules/FTabs/FTab.vue +63 -0
  144. package/src/components/molecules/FTabs/FTabs.stories.js +277 -0
  145. package/src/components/molecules/FTabs/FTabs.test.ts +264 -0
  146. package/src/components/molecules/FTabs/FTabs.vue +273 -0
  147. package/src/components/molecules/FToast/FToast.stories.js +150 -0
  148. package/src/components/molecules/FToast/FToast.test.ts +157 -0
  149. package/src/components/molecules/FToast/FToast.vue +283 -0
  150. package/src/components/molecules/index.ts +37 -0
  151. package/src/components/organisms/FActivityFeed/FActivityFeed.stories.js +217 -0
  152. package/src/components/organisms/FActivityFeed/FActivityFeed.test.ts +134 -0
  153. package/src/components/organisms/FActivityFeed/FActivityFeed.vue +589 -0
  154. package/src/components/organisms/FDataTable/FDataTable.stories.js +370 -0
  155. package/src/components/organisms/FDataTable/FDataTable.test.ts +248 -0
  156. package/src/components/organisms/FDataTable/FDataTable.vue +808 -0
  157. package/src/components/organisms/FDrawer/FDrawer.stories.js +296 -0
  158. package/src/components/organisms/FDrawer/FDrawer.test.ts +142 -0
  159. package/src/components/organisms/FDrawer/FDrawer.vue +303 -0
  160. package/src/components/organisms/FFileUpload/FFileUpload.stories.js +162 -0
  161. package/src/components/organisms/FFileUpload/FFileUpload.test.ts +103 -0
  162. package/src/components/organisms/FFileUpload/FFileUpload.vue +616 -0
  163. package/src/components/organisms/FFilterSidebar/FFilterSidebar.stories.js +161 -0
  164. package/src/components/organisms/FFilterSidebar/FFilterSidebar.test.ts +92 -0
  165. package/src/components/organisms/FFilterSidebar/FFilterSidebar.vue +458 -0
  166. package/src/components/organisms/FForm/FForm.stories.js +270 -0
  167. package/src/components/organisms/FForm/FForm.test.ts +63 -0
  168. package/src/components/organisms/FForm/FForm.vue +19 -0
  169. package/src/components/organisms/FModal/FModal.stories.js +227 -0
  170. package/src/components/organisms/FModal/FModal.test.ts +181 -0
  171. package/src/components/organisms/FModal/FModal.vue +319 -0
  172. package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.stories.js +176 -0
  173. package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.test.ts +95 -0
  174. package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.vue +577 -0
  175. package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.stories.js +197 -0
  176. package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.test.ts +114 -0
  177. package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.vue +212 -0
  178. package/src/components/organisms/FOnboardingStepper/FStepperProgress.stories.js +122 -0
  179. package/src/components/organisms/FOnboardingStepper/FStepperProgress.test.ts +130 -0
  180. package/src/components/organisms/FOnboardingStepper/FStepperProgress.vue +146 -0
  181. package/src/components/organisms/FPageHeader/FPageHeader.stories.js +142 -0
  182. package/src/components/organisms/FPageHeader/FPageHeader.test.ts +83 -0
  183. package/src/components/organisms/FPageHeader/FPageHeader.vue +241 -0
  184. package/src/components/organisms/FProfileSection/FProfileSection.stories.js +190 -0
  185. package/src/components/organisms/FProfileSection/FProfileSection.test.ts +85 -0
  186. package/src/components/organisms/FProfileSection/FProfileSection.vue +562 -0
  187. package/src/components/organisms/FToastProvider/FToastProvider.stories.js +290 -0
  188. package/src/components/organisms/FToastProvider/FToastProvider.test.ts +215 -0
  189. package/src/components/organisms/FToastProvider/FToastProvider.vue +214 -0
  190. package/src/components/organisms/FUserMenu/FUserMenu.stories.js +170 -0
  191. package/src/components/organisms/FUserMenu/FUserMenu.test.ts +102 -0
  192. package/src/components/organisms/FUserMenu/FUserMenu.vue +407 -0
  193. package/src/components/organisms/index.ts +29 -0
  194. package/src/components/utils/FThemeProvider.stories.js +236 -0
  195. package/src/components/utils/FThemeProvider.test.ts +244 -0
  196. package/src/components/utils/FThemeProvider.vue +191 -0
  197. package/src/components/utils/index.ts +3 -0
  198. package/src/components.d.ts +602 -0
  199. package/src/composables/README.md +233 -0
  200. package/src/composables/index.ts +25 -0
  201. package/src/composables/useDataTableState.test.ts +378 -0
  202. package/src/composables/useDataTableState.ts +361 -0
  203. package/src/composables/useFormValidation.test.ts +198 -0
  204. package/src/composables/useFormValidation.ts +178 -0
  205. package/src/composables/useSidebarState.test.ts +307 -0
  206. package/src/composables/useSidebarState.ts +201 -0
  207. package/src/env.d.ts +14 -0
  208. package/src/index.ts +167 -0
  209. package/src/styles/tailwind.css +173 -0
  210. package/src/types.ts +740 -0
@@ -0,0 +1,616 @@
1
+ <template>
2
+ <div :class="containerClasses">
3
+ <!-- Hidden file input -->
4
+ <input
5
+ ref="fileInput"
6
+ type="file"
7
+ :accept="accept"
8
+ :multiple="multiple"
9
+ :disabled="disabled"
10
+ class="sr-only"
11
+ @change="handleFileChange"
12
+ />
13
+
14
+ <!-- Drop zone -->
15
+ <div
16
+ :class="dropZoneClasses"
17
+ @click="triggerFileInput"
18
+ @dragenter.prevent="handleDragEnter"
19
+ @dragover.prevent="handleDragOver"
20
+ @dragleave.prevent="handleDragLeave"
21
+ @drop.prevent="handleDrop"
22
+ >
23
+ <f-icon name="upload" size="lg" :class="iconClasses" />
24
+ <f-typography variant="body" :class="textClasses">
25
+ <slot name="label">
26
+ {{ dropZoneLabel }}
27
+ </slot>
28
+ </f-typography>
29
+ <f-typography v-if="hint" variant="caption" class="text-neutral-500">
30
+ {{ hint }}
31
+ </f-typography>
32
+ <f-button
33
+ v-if="showButton"
34
+ variant="outline"
35
+ size="small"
36
+ :disabled="disabled"
37
+ class="mt-2"
38
+ @click.stop="triggerFileInput"
39
+ >
40
+ <template #iconLeft>
41
+ <f-icon name="upload" size="sm" />
42
+ </template>
43
+ {{ buttonLabel }}
44
+ </f-button>
45
+ </div>
46
+
47
+ <!-- Alert for errors/success -->
48
+ <f-alert
49
+ v-if="alertMessage"
50
+ :variant="alertVariant"
51
+ :message="alertMessage"
52
+ :closable="true"
53
+ class="mt-3"
54
+ @close="clearAlert"
55
+ />
56
+
57
+ <!-- File previews -->
58
+ <div v-if="hasFiles" class="mt-3 space-y-2">
59
+ <f-file-preview
60
+ v-for="file in internalFiles"
61
+ :key="file.id"
62
+ :file-name="file.name"
63
+ :file-type="file.extension"
64
+ :loading="file.status === 'uploading'"
65
+ :disabled="disabled || file.status === 'uploading'"
66
+ :loading-label="loadingLabel"
67
+ @remove="handleRemoveFile(file)"
68
+ />
69
+ </div>
70
+
71
+ <!-- Progress bar for overall upload -->
72
+ <div v-if="showProgress && isUploading" class="mt-3">
73
+ <div class="flex items-center justify-between mb-1">
74
+ <f-typography variant="caption" class="text-neutral-600">
75
+ {{ progressLabel }}
76
+ </f-typography>
77
+ <f-typography variant="caption" class="text-neutral-600">
78
+ {{ uploadProgress }}%
79
+ </f-typography>
80
+ </div>
81
+ <div class="w-full bg-neutral-200 rounded-full h-2">
82
+ <div
83
+ class="bg-primary-600 h-2 rounded-full transition-all duration-300"
84
+ :style="{ width: `${uploadProgress}%` }"
85
+ />
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </template>
90
+
91
+ <script>
92
+ import FIcon from '../../atoms/FIcon/FIcon.vue';
93
+ import FTypography from '../../atoms/FTypography/FTypography.vue';
94
+ import FButton from '../../atoms/FButton/FButton.vue';
95
+ import FAlert from '../../molecules/FAlert/FAlert.vue';
96
+ import FFilePreview from '../../molecules/FFilePreview/FFilePreview.vue';
97
+
98
+ let idCounter = 0;
99
+
100
+ /**
101
+ * File state constants
102
+ */
103
+ const FILE_STATUS = {
104
+ PENDING: 'pending',
105
+ UPLOADING: 'uploading',
106
+ SUCCESS: 'success',
107
+ ERROR: 'error'
108
+ };
109
+
110
+ export default {
111
+ name: 'FFileUpload',
112
+ components: {
113
+ FIcon,
114
+ FTypography,
115
+ FButton,
116
+ FAlert,
117
+ FFilePreview
118
+ },
119
+ props: {
120
+ /**
121
+ * Array of files (for v-model support)
122
+ * Each file object should have: { id, name, file, status, progress }
123
+ */
124
+ value: {
125
+ type: Array,
126
+ default: () => []
127
+ },
128
+ /**
129
+ * Accepted file types (MIME types or extensions)
130
+ * Example: 'image/*,.pdf,.doc,.docx'
131
+ */
132
+ accept: {
133
+ type: String,
134
+ default: ''
135
+ },
136
+ /**
137
+ * Allow multiple file selection
138
+ */
139
+ multiple: {
140
+ type: Boolean,
141
+ default: false
142
+ },
143
+ /**
144
+ * Maximum file size in bytes
145
+ */
146
+ maxSize: {
147
+ type: Number,
148
+ default: 0
149
+ },
150
+ /**
151
+ * Maximum number of files allowed
152
+ */
153
+ maxFiles: {
154
+ type: Number,
155
+ default: 0
156
+ },
157
+ /**
158
+ * Disable the upload component
159
+ */
160
+ disabled: {
161
+ type: Boolean,
162
+ default: false
163
+ },
164
+ /**
165
+ * Show the upload button inside the drop zone
166
+ */
167
+ showButton: {
168
+ type: Boolean,
169
+ default: true
170
+ },
171
+ /**
172
+ * Show progress bar during upload
173
+ */
174
+ showProgress: {
175
+ type: Boolean,
176
+ default: true
177
+ },
178
+ /**
179
+ * Label for the drop zone
180
+ */
181
+ dropZoneLabel: {
182
+ type: String,
183
+ default: 'Glissez-déposez vos fichiers ici'
184
+ },
185
+ /**
186
+ * Label for the upload button
187
+ */
188
+ buttonLabel: {
189
+ type: String,
190
+ default: 'Parcourir'
191
+ },
192
+ /**
193
+ * Hint text displayed below the drop zone label
194
+ */
195
+ hint: {
196
+ type: String,
197
+ default: ''
198
+ },
199
+ /**
200
+ * Loading label for file preview
201
+ */
202
+ loadingLabel: {
203
+ type: String,
204
+ default: 'Téléversement en cours'
205
+ },
206
+ /**
207
+ * Progress label shown during upload
208
+ */
209
+ progressLabel: {
210
+ type: String,
211
+ default: 'Progression'
212
+ },
213
+ /**
214
+ * Error message for file size validation
215
+ */
216
+ errorSizeMessage: {
217
+ type: String,
218
+ default: 'Le fichier dépasse la taille maximale autorisée'
219
+ },
220
+ /**
221
+ * Error message for file type validation
222
+ */
223
+ errorTypeMessage: {
224
+ type: String,
225
+ default: "Ce type de fichier n'est pas autorisé"
226
+ },
227
+ /**
228
+ * Error message for max files validation
229
+ */
230
+ errorMaxFilesMessage: {
231
+ type: String,
232
+ default: 'Nombre maximum de fichiers atteint'
233
+ },
234
+ /**
235
+ * Success message after upload
236
+ */
237
+ successMessage: {
238
+ type: String,
239
+ default: 'Fichier(s) téléversé(s) avec succès'
240
+ }
241
+ },
242
+ data() {
243
+ return {
244
+ isDragging: false,
245
+ alertMessage: '',
246
+ alertVariant: 'info',
247
+ uploadProgress: 0
248
+ };
249
+ },
250
+ computed: {
251
+ /**
252
+ * Internal files list synced with v-model
253
+ */
254
+ internalFiles: {
255
+ get() {
256
+ return this.value;
257
+ },
258
+ set(val) {
259
+ this.$emit('input', val);
260
+ }
261
+ },
262
+ /**
263
+ * Check if there are files
264
+ */
265
+ hasFiles() {
266
+ return this.internalFiles.length > 0;
267
+ },
268
+ /**
269
+ * Check if any file is currently uploading
270
+ */
271
+ isUploading() {
272
+ return this.internalFiles.some((f) => f.status === FILE_STATUS.UPLOADING);
273
+ },
274
+ /**
275
+ * Container classes
276
+ */
277
+ containerClasses() {
278
+ return 'w-full';
279
+ },
280
+ /**
281
+ * Drop zone classes
282
+ */
283
+ dropZoneClasses() {
284
+ const baseClasses =
285
+ 'flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-lg cursor-pointer transition-colors duration-200';
286
+ const stateClasses = this.isDragging
287
+ ? 'border-primary-500 bg-primary-50'
288
+ : 'border-neutral-300 hover:border-neutral-400 bg-neutral-50';
289
+ const disabledClasses = this.disabled
290
+ ? 'opacity-50 cursor-not-allowed pointer-events-none'
291
+ : '';
292
+
293
+ return [baseClasses, stateClasses, disabledClasses]
294
+ .filter(Boolean)
295
+ .join(' ');
296
+ },
297
+ /**
298
+ * Icon classes
299
+ */
300
+ iconClasses() {
301
+ return this.isDragging ? 'text-primary-500' : 'text-neutral-400';
302
+ },
303
+ /**
304
+ * Text classes
305
+ */
306
+ textClasses() {
307
+ return this.isDragging ? 'text-primary-600' : 'text-neutral-600';
308
+ }
309
+ },
310
+ methods: {
311
+ /**
312
+ * Trigger the hidden file input
313
+ */
314
+ triggerFileInput() {
315
+ if (!this.disabled) {
316
+ this.$refs.fileInput.click();
317
+ }
318
+ },
319
+ /**
320
+ * Handle file input change
321
+ */
322
+ handleFileChange(event) {
323
+ const files = Array.from(event.target.files);
324
+ this.processFiles(files);
325
+ // Reset input to allow selecting the same file again
326
+ event.target.value = '';
327
+ },
328
+ /**
329
+ * Handle drag enter event
330
+ */
331
+ handleDragEnter(event) {
332
+ if (!this.disabled) {
333
+ event.preventDefault();
334
+ this.isDragging = true;
335
+ }
336
+ },
337
+ /**
338
+ * Handle drag over event
339
+ */
340
+ handleDragOver(event) {
341
+ if (!this.disabled) {
342
+ event.preventDefault();
343
+ this.isDragging = true;
344
+ }
345
+ },
346
+ /**
347
+ * Handle drag leave event
348
+ */
349
+ handleDragLeave(event) {
350
+ if (!this.disabled) {
351
+ event.preventDefault();
352
+ this.isDragging = false;
353
+ }
354
+ },
355
+ /**
356
+ * Handle drop event
357
+ */
358
+ handleDrop(event) {
359
+ if (!this.disabled) {
360
+ this.isDragging = false;
361
+ const files = Array.from(event.dataTransfer.files);
362
+ this.processFiles(files);
363
+ }
364
+ },
365
+ /**
366
+ * Process and validate files
367
+ */
368
+ processFiles(files) {
369
+ this.clearAlert();
370
+
371
+ // Handle empty files array
372
+ if (!files || files.length === 0) {
373
+ return;
374
+ }
375
+
376
+ // If not multiple, only take the first file
377
+ const filesToProcess = this.multiple ? files : [files[0]];
378
+
379
+ // Check max files limit (only for multiple mode)
380
+ if (this.multiple && this.maxFiles > 0) {
381
+ const totalFiles = this.internalFiles.length + filesToProcess.length;
382
+ if (totalFiles > this.maxFiles) {
383
+ this.showError(this.errorMaxFilesMessage);
384
+ return;
385
+ }
386
+ }
387
+
388
+ const validFiles = [];
389
+ for (const file of filesToProcess) {
390
+ const validation = this.validateFile(file);
391
+ if (!validation.valid) {
392
+ this.showError(validation.error);
393
+ return;
394
+ }
395
+
396
+ const fileObject = this.createFileObject(file);
397
+ validFiles.push(fileObject);
398
+ }
399
+
400
+ // If not multiple, replace existing files
401
+ if (!this.multiple) {
402
+ this.internalFiles = validFiles;
403
+ } else {
404
+ this.internalFiles = [...this.internalFiles, ...validFiles];
405
+ }
406
+
407
+ // Emit files-selected event
408
+ this.$emit('files-selected', validFiles);
409
+ },
410
+ /**
411
+ * Validate a single file
412
+ */
413
+ validateFile(file) {
414
+ // Validate file type
415
+ if (this.accept) {
416
+ const isValid = this.isFileTypeValid(file);
417
+ if (!isValid) {
418
+ return { valid: false, error: this.errorTypeMessage };
419
+ }
420
+ }
421
+
422
+ // Validate file size
423
+ if (this.maxSize > 0 && file.size > this.maxSize) {
424
+ return { valid: false, error: this.errorSizeMessage };
425
+ }
426
+
427
+ return { valid: true };
428
+ },
429
+ /**
430
+ * Check if file type is valid based on accept attribute
431
+ */
432
+ isFileTypeValid(file) {
433
+ const acceptedTypes = this.accept.split(',').map((t) => t.trim());
434
+
435
+ return acceptedTypes.some((acceptedType) => {
436
+ if (acceptedType.startsWith('.')) {
437
+ // Extension check
438
+ const ext = '.' + file.name.split('.').pop().toLowerCase();
439
+ return ext === acceptedType.toLowerCase();
440
+ } else if (acceptedType.endsWith('/*')) {
441
+ // MIME type wildcard (e.g., image/*)
442
+ const baseType = acceptedType.replace('/*', '');
443
+ return file.type.startsWith(baseType);
444
+ } else {
445
+ // Exact MIME type match
446
+ return file.type === acceptedType;
447
+ }
448
+ });
449
+ },
450
+ /**
451
+ * Create a file object for internal tracking
452
+ */
453
+ createFileObject(file) {
454
+ const extension = file.name.split('.').pop().toLowerCase();
455
+ return {
456
+ id: `file-${++idCounter}`,
457
+ name: file.name,
458
+ size: file.size,
459
+ type: file.type,
460
+ extension,
461
+ file,
462
+ status: FILE_STATUS.PENDING,
463
+ progress: 0
464
+ };
465
+ },
466
+ /**
467
+ * Remove a file from the list
468
+ */
469
+ handleRemoveFile(fileToRemove) {
470
+ this.internalFiles = this.internalFiles.filter(
471
+ (f) => f.id !== fileToRemove.id
472
+ );
473
+ this.$emit('file-removed', fileToRemove);
474
+ },
475
+ /**
476
+ * Show error message
477
+ */
478
+ showError(message) {
479
+ this.alertMessage = message;
480
+ this.alertVariant = 'error';
481
+ },
482
+ /**
483
+ * Show success message
484
+ */
485
+ showSuccess(message) {
486
+ this.alertMessage = message || this.successMessage;
487
+ this.alertVariant = 'success';
488
+ },
489
+ /**
490
+ * Clear alert message
491
+ */
492
+ clearAlert() {
493
+ this.alertMessage = '';
494
+ },
495
+ /**
496
+ * Start upload for a specific file (to be called externally)
497
+ */
498
+ startUpload(fileId) {
499
+ const file = this.internalFiles.find((f) => f.id === fileId);
500
+ if (file) {
501
+ file.status = FILE_STATUS.UPLOADING;
502
+ file.progress = 0;
503
+ this.updateFile(file);
504
+ this.$emit('upload-start', file);
505
+ }
506
+ },
507
+ /**
508
+ * Update upload progress for a specific file
509
+ */
510
+ updateProgress(fileId, progress) {
511
+ const file = this.internalFiles.find((f) => f.id === fileId);
512
+ if (file) {
513
+ file.progress = progress;
514
+ this.updateFile(file);
515
+ this.$emit('upload-progress', { file, progress });
516
+
517
+ // Update overall progress
518
+ this.calculateOverallProgress();
519
+ }
520
+ },
521
+ /**
522
+ * Mark file as successfully uploaded
523
+ */
524
+ markAsSuccess(fileId) {
525
+ const file = this.internalFiles.find((f) => f.id === fileId);
526
+ if (file) {
527
+ file.status = FILE_STATUS.SUCCESS;
528
+ file.progress = 100;
529
+ this.updateFile(file);
530
+ this.$emit('upload-success', file);
531
+
532
+ // Check if all files are done
533
+ if (this.internalFiles.every((f) => f.status === FILE_STATUS.SUCCESS)) {
534
+ this.showSuccess();
535
+ this.$emit('upload-complete', this.internalFiles);
536
+ }
537
+ }
538
+ },
539
+ /**
540
+ * Mark file as failed
541
+ */
542
+ markAsError(fileId, errorMessage) {
543
+ const file = this.internalFiles.find((f) => f.id === fileId);
544
+ if (file) {
545
+ file.status = FILE_STATUS.ERROR;
546
+ this.updateFile(file);
547
+ this.showError(errorMessage);
548
+ this.$emit('upload-error', { file, error: errorMessage });
549
+ }
550
+ },
551
+ /**
552
+ * Update a file in the internal list
553
+ */
554
+ updateFile(updatedFile) {
555
+ const index = this.internalFiles.findIndex(
556
+ (f) => f.id === updatedFile.id
557
+ );
558
+ if (index !== -1) {
559
+ const newFiles = [...this.internalFiles];
560
+ newFiles[index] = { ...updatedFile };
561
+ this.internalFiles = newFiles;
562
+ }
563
+ },
564
+ /**
565
+ * Calculate overall upload progress
566
+ */
567
+ calculateOverallProgress() {
568
+ if (!this.hasFiles) {
569
+ this.uploadProgress = 0;
570
+ return;
571
+ }
572
+
573
+ const uploadingFiles = this.internalFiles.filter(
574
+ (f) =>
575
+ f.status === FILE_STATUS.UPLOADING || f.status === FILE_STATUS.SUCCESS
576
+ );
577
+
578
+ if (uploadingFiles.length === 0) {
579
+ this.uploadProgress = 0;
580
+ return;
581
+ }
582
+
583
+ const totalProgress = uploadingFiles.reduce(
584
+ (sum, f) => sum + f.progress,
585
+ 0
586
+ );
587
+ this.uploadProgress = Math.round(totalProgress / uploadingFiles.length);
588
+ },
589
+ /**
590
+ * Clear all files
591
+ */
592
+ clearFiles() {
593
+ this.internalFiles = [];
594
+ this.clearAlert();
595
+ this.uploadProgress = 0;
596
+ this.$emit('files-cleared');
597
+ },
598
+ /**
599
+ * Get all pending files (ready for upload)
600
+ */
601
+ getPendingFiles() {
602
+ return this.internalFiles.filter((f) => f.status === FILE_STATUS.PENDING);
603
+ },
604
+ /**
605
+ * Start upload for all pending files
606
+ */
607
+ uploadAll() {
608
+ const pendingFiles = this.getPendingFiles();
609
+ pendingFiles.forEach((file) => {
610
+ this.startUpload(file.id);
611
+ });
612
+ this.$emit('upload-all', pendingFiles);
613
+ }
614
+ }
615
+ };
616
+ </script>