@ticatec/uniface-element 0.2.4 → 0.3.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 (176) hide show
  1. package/dist/accordion/README.md +169 -0
  2. package/dist/accordion/README_CN.md +176 -0
  3. package/dist/action-bar/README.md +275 -0
  4. package/dist/action-bar/README_CN.md +285 -0
  5. package/dist/app-layout/README.md +374 -0
  6. package/dist/app-layout/README_CN.md +381 -0
  7. package/dist/app-top-bar/README.md +357 -0
  8. package/dist/app-top-bar/README_CN.md +365 -0
  9. package/dist/attachment-files/AttachmentFilesField.svelte +67 -1
  10. package/dist/attachment-files/AttachmentFilesField.svelte.d.ts +8 -1
  11. package/dist/base-calendar/CalendarBase.svelte +0 -3
  12. package/dist/box/README.md +385 -0
  13. package/dist/box/README_CN.md +393 -0
  14. package/dist/breadcrumbs/README.md +442 -0
  15. package/dist/breadcrumbs/README_CN.md +442 -0
  16. package/dist/button/Button.svelte +27 -7
  17. package/dist/button/Button.svelte.d.ts +3 -1
  18. package/dist/button/IconButton.svelte +26 -6
  19. package/dist/button/IconButton.svelte.d.ts +3 -1
  20. package/dist/button/IconButton_README.md +513 -0
  21. package/dist/button/IconButton_README_CN.md +513 -0
  22. package/dist/button/README.md +428 -0
  23. package/dist/button/README_CN.md +428 -0
  24. package/dist/button/TextButton.svelte +26 -6
  25. package/dist/button/TextButton.svelte.d.ts +3 -1
  26. package/dist/button/TextButton_README.md +571 -0
  27. package/dist/button/TextButton_README_CN.md +571 -0
  28. package/dist/card/README.md +666 -0
  29. package/dist/card/README_CN.md +666 -0
  30. package/dist/cascade-options-select/CascadeOptionSelect.svelte +43 -7
  31. package/dist/cascade-options-select/CascadeOptionSelect.svelte.d.ts +3 -4
  32. package/dist/cascade-options-select/README.md +663 -2
  33. package/dist/cascade-options-select/README_CN.md +664 -0
  34. package/dist/checkbox/CheckBox.svelte +20 -6
  35. package/dist/checkbox/CheckBox.svelte.d.ts +4 -2
  36. package/dist/checkbox/README.md +562 -0
  37. package/dist/checkbox/README_CN.md +727 -0
  38. package/dist/common-editor/NumberInput.svelte +39 -26
  39. package/dist/common-editor/NumberInput.svelte.d.ts +5 -1
  40. package/dist/concise-data-table/ConciseListTable.svelte +133 -36
  41. package/dist/concise-data-table/ConciseListTable.svelte.d.ts +8 -1
  42. package/dist/concise-data-table/DataCell.svelte +5 -6
  43. package/dist/concise-data-table/DataRow.svelte +22 -2
  44. package/dist/concise-data-table/DataRow.svelte.d.ts +1 -0
  45. package/dist/concise-data-table/README.md +606 -0
  46. package/dist/concise-data-table/README_CN.md +611 -0
  47. package/dist/context-menu/ContextMenu.svelte +138 -21
  48. package/dist/context-menu/ContextMenu.svelte.d.ts +5 -0
  49. package/dist/context-menu/ContextMenuItem.d.ts +20 -4
  50. package/dist/context-menu/README.md +551 -0
  51. package/dist/context-menu/README_CN.md +551 -0
  52. package/dist/criteria-field/CriteriaField.svelte +3 -5
  53. package/dist/criteria-field/README.md +532 -0
  54. package/dist/criteria-field/README_CN.md +532 -0
  55. package/dist/data-table/README.md +712 -0
  56. package/dist/data-table/README_CN.md +712 -0
  57. package/dist/date-picker/DatePicker.svelte +44 -7
  58. package/dist/date-picker/DatePicker.svelte.d.ts +3 -4
  59. package/dist/date-picker/DateTimePicker.svelte +32 -8
  60. package/dist/date-picker/DateTimePicker.svelte.d.ts +3 -4
  61. package/dist/date-picker/README.md +326 -0
  62. package/dist/date-picker/README_CN.md +316 -0
  63. package/dist/date-picker/ScrollBar.svelte +18 -8
  64. package/dist/date-picker/ScrollBar.svelte.d.ts +1 -1
  65. package/dist/date-picker/TimePanel.svelte +5 -5
  66. package/dist/date-picker/TimePanel.svelte.d.ts +1 -1
  67. package/dist/date-range/README.md +374 -0
  68. package/dist/date-range/README_CN.md +374 -0
  69. package/dist/dialog/README.md +596 -0
  70. package/dist/dialog/README_CN.md +596 -0
  71. package/dist/drawer/README.md +679 -1
  72. package/dist/drawer/README_CN.md +679 -0
  73. package/dist/group-box/group-check-box/GroupCheckBox.svelte +62 -5
  74. package/dist/group-box/group-check-box/GroupCheckBox.svelte.d.ts +7 -2
  75. package/dist/group-box/group-check-box/README.md +749 -0
  76. package/dist/group-box/group-check-box/README_CN.md +750 -0
  77. package/dist/group-box/group-radio-box/GroupRadioBox.svelte +69 -4
  78. package/dist/group-box/group-radio-box/GroupRadioBox.svelte.d.ts +8 -2
  79. package/dist/group-box/group-radio-box/README.md +920 -0
  80. package/dist/group-box/group-radio-box/README_CN.md +920 -0
  81. package/dist/image-files/README.md +1346 -0
  82. package/dist/image-files/README_CN.md +1346 -0
  83. package/dist/input-options-select/InputOptionsSelect.svelte +161 -65
  84. package/dist/input-options-select/InputOptionsSelect.svelte.d.ts +4 -1
  85. package/dist/lib/forwardAllEvents.d.ts +3 -0
  86. package/dist/lib/forwardAllEvents.js +24 -0
  87. package/dist/list-box/README.md +579 -0
  88. package/dist/list-box/README_CN.md +682 -0
  89. package/dist/lookup-editor/LookupEditor.svelte +15 -3
  90. package/dist/lookup-editor/LookupEditor.svelte.d.ts +3 -4
  91. package/dist/lookup-editor/README.md +684 -0
  92. package/dist/lookup-editor/README_CN.md +684 -0
  93. package/dist/memo-editor/MemoEditor.svelte +77 -27
  94. package/dist/memo-editor/MemoEditor.svelte.d.ts +6 -3
  95. package/dist/memo-editor/README.md +819 -0
  96. package/dist/memo-editor/README_CN.md +819 -0
  97. package/dist/message-box/README.md +398 -0
  98. package/dist/message-box/README_CN.md +398 -0
  99. package/dist/number-editor/NumberEditor.svelte +2 -2
  100. package/dist/number-editor/NumberEditor.svelte.d.ts +1 -1
  101. package/dist/number-editor/README.md +353 -22
  102. package/dist/number-editor/README_CN.md +357 -0
  103. package/dist/number-range/README.md +443 -0
  104. package/dist/number-range/README_CN.md +443 -0
  105. package/dist/options-multi-select/OptionsMultiSelect.svelte +87 -12
  106. package/dist/options-multi-select/OptionsMultiSelect.svelte.d.ts +3 -1
  107. package/dist/options-multi-select/README.md +480 -0
  108. package/dist/options-multi-select/README_CN.md +480 -0
  109. package/dist/options-select/OptionsSelect.svelte +36 -5
  110. package/dist/options-select/OptionsSelect.svelte.d.ts +3 -4
  111. package/dist/options-select/README.md +632 -0
  112. package/dist/options-select/README_CN.md +632 -0
  113. package/dist/page/README.md +605 -0
  114. package/dist/page/README_CN.md +605 -0
  115. package/dist/pagination/README.md +946 -0
  116. package/dist/pagination/README_CN.md +946 -0
  117. package/dist/pagination-panel/README.md +1075 -0
  118. package/dist/pagination-panel/README_CN.md +1075 -0
  119. package/dist/popup-hint/README.md +841 -0
  120. package/dist/popup-hint/README_CN.md +840 -0
  121. package/dist/progress-bar/README.md +242 -0
  122. package/dist/progress-bar/README_CN.md +242 -0
  123. package/dist/progress-step-bar/README.md +316 -0
  124. package/dist/progress-step-bar/README_CN.md +316 -0
  125. package/dist/prompts-text-editor/PromptsTextEditor.svelte +6 -3
  126. package/dist/prompts-text-editor/PromptsTextEditor.svelte.d.ts +3 -1
  127. package/dist/prompts-text-editor/README.md +329 -0
  128. package/dist/prompts-text-editor/README_CN.md +329 -0
  129. package/dist/property-editor/README.md +969 -0
  130. package/dist/property-editor/README_CN.md +969 -0
  131. package/dist/radio-button/README.md +372 -0
  132. package/dist/radio-button/README_CN.md +372 -0
  133. package/dist/radio-button/RadioButton.svelte +32 -3
  134. package/dist/radio-button/RadioButton.svelte.d.ts +12 -1
  135. package/dist/search-box/README.md +425 -0
  136. package/dist/search-box/README_CN.md +424 -0
  137. package/dist/search-box/SearchBox.svelte +22 -6
  138. package/dist/search-box/SearchBox.svelte.d.ts +4 -1
  139. package/dist/tabs/README.md +919 -0
  140. package/dist/tabs/README_CN.md +920 -0
  141. package/dist/tag/README.md +922 -0
  142. package/dist/tag/README_CN.md +922 -0
  143. package/dist/text-editor/PasswordEditor.svelte +17 -2
  144. package/dist/text-editor/PasswordEditor.svelte.d.ts +3 -3
  145. package/dist/text-editor/PasswordEditor_README.md +551 -0
  146. package/dist/text-editor/PasswordEditor_README_CN.md +551 -0
  147. package/dist/text-editor/README.md +466 -0
  148. package/dist/text-editor/README_CN.md +466 -0
  149. package/dist/text-editor/TextEditor.svelte +51 -19
  150. package/dist/text-editor/TextEditor.svelte.d.ts +0 -1
  151. package/dist/time-editor/README.md +498 -24
  152. package/dist/time-editor/README_CN.md +498 -0
  153. package/dist/time-editor/TimeEditor.svelte +7 -3
  154. package/dist/time-editor/TimeEditor.svelte.d.ts +3 -1
  155. package/dist/toast/README.md +667 -0
  156. package/dist/toast/README_CN.md +667 -0
  157. package/dist/transfer/README.md +795 -0
  158. package/dist/transfer/Transfer.svelte +6 -4
  159. package/dist/transfer/Transfer.svelte.d.ts +3 -1
  160. package/dist/tree-view/README.md +761 -0
  161. package/dist/tree-view/README_CN.md +761 -0
  162. package/dist/tree-view/TreeView.svelte +5 -3
  163. package/dist/tree-view/TreeView.svelte.d.ts +3 -1
  164. package/dist/unit-number-editor/README.md +546 -20
  165. package/dist/unit-number-editor/README_CN.md +525 -0
  166. package/dist/unit-number-editor/UnitNumberEditor.svelte +62 -8
  167. package/dist/unit-number-editor/UnitNumberEditor.svelte.d.ts +5 -1
  168. package/docs/form-field/cn/README_CN.md +364 -0
  169. package/docs/form-field/en/README.md +364 -0
  170. package/docs/form-panel/flex-form/cn/README_CN.md +586 -0
  171. package/docs/form-panel/flex-form/en/README.md +586 -0
  172. package/docs/form-panel/flex-row-form/cn/README_CN.md +787 -0
  173. package/docs/form-panel/flex-row-form/en/README.md +787 -0
  174. package/docs/form-panel/grid-form/en/README.md +744 -0
  175. package/package.json +1 -1
  176. package/dist/drawer/README-CN.md +0 -1
@@ -0,0 +1,1346 @@
1
+ # ImageFilesField
2
+
3
+ A comprehensive image upload and management component that provides drag-and-drop upload functionality, image preview, thumbnail generation, and file management capabilities. Perfect for handling image galleries, avatar uploads, and document attachments with visual previews.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @ticatec/uniface-element
9
+ ```
10
+
11
+ ## Import
12
+
13
+ ```typescript
14
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
15
+ import type { ImageFile, RemoveConfirm } from "@ticatec/uniface-element/ImageFilesField";
16
+ import type { FileUpload } from "@ticatec/uniface-element";
17
+ ```
18
+
19
+ ## Basic Usage
20
+
21
+ ```svelte
22
+ <script>
23
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
24
+
25
+ let files = [];
26
+
27
+ // Simple upload function
28
+ const uploadImage = (file, progressUpdate, onUploaded, errorHandler) => {
29
+ // Simulate upload progress
30
+ let progress = 0;
31
+ const interval = setInterval(() => {
32
+ progress += 10;
33
+ progressUpdate(Math.floor(file.size / 100 * progress));
34
+
35
+ if (progress >= 100) {
36
+ clearInterval(interval);
37
+ // Call onUploaded with the uploaded file URL and thumbnail URL
38
+ onUploaded(`/uploads/${file.name}`, `/thumbnails/${file.name}`);
39
+ }
40
+ }, 100);
41
+
42
+ return {
43
+ cancel: async () => {
44
+ clearInterval(interval);
45
+ return true;
46
+ }
47
+ };
48
+ };
49
+ </script>
50
+
51
+ <ImageFilesField
52
+ bind:files={files}
53
+ uploadFile={uploadImage}
54
+ maxFiles={5}
55
+ />
56
+ ```
57
+
58
+ ## Props
59
+
60
+ | Prop | Type | Default | Description |
61
+ |------|------|---------|-------------|
62
+ | `files` | `Array<ImageFile>` | `[]` | Array of uploaded image files |
63
+ | `uploadFile` | `FileUpload` | required | Upload function handler |
64
+ | `maxFiles` | `number` | `1` | Maximum number of files allowed |
65
+ | `variant` | `"" \| "plain" \| "outlined" \| "filled"` | `""` | Visual variant |
66
+ | `disabled` | `boolean` | `false` | Whether the component is disabled |
67
+ | `readonly` | `boolean` | `false` | Whether the component is read-only |
68
+ | `displayMode` | `DisplayMode` | `DisplayMode.Edit` | Display mode (Edit or View) |
69
+ | `removeFileConfirm` | `RemoveConfirm \| null` | `null` | Confirmation function for file removal |
70
+ | `style` | `string` | `""` | Additional CSS styles |
71
+ | `text` | `string \| null` | `null` | Text representation of files (auto-generated) |
72
+
73
+ ## Types
74
+
75
+ ### ImageFile
76
+
77
+ ```typescript
78
+ interface ImageFile {
79
+ name: string; // File name
80
+ uri: string; // Full-size image URL
81
+ thumbnail?: string; // Thumbnail URL (optional)
82
+ }
83
+ ```
84
+
85
+ ### RemoveConfirm
86
+
87
+ ```typescript
88
+ type RemoveConfirm = (file: ImageFile) => Promise<boolean>;
89
+ ```
90
+
91
+ ### FileUpload
92
+
93
+ ```typescript
94
+ type FileUpload = (
95
+ file: File,
96
+ progressUpdate: ProgressUpdate,
97
+ onUploaded: OnUploaded,
98
+ errorHandler: ErrorHandler
99
+ ) => UploadProgress;
100
+
101
+ type ProgressUpdate = (bytesUploaded: number) => void;
102
+ type OnUploaded = (url: string, thumbnail?: string) => void;
103
+ type ErrorHandler = (error: Error) => void;
104
+
105
+ interface UploadProgress {
106
+ cancel: () => Promise<boolean>;
107
+ }
108
+ ```
109
+
110
+ ## Examples
111
+
112
+ ### Basic Image Upload with Preview
113
+
114
+ ```svelte
115
+ <script>
116
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
117
+ import FormField from "@ticatec/uniface-element/FormField";
118
+
119
+ let profileImages = [];
120
+
121
+ const uploadProfileImage = (file, progressUpdate, onUploaded, errorHandler) => {
122
+ const formData = new FormData();
123
+ formData.append('image', file);
124
+
125
+ const xhr = new XMLHttpRequest();
126
+
127
+ xhr.upload.addEventListener('progress', (e) => {
128
+ if (e.lengthComputable) {
129
+ progressUpdate(e.loaded);
130
+ }
131
+ });
132
+
133
+ xhr.onload = () => {
134
+ if (xhr.status === 200) {
135
+ const response = JSON.parse(xhr.responseText);
136
+ onUploaded(response.url, response.thumbnail);
137
+ } else {
138
+ errorHandler(new Error(`Upload failed: ${xhr.statusText}`));
139
+ }
140
+ };
141
+
142
+ xhr.onerror = () => {
143
+ errorHandler(new Error('Network error during upload'));
144
+ };
145
+
146
+ xhr.open('POST', '/api/upload/profile-image');
147
+ xhr.send(formData);
148
+
149
+ return {
150
+ cancel: async () => {
151
+ xhr.abort();
152
+ return true;
153
+ }
154
+ };
155
+ };
156
+
157
+ const confirmRemoveImage = async (file) => {
158
+ return confirm(`Are you sure you want to remove ${file.name}?`);
159
+ };
160
+ </script>
161
+
162
+ <div class="profile-section">
163
+ <FormField label="Profile Pictures">
164
+ <ImageFilesField
165
+ bind:files={profileImages}
166
+ uploadFile={uploadProfileImage}
167
+ removeFileConfirm={confirmRemoveImage}
168
+ maxFiles={3}
169
+ variant="outlined"
170
+ />
171
+ </FormField>
172
+
173
+ <div class="image-info">
174
+ <p>Uploaded {profileImages.length} image(s)</p>
175
+ {#each profileImages as image}
176
+ <p>• {image.name}</p>
177
+ {/each}
178
+ </div>
179
+ </div>
180
+
181
+ <style>
182
+ .profile-section {
183
+ max-width: 600px;
184
+ margin: 20px;
185
+ padding: 20px;
186
+ border: 1px solid #e2e8f0;
187
+ border-radius: 8px;
188
+ }
189
+
190
+ .image-info {
191
+ margin-top: 16px;
192
+ padding: 12px;
193
+ background: #f8fafc;
194
+ border-radius: 6px;
195
+ }
196
+ </style>
197
+ ```
198
+
199
+ ### Product Gallery Management
200
+
201
+ ```svelte
202
+ <script>
203
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
204
+ import FormField from "@ticatec/uniface-element/FormField";
205
+
206
+ let productImages = [
207
+ {
208
+ name: "product-main.jpg",
209
+ uri: "/images/products/product-main.jpg",
210
+ thumbnail: "/images/products/thumbs/product-main.jpg"
211
+ },
212
+ {
213
+ name: "product-side.jpg",
214
+ uri: "/images/products/product-side.jpg",
215
+ thumbnail: "/images/products/thumbs/product-side.jpg"
216
+ }
217
+ ];
218
+
219
+ let isUploading = false;
220
+ let uploadProgress = {};
221
+
222
+ const uploadProductImage = (file, progressUpdate, onUploaded, errorHandler) => {
223
+ isUploading = true;
224
+ const fileId = `${file.name}-${Date.now()}`;
225
+ uploadProgress[fileId] = 0;
226
+
227
+ // Simulate image processing and upload
228
+ const totalSteps = 100;
229
+ let currentStep = 0;
230
+
231
+ const interval = setInterval(() => {
232
+ currentStep += Math.random() * 5;
233
+
234
+ if (currentStep >= totalSteps) {
235
+ currentStep = totalSteps;
236
+ clearInterval(interval);
237
+
238
+ // Simulate successful upload
239
+ const baseUrl = '/api/products/images';
240
+ const imageUrl = `${baseUrl}/${fileId}.jpg`;
241
+ const thumbnailUrl = `${baseUrl}/thumbs/${fileId}.jpg`;
242
+
243
+ onUploaded(imageUrl, thumbnailUrl);
244
+ delete uploadProgress[fileId];
245
+ isUploading = false;
246
+ }
247
+
248
+ const bytesUploaded = Math.floor(file.size * (currentStep / totalSteps));
249
+ progressUpdate(bytesUploaded);
250
+ uploadProgress[fileId] = currentStep;
251
+ uploadProgress = { ...uploadProgress };
252
+ }, 50);
253
+
254
+ return {
255
+ cancel: async () => {
256
+ clearInterval(interval);
257
+ delete uploadProgress[fileId];
258
+ uploadProgress = { ...uploadProgress };
259
+ isUploading = false;
260
+ return true;
261
+ }
262
+ };
263
+ };
264
+
265
+ const confirmRemoveProductImage = async (file) => {
266
+ const result = await new Promise((resolve) => {
267
+ const modal = document.createElement('div');
268
+ modal.innerHTML = `
269
+ <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
270
+ <div style="background: white; padding: 24px; border-radius: 8px; max-width: 400px;">
271
+ <h3>Remove Image</h3>
272
+ <p>Are you sure you want to remove <strong>${file.name}</strong>?</p>
273
+ <p>This action cannot be undone.</p>
274
+ <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
275
+ <button id="cancel">Cancel</button>
276
+ <button id="confirm" style="background: #dc2626; color: white;">Remove</button>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ `;
281
+
282
+ document.body.appendChild(modal);
283
+
284
+ modal.querySelector('#cancel').onclick = () => {
285
+ document.body.removeChild(modal);
286
+ resolve(false);
287
+ };
288
+
289
+ modal.querySelector('#confirm').onclick = () => {
290
+ document.body.removeChild(modal);
291
+ resolve(true);
292
+ };
293
+ });
294
+
295
+ return result;
296
+ };
297
+
298
+ $: totalImages = productImages.length;
299
+ $: canAddMore = totalImages < 10;
300
+ </script>
301
+
302
+ <div class="product-gallery">
303
+ <h2>Product Image Gallery</h2>
304
+
305
+ <FormField label="Product Images (Max 10)" error={!canAddMore ? "Maximum 10 images allowed" : ""}>
306
+ <ImageFilesField
307
+ bind:files={productImages}
308
+ uploadFile={uploadProductImage}
309
+ removeFileConfirm={confirmRemoveProductImage}
310
+ maxFiles={10}
311
+ variant="filled"
312
+ disabled={!canAddMore}
313
+ />
314
+ </FormField>
315
+
316
+ <div class="gallery-stats">
317
+ <div class="stat-item">
318
+ <span class="stat-label">Total Images:</span>
319
+ <span class="stat-value">{totalImages}/10</span>
320
+ </div>
321
+
322
+ {#if isUploading}
323
+ <div class="upload-status">
324
+ <span>Uploading images...</span>
325
+ <div class="progress-indicators">
326
+ {#each Object.entries(uploadProgress) as [fileId, progress]}
327
+ <div class="progress-bar">
328
+ <div class="progress-fill" style="width: {progress}%"></div>
329
+ </div>
330
+ {/each}
331
+ </div>
332
+ </div>
333
+ {/if}
334
+ </div>
335
+
336
+ <div class="image-list">
337
+ <h3>Current Images</h3>
338
+ {#each productImages as image, index}
339
+ <div class="image-item">
340
+ <img src={image.thumbnail || image.uri} alt={image.name} width="50" height="50" />
341
+ <div class="image-details">
342
+ <span class="image-name">{image.name}</span>
343
+ <span class="image-position">Position {index + 1}</span>
344
+ </div>
345
+ </div>
346
+ {/each}
347
+ </div>
348
+ </div>
349
+
350
+ <style>
351
+ .product-gallery {
352
+ max-width: 800px;
353
+ margin: 20px;
354
+ padding: 24px;
355
+ border: 1px solid #e2e8f0;
356
+ border-radius: 8px;
357
+ }
358
+
359
+ .gallery-stats {
360
+ margin: 16px 0;
361
+ padding: 12px;
362
+ background: #f8fafc;
363
+ border-radius: 6px;
364
+ }
365
+
366
+ .stat-item {
367
+ display: flex;
368
+ justify-content: space-between;
369
+ margin-bottom: 8px;
370
+ }
371
+
372
+ .stat-label {
373
+ font-weight: 500;
374
+ }
375
+
376
+ .stat-value {
377
+ color: #3b82f6;
378
+ font-weight: 600;
379
+ }
380
+
381
+ .upload-status {
382
+ margin-top: 12px;
383
+ }
384
+
385
+ .progress-indicators {
386
+ margin-top: 8px;
387
+ }
388
+
389
+ .progress-bar {
390
+ height: 4px;
391
+ background: #e5e7eb;
392
+ border-radius: 2px;
393
+ overflow: hidden;
394
+ margin: 4px 0;
395
+ }
396
+
397
+ .progress-fill {
398
+ height: 100%;
399
+ background: #3b82f6;
400
+ transition: width 0.3s ease;
401
+ }
402
+
403
+ .image-list {
404
+ margin-top: 20px;
405
+ }
406
+
407
+ .image-item {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 12px;
411
+ padding: 8px;
412
+ border-bottom: 1px solid #f3f4f6;
413
+ }
414
+
415
+ .image-item img {
416
+ border-radius: 4px;
417
+ object-fit: cover;
418
+ }
419
+
420
+ .image-details {
421
+ display: flex;
422
+ flex-direction: column;
423
+ gap: 2px;
424
+ }
425
+
426
+ .image-name {
427
+ font-weight: 500;
428
+ }
429
+
430
+ .image-position {
431
+ font-size: 0.875rem;
432
+ color: #6b7280;
433
+ }
434
+ </style>
435
+ ```
436
+
437
+ ### Avatar Upload with Cropping
438
+
439
+ ```svelte
440
+ <script>
441
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
442
+ import FormField from "@ticatec/uniface-element/FormField";
443
+ import { DisplayMode } from "@ticatec/uniface-element";
444
+
445
+ let userAvatar = [];
446
+ let displayMode = DisplayMode.Edit;
447
+
448
+ const uploadAvatar = (file, progressUpdate, onUploaded, errorHandler) => {
449
+ // Validate file type
450
+ if (!file.type.startsWith('image/')) {
451
+ errorHandler(new Error('Please select an image file'));
452
+ return { cancel: async () => true };
453
+ }
454
+
455
+ // Validate file size (max 5MB)
456
+ if (file.size > 5 * 1024 * 1024) {
457
+ errorHandler(new Error('Image must be smaller than 5MB'));
458
+ return { cancel: async () => true };
459
+ }
460
+
461
+ // Create image for validation
462
+ const img = new Image();
463
+ const canvas = document.createElement('canvas');
464
+ const ctx = canvas.getContext('2d');
465
+
466
+ return new Promise((resolve) => {
467
+ img.onload = () => {
468
+ // Set canvas size for square thumbnail
469
+ const size = 200;
470
+ canvas.width = size;
471
+ canvas.height = size;
472
+
473
+ // Calculate crop dimensions (center crop to square)
474
+ const minDimension = Math.min(img.width, img.height);
475
+ const x = (img.width - minDimension) / 2;
476
+ const y = (img.height - minDimension) / 2;
477
+
478
+ // Draw cropped and resized image
479
+ ctx.drawImage(img, x, y, minDimension, minDimension, 0, 0, size, size);
480
+
481
+ // Simulate upload progress
482
+ let progress = 0;
483
+ const interval = setInterval(() => {
484
+ progress += 10;
485
+ progressUpdate(Math.floor(file.size * (progress / 100)));
486
+
487
+ if (progress >= 100) {
488
+ clearInterval(interval);
489
+
490
+ // Convert canvas to blob and create URLs
491
+ canvas.toBlob((blob) => {
492
+ const thumbnailUrl = URL.createObjectURL(blob);
493
+ const fullUrl = URL.createObjectURL(file);
494
+
495
+ onUploaded(fullUrl, thumbnailUrl);
496
+ }, 'image/jpeg', 0.8);
497
+ }
498
+ }, 100);
499
+
500
+ resolve({
501
+ cancel: async () => {
502
+ clearInterval(interval);
503
+ return true;
504
+ }
505
+ });
506
+ };
507
+
508
+ img.onerror = () => {
509
+ errorHandler(new Error('Invalid image file'));
510
+ resolve({ cancel: async () => true });
511
+ };
512
+
513
+ img.src = URL.createObjectURL(file);
514
+ });
515
+ };
516
+
517
+ const confirmRemoveAvatar = async (file) => {
518
+ return confirm('Remove your profile picture?');
519
+ };
520
+
521
+ const toggleMode = () => {
522
+ displayMode = displayMode === DisplayMode.Edit ? DisplayMode.View : DisplayMode.Edit;
523
+ };
524
+
525
+ $: hasAvatar = userAvatar.length > 0;
526
+ </script>
527
+
528
+ <div class="avatar-section">
529
+ <div class="avatar-header">
530
+ <h2>Profile Picture</h2>
531
+ <button on:click={toggleMode} class="mode-toggle">
532
+ {displayMode === DisplayMode.Edit ? 'Preview Mode' : 'Edit Mode'}
533
+ </button>
534
+ </div>
535
+
536
+ <div class="avatar-container">
537
+ <FormField label="Upload Avatar">
538
+ <ImageFilesField
539
+ bind:files={userAvatar}
540
+ uploadFile={uploadAvatar}
541
+ removeFileConfirm={confirmRemoveAvatar}
542
+ maxFiles={1}
543
+ variant="outlined"
544
+ {displayMode}
545
+ style="min-height: 200px; display: flex; align-items: center; justify-content: center;"
546
+ />
547
+ </FormField>
548
+
549
+ {#if !hasAvatar && displayMode === DisplayMode.Edit}
550
+ <div class="avatar-placeholder">
551
+ <div class="placeholder-icon">👤</div>
552
+ <p>Click to upload your profile picture</p>
553
+ <p class="placeholder-hint">Recommended: Square image, max 5MB</p>
554
+ </div>
555
+ {/if}
556
+ </div>
557
+
558
+ {#if hasAvatar}
559
+ <div class="avatar-info">
560
+ <h3>Current Avatar</h3>
561
+ <div class="avatar-details">
562
+ <img
563
+ src={userAvatar[0].thumbnail || userAvatar[0].uri}
564
+ alt="Avatar preview"
565
+ class="avatar-preview"
566
+ />
567
+ <div class="avatar-metadata">
568
+ <p><strong>File:</strong> {userAvatar[0].name}</p>
569
+ <p><strong>Status:</strong> ✅ Uploaded successfully</p>
570
+ </div>
571
+ </div>
572
+ </div>
573
+ {/if}
574
+ </div>
575
+
576
+ <style>
577
+ .avatar-section {
578
+ max-width: 500px;
579
+ margin: 20px;
580
+ padding: 24px;
581
+ border: 1px solid #e2e8f0;
582
+ border-radius: 8px;
583
+ }
584
+
585
+ .avatar-header {
586
+ display: flex;
587
+ justify-content: space-between;
588
+ align-items: center;
589
+ margin-bottom: 20px;
590
+ }
591
+
592
+ .mode-toggle {
593
+ padding: 6px 12px;
594
+ background: #f3f4f6;
595
+ border: 1px solid #d1d5db;
596
+ border-radius: 6px;
597
+ cursor: pointer;
598
+ }
599
+
600
+ .avatar-container {
601
+ position: relative;
602
+ }
603
+
604
+ .avatar-placeholder {
605
+ position: absolute;
606
+ top: 50%;
607
+ left: 50%;
608
+ transform: translate(-50%, -50%);
609
+ text-align: center;
610
+ pointer-events: none;
611
+ }
612
+
613
+ .placeholder-icon {
614
+ font-size: 48px;
615
+ margin-bottom: 12px;
616
+ }
617
+
618
+ .placeholder-hint {
619
+ font-size: 0.875rem;
620
+ color: #6b7280;
621
+ }
622
+
623
+ .avatar-info {
624
+ margin-top: 20px;
625
+ padding: 16px;
626
+ background: #f8fafc;
627
+ border-radius: 8px;
628
+ }
629
+
630
+ .avatar-details {
631
+ display: flex;
632
+ align-items: center;
633
+ gap: 16px;
634
+ }
635
+
636
+ .avatar-preview {
637
+ width: 80px;
638
+ height: 80px;
639
+ border-radius: 50%;
640
+ object-fit: cover;
641
+ border: 3px solid white;
642
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
643
+ }
644
+
645
+ .avatar-metadata p {
646
+ margin: 4px 0;
647
+ }
648
+ </style>
649
+ ```
650
+
651
+ ### Document Attachments with Thumbnails
652
+
653
+ ```svelte
654
+ <script>
655
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
656
+ import FormField from "@ticatec/uniface-element/FormField";
657
+
658
+ let documentImages = [];
659
+ let uploadErrors = [];
660
+
661
+ const uploadDocument = (file, progressUpdate, onUploaded, errorHandler) => {
662
+ // Clear previous errors
663
+ uploadErrors = [];
664
+
665
+ // Validate file type (images only for this example)
666
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
667
+ if (!allowedTypes.includes(file.type)) {
668
+ const error = new Error(`Unsupported file type: ${file.type}. Please upload JPEG, PNG, GIF, or WebP images.`);
669
+ errorHandler(error);
670
+ uploadErrors = [...uploadErrors, error.message];
671
+ return { cancel: async () => true };
672
+ }
673
+
674
+ // Simulate document processing (OCR, thumbnail generation, etc.)
675
+ let progress = 0;
676
+ const stages = [
677
+ 'Uploading file...',
678
+ 'Generating thumbnail...',
679
+ 'Processing document...',
680
+ 'Saving to database...',
681
+ 'Complete!'
682
+ ];
683
+ let currentStage = 0;
684
+
685
+ const interval = setInterval(() => {
686
+ progress += Math.random() * 15;
687
+
688
+ if (progress > (currentStage + 1) * 20 && currentStage < stages.length - 1) {
689
+ currentStage++;
690
+ console.log(stages[currentStage]);
691
+ }
692
+
693
+ if (progress >= 100) {
694
+ progress = 100;
695
+ clearInterval(interval);
696
+
697
+ // Simulate successful upload
698
+ const documentId = Date.now().toString();
699
+ const documentUrl = `/api/documents/${documentId}`;
700
+ const thumbnailUrl = `/api/documents/${documentId}/thumbnail`;
701
+
702
+ onUploaded(documentUrl, thumbnailUrl);
703
+ }
704
+
705
+ progressUpdate(Math.floor(file.size * (progress / 100)));
706
+ }, 200);
707
+
708
+ return {
709
+ cancel: async () => {
710
+ clearInterval(interval);
711
+ console.log('Upload cancelled');
712
+ return true;
713
+ }
714
+ };
715
+ };
716
+
717
+ const confirmRemoveDocument = async (file) => {
718
+ const userConfirmed = confirm(`Remove document "${file.name}"?\n\nThis will permanently delete the document and its thumbnail.`);
719
+
720
+ if (userConfirmed) {
721
+ // Simulate API call to delete document
722
+ console.log('Deleting document:', file.name);
723
+ // You could add actual deletion logic here
724
+ }
725
+
726
+ return userConfirmed;
727
+ };
728
+
729
+ $: documentCount = documentImages.length;
730
+ $: hasErrors = uploadErrors.length > 0;
731
+ </script>
732
+
733
+ <div class="document-section">
734
+ <h2>Document Attachments</h2>
735
+
736
+ <FormField label="Upload Documents" error={hasErrors ? uploadErrors.join(', ') : ''}>
737
+ <ImageFilesField
738
+ bind:files={documentImages}
739
+ uploadFile={uploadDocument}
740
+ removeFileConfirm={confirmRemoveDocument}
741
+ maxFiles={20}
742
+ variant="filled"
743
+ />
744
+ </FormField>
745
+
746
+ <div class="document-summary">
747
+ <div class="summary-stats">
748
+ <div class="stat">
749
+ <span class="stat-number">{documentCount}</span>
750
+ <span class="stat-label">Documents</span>
751
+ </div>
752
+ <div class="stat">
753
+ <span class="stat-number">{20 - documentCount}</span>
754
+ <span class="stat-label">Remaining</span>
755
+ </div>
756
+ </div>
757
+
758
+ {#if hasErrors}
759
+ <div class="error-panel">
760
+ <h4>Upload Errors</h4>
761
+ {#each uploadErrors as error}
762
+ <p class="error-message">❌ {error}</p>
763
+ {/each}
764
+ </div>
765
+ {/if}
766
+ </div>
767
+
768
+ {#if documentCount > 0}
769
+ <div class="document-list">
770
+ <h3>Uploaded Documents</h3>
771
+ <div class="document-grid">
772
+ {#each documentImages as doc, index}
773
+ <div class="document-card">
774
+ <div class="document-thumbnail">
775
+ <img src={doc.thumbnail || doc.uri} alt={doc.name} />
776
+ <div class="document-index">{index + 1}</div>
777
+ </div>
778
+ <div class="document-info">
779
+ <p class="document-name" title={doc.name}>{doc.name}</p>
780
+ <p class="document-status">✅ Processed</p>
781
+ </div>
782
+ </div>
783
+ {/each}
784
+ </div>
785
+ </div>
786
+ {/if}
787
+ </div>
788
+
789
+ <style>
790
+ .document-section {
791
+ max-width: 800px;
792
+ margin: 20px;
793
+ padding: 24px;
794
+ border: 1px solid #e2e8f0;
795
+ border-radius: 8px;
796
+ }
797
+
798
+ .document-summary {
799
+ margin: 16px 0;
800
+ }
801
+
802
+ .summary-stats {
803
+ display: flex;
804
+ gap: 24px;
805
+ margin-bottom: 16px;
806
+ }
807
+
808
+ .stat {
809
+ display: flex;
810
+ flex-direction: column;
811
+ align-items: center;
812
+ padding: 12px;
813
+ background: #f8fafc;
814
+ border-radius: 8px;
815
+ min-width: 80px;
816
+ }
817
+
818
+ .stat-number {
819
+ font-size: 1.5rem;
820
+ font-weight: 600;
821
+ color: #3b82f6;
822
+ }
823
+
824
+ .stat-label {
825
+ font-size: 0.875rem;
826
+ color: #6b7280;
827
+ }
828
+
829
+ .error-panel {
830
+ padding: 12px;
831
+ background: #fef2f2;
832
+ border: 1px solid #fecaca;
833
+ border-radius: 6px;
834
+ }
835
+
836
+ .error-message {
837
+ margin: 4px 0;
838
+ color: #dc2626;
839
+ font-size: 0.875rem;
840
+ }
841
+
842
+ .document-list {
843
+ margin-top: 24px;
844
+ }
845
+
846
+ .document-grid {
847
+ display: grid;
848
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
849
+ gap: 16px;
850
+ margin-top: 12px;
851
+ }
852
+
853
+ .document-card {
854
+ border: 1px solid #e5e7eb;
855
+ border-radius: 8px;
856
+ overflow: hidden;
857
+ background: white;
858
+ }
859
+
860
+ .document-thumbnail {
861
+ position: relative;
862
+ height: 120px;
863
+ background: #f9fafb;
864
+ display: flex;
865
+ align-items: center;
866
+ justify-content: center;
867
+ }
868
+
869
+ .document-thumbnail img {
870
+ max-width: 100%;
871
+ max-height: 100%;
872
+ object-fit: cover;
873
+ }
874
+
875
+ .document-index {
876
+ position: absolute;
877
+ top: 8px;
878
+ right: 8px;
879
+ background: #3b82f6;
880
+ color: white;
881
+ border-radius: 50%;
882
+ width: 24px;
883
+ height: 24px;
884
+ display: flex;
885
+ align-items: center;
886
+ justify-content: center;
887
+ font-size: 0.75rem;
888
+ font-weight: 600;
889
+ }
890
+
891
+ .document-info {
892
+ padding: 12px;
893
+ }
894
+
895
+ .document-name {
896
+ margin: 0 0 4px 0;
897
+ font-weight: 500;
898
+ font-size: 0.875rem;
899
+ overflow: hidden;
900
+ text-overflow: ellipsis;
901
+ white-space: nowrap;
902
+ }
903
+
904
+ .document-status {
905
+ margin: 0;
906
+ font-size: 0.75rem;
907
+ color: #16a34a;
908
+ }
909
+ </style>
910
+ ```
911
+
912
+ ### Custom Upload Progress and Error Handling
913
+
914
+ ```svelte
915
+ <script>
916
+ import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
917
+ import FormField from "@ticatec/uniface-element/FormField";
918
+
919
+ let galleryImages = [];
920
+ let activeUploads = new Map();
921
+ let uploadHistory = [];
922
+
923
+ const uploadWithErrorHandling = (file, progressUpdate, onUploaded, errorHandler) => {
924
+ const uploadId = Date.now() + Math.random();
925
+
926
+ // Add to active uploads tracking
927
+ activeUploads.set(uploadId, {
928
+ file: file.name,
929
+ startTime: new Date(),
930
+ progress: 0
931
+ });
932
+ activeUploads = new Map(activeUploads);
933
+
934
+ // Simulate various upload scenarios
935
+ const scenarios = [
936
+ { success: true, delay: 2000 }, // Success after 2s
937
+ { success: false, error: 'Network timeout', delay: 3000 }, // Network error
938
+ { success: false, error: 'Server error 500', delay: 1500 }, // Server error
939
+ { success: true, delay: 4000 }, // Slow success
940
+ ];
941
+
942
+ const scenario = scenarios[Math.floor(Math.random() * scenarios.length)];
943
+ let progress = 0;
944
+ let cancelled = false;
945
+
946
+ const interval = setInterval(() => {
947
+ if (cancelled) {
948
+ clearInterval(interval);
949
+ return;
950
+ }
951
+
952
+ progress += Math.random() * 20;
953
+
954
+ // Update tracking
955
+ if (activeUploads.has(uploadId)) {
956
+ activeUploads.get(uploadId).progress = Math.min(progress, 95);
957
+ activeUploads = new Map(activeUploads);
958
+ }
959
+
960
+ progressUpdate(Math.floor(file.size * (Math.min(progress, 95) / 100)));
961
+
962
+ if (progress >= 100) {
963
+ clearInterval(interval);
964
+
965
+ setTimeout(() => {
966
+ if (cancelled) return;
967
+
968
+ // Remove from active uploads
969
+ activeUploads.delete(uploadId);
970
+ activeUploads = new Map(activeUploads);
971
+
972
+ if (scenario.success) {
973
+ // Successful upload
974
+ const imageUrl = `/uploads/${uploadId}_${file.name}`;
975
+ const thumbnailUrl = `/uploads/thumbs/${uploadId}_${file.name}`;
976
+
977
+ onUploaded(imageUrl, thumbnailUrl);
978
+
979
+ // Add to history
980
+ uploadHistory = [{
981
+ file: file.name,
982
+ status: 'success',
983
+ timestamp: new Date(),
984
+ message: 'Upload completed successfully'
985
+ }, ...uploadHistory];
986
+ } else {
987
+ // Failed upload
988
+ const error = new Error(scenario.error);
989
+ errorHandler(error);
990
+
991
+ // Add to history
992
+ uploadHistory = [{
993
+ file: file.name,
994
+ status: 'error',
995
+ timestamp: new Date(),
996
+ message: scenario.error
997
+ }, ...uploadHistory];
998
+ }
999
+ }, scenario.delay - (progress > 95 ? (progress - 95) * 50 : 0));
1000
+ }
1001
+ }, 100);
1002
+
1003
+ return {
1004
+ cancel: async () => {
1005
+ cancelled = true;
1006
+ clearInterval(interval);
1007
+
1008
+ // Remove from active uploads
1009
+ activeUploads.delete(uploadId);
1010
+ activeUploads = new Map(activeUploads);
1011
+
1012
+ // Add to history
1013
+ uploadHistory = [{
1014
+ file: file.name,
1015
+ status: 'cancelled',
1016
+ timestamp: new Date(),
1017
+ message: 'Upload cancelled by user'
1018
+ }, ...uploadHistory];
1019
+
1020
+ return true;
1021
+ }
1022
+ };
1023
+ };
1024
+
1025
+ const confirmRemoveGalleryImage = async (file) => {
1026
+ const shouldRemove = confirm(`Remove "${file.name}" from gallery?`);
1027
+
1028
+ if (shouldRemove) {
1029
+ // Add to history
1030
+ uploadHistory = [{
1031
+ file: file.name,
1032
+ status: 'removed',
1033
+ timestamp: new Date(),
1034
+ message: 'Image removed from gallery'
1035
+ }, ...uploadHistory];
1036
+ }
1037
+
1038
+ return shouldRemove;
1039
+ };
1040
+
1041
+ const clearHistory = () => {
1042
+ uploadHistory = [];
1043
+ };
1044
+
1045
+ const formatTime = (date) => {
1046
+ return date.toLocaleTimeString();
1047
+ };
1048
+
1049
+ const getStatusIcon = (status) => {
1050
+ const icons = {
1051
+ success: '✅',
1052
+ error: '❌',
1053
+ cancelled: '⏹️',
1054
+ removed: '🗑️'
1055
+ };
1056
+ return icons[status] || '📄';
1057
+ };
1058
+
1059
+ $: hasActiveUploads = activeUploads.size > 0;
1060
+ $: totalImages = galleryImages.length;
1061
+ </script>
1062
+
1063
+ <div class="advanced-upload">
1064
+ <h2>Advanced Image Upload</h2>
1065
+
1066
+ <FormField label="Gallery Images">
1067
+ <ImageFilesField
1068
+ bind:files={galleryImages}
1069
+ uploadFile={uploadWithErrorHandling}
1070
+ removeFileConfirm={confirmRemoveGalleryImage}
1071
+ maxFiles={15}
1072
+ variant="outlined"
1073
+ />
1074
+ </FormField>
1075
+
1076
+ {#if hasActiveUploads}
1077
+ <div class="active-uploads">
1078
+ <h3>Active Uploads</h3>
1079
+ {#each Array.from(activeUploads.entries()) as [id, upload]}
1080
+ <div class="upload-item">
1081
+ <div class="upload-info">
1082
+ <span class="upload-filename">{upload.file}</span>
1083
+ <span class="upload-time">Started: {formatTime(upload.startTime)}</span>
1084
+ </div>
1085
+ <div class="upload-progress">
1086
+ <div class="progress-bar">
1087
+ <div class="progress-fill" style="width: {upload.progress}%"></div>
1088
+ </div>
1089
+ <span class="progress-text">{Math.round(upload.progress)}%</span>
1090
+ </div>
1091
+ </div>
1092
+ {/each}
1093
+ </div>
1094
+ {/if}
1095
+
1096
+ <div class="gallery-stats">
1097
+ <div class="stat-row">
1098
+ <span>Total Images: {totalImages}/15</span>
1099
+ <span>Active Uploads: {activeUploads.size}</span>
1100
+ </div>
1101
+ </div>
1102
+
1103
+ {#if uploadHistory.length > 0}
1104
+ <div class="upload-history">
1105
+ <div class="history-header">
1106
+ <h3>Upload History</h3>
1107
+ <button on:click={clearHistory} class="clear-history">Clear</button>
1108
+ </div>
1109
+
1110
+ <div class="history-list">
1111
+ {#each uploadHistory.slice(0, 10) as entry}
1112
+ <div class="history-item" class:error={entry.status === 'error'}>
1113
+ <span class="history-icon">{getStatusIcon(entry.status)}</span>
1114
+ <div class="history-details">
1115
+ <span class="history-file">{entry.file}</span>
1116
+ <span class="history-message">{entry.message}</span>
1117
+ </div>
1118
+ <span class="history-time">{formatTime(entry.timestamp)}</span>
1119
+ </div>
1120
+ {/each}
1121
+ </div>
1122
+ </div>
1123
+ {/if}
1124
+ </div>
1125
+
1126
+ <style>
1127
+ .advanced-upload {
1128
+ max-width: 800px;
1129
+ margin: 20px;
1130
+ padding: 24px;
1131
+ border: 1px solid #e2e8f0;
1132
+ border-radius: 8px;
1133
+ }
1134
+
1135
+ .active-uploads {
1136
+ margin: 20px 0;
1137
+ padding: 16px;
1138
+ background: #fef3c7;
1139
+ border-radius: 8px;
1140
+ border-left: 4px solid #f59e0b;
1141
+ }
1142
+
1143
+ .upload-item {
1144
+ display: flex;
1145
+ align-items: center;
1146
+ gap: 16px;
1147
+ margin: 8px 0;
1148
+ }
1149
+
1150
+ .upload-info {
1151
+ flex: 1;
1152
+ display: flex;
1153
+ flex-direction: column;
1154
+ gap: 2px;
1155
+ }
1156
+
1157
+ .upload-filename {
1158
+ font-weight: 500;
1159
+ }
1160
+
1161
+ .upload-time {
1162
+ font-size: 0.75rem;
1163
+ color: #6b7280;
1164
+ }
1165
+
1166
+ .upload-progress {
1167
+ display: flex;
1168
+ align-items: center;
1169
+ gap: 8px;
1170
+ min-width: 120px;
1171
+ }
1172
+
1173
+ .progress-bar {
1174
+ flex: 1;
1175
+ height: 6px;
1176
+ background: #e5e7eb;
1177
+ border-radius: 3px;
1178
+ overflow: hidden;
1179
+ }
1180
+
1181
+ .progress-fill {
1182
+ height: 100%;
1183
+ background: #3b82f6;
1184
+ transition: width 0.3s ease;
1185
+ }
1186
+
1187
+ .progress-text {
1188
+ font-size: 0.875rem;
1189
+ font-weight: 500;
1190
+ min-width: 40px;
1191
+ }
1192
+
1193
+ .gallery-stats {
1194
+ margin: 16px 0;
1195
+ padding: 12px;
1196
+ background: #f8fafc;
1197
+ border-radius: 6px;
1198
+ }
1199
+
1200
+ .stat-row {
1201
+ display: flex;
1202
+ justify-content: space-between;
1203
+ font-weight: 500;
1204
+ }
1205
+
1206
+ .upload-history {
1207
+ margin-top: 24px;
1208
+ }
1209
+
1210
+ .history-header {
1211
+ display: flex;
1212
+ justify-content: space-between;
1213
+ align-items: center;
1214
+ margin-bottom: 12px;
1215
+ }
1216
+
1217
+ .clear-history {
1218
+ padding: 4px 8px;
1219
+ background: #f3f4f6;
1220
+ border: 1px solid #d1d5db;
1221
+ border-radius: 4px;
1222
+ cursor: pointer;
1223
+ font-size: 0.875rem;
1224
+ }
1225
+
1226
+ .history-list {
1227
+ border: 1px solid #e5e7eb;
1228
+ border-radius: 6px;
1229
+ overflow: hidden;
1230
+ }
1231
+
1232
+ .history-item {
1233
+ display: flex;
1234
+ align-items: center;
1235
+ gap: 12px;
1236
+ padding: 12px;
1237
+ border-bottom: 1px solid #f3f4f6;
1238
+ background: white;
1239
+ }
1240
+
1241
+ .history-item:last-child {
1242
+ border-bottom: none;
1243
+ }
1244
+
1245
+ .history-item.error {
1246
+ background: #fef2f2;
1247
+ }
1248
+
1249
+ .history-icon {
1250
+ font-size: 1.25rem;
1251
+ }
1252
+
1253
+ .history-details {
1254
+ flex: 1;
1255
+ display: flex;
1256
+ flex-direction: column;
1257
+ gap: 2px;
1258
+ }
1259
+
1260
+ .history-file {
1261
+ font-weight: 500;
1262
+ font-size: 0.875rem;
1263
+ }
1264
+
1265
+ .history-message {
1266
+ font-size: 0.75rem;
1267
+ color: #6b7280;
1268
+ }
1269
+
1270
+ .history-time {
1271
+ font-size: 0.75rem;
1272
+ color: #9ca3af;
1273
+ min-width: 80px;
1274
+ text-align: right;
1275
+ }
1276
+ </style>
1277
+ ```
1278
+
1279
+ ## Styling
1280
+
1281
+ The ImageFilesField component can be styled using CSS custom properties:
1282
+
1283
+ ```css
1284
+ .uniface-images-field {
1285
+ --border-color: #e2e8f0;
1286
+ --background-color: #ffffff;
1287
+ --hover-background: #f9fafb;
1288
+ --upload-icon-color: #6b7280;
1289
+ --preview-overlay: rgba(0, 0, 0, 0.7);
1290
+ }
1291
+
1292
+ /* Variant-specific styling */
1293
+ .uniface-common-editor.outlined {
1294
+ border: 2px dashed var(--border-color);
1295
+ border-radius: 8px;
1296
+ padding: 20px;
1297
+ }
1298
+
1299
+ .uniface-common-editor.filled {
1300
+ background-color: #f8fafc;
1301
+ border: 1px solid var(--border-color);
1302
+ border-radius: 8px;
1303
+ padding: 20px;
1304
+ }
1305
+
1306
+ /* Upload area styling */
1307
+ .editor-action-icon {
1308
+ display: flex;
1309
+ align-items: center;
1310
+ justify-content: center;
1311
+ padding: 20px;
1312
+ border: 2px dashed #d1d5db;
1313
+ border-radius: 8px;
1314
+ cursor: pointer;
1315
+ transition: all 0.2s;
1316
+ }
1317
+
1318
+ .editor-action-icon:hover {
1319
+ border-color: #3b82f6;
1320
+ background-color: #f0f9ff;
1321
+ }
1322
+ ```
1323
+
1324
+ ## Accessibility
1325
+
1326
+ The ImageFilesField component includes several accessibility features:
1327
+
1328
+ - Keyboard navigation support
1329
+ - ARIA labels for screen readers
1330
+ - Focus management for interactive elements
1331
+ - Alt text support for images
1332
+ - Screen reader announcements for upload progress
1333
+
1334
+ ## Best Practices
1335
+
1336
+ 1. **File Validation**: Always validate file types and sizes before upload
1337
+ 2. **Error Handling**: Provide clear error messages and retry mechanisms
1338
+ 3. **Progress Feedback**: Show upload progress and allow cancellation
1339
+ 4. **Thumbnail Generation**: Generate appropriate thumbnails for performance
1340
+ 5. **Security**: Validate files on both client and server side
1341
+ 6. **Storage**: Implement proper file storage and cleanup strategies
1342
+ 7. **Performance**: Consider lazy loading for large image galleries
1343
+
1344
+ ## License
1345
+
1346
+ MIT