vgapp 1.5.1 → 1.5.3

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.
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "files": {
18
18
  "is-count": "Превышен лимит по количеству файлов",
19
- "is-sizes": "Превышен размер файл",
19
+ "is-sizes": "Превышен размер файла",
20
20
  "is-types": "Недопустимый тип файла",
21
21
  "is-total-size": "Превышен максимально разрешённый размер для выбранных файлов",
22
22
  "titles": "Удаление файлов",
@@ -460,4 +460,4 @@
460
460
  [data-vg-filepreview-state="error"] .preview,
461
461
  [data-vg-filepreview-state="empty"] .preview {
462
462
  opacity: .9;
463
- }
463
+ }
@@ -26,9 +26,10 @@ class VGFilesBase extends BaseModule {
26
26
  this._isInitialized = true;
27
27
 
28
28
  this._tpl = Html('dom');
29
- this._files = [];
30
- this._errors = new Set();
31
- this._fileObjectUrls = new Map();
29
+ this._files = [];
30
+ this._errors = new Set();
31
+ this._validationErrorsByKey = new Map();
32
+ this._fileObjectUrls = new Map();
32
33
  this._audioMetaPromises = new Map();
33
34
 
34
35
  this._nodes = {
@@ -197,9 +198,10 @@ class VGFilesBase extends BaseModule {
197
198
  filesToProcess = this._mergeFilesByOrder(incoming, this._files, Boolean(this._params.prepend));
198
199
  }
199
200
 
200
- this._files = this._filterFiles(filesToProcess);
201
-
202
- this._renderErrors();
201
+ this._files = this._filterFiles(filesToProcess);
202
+ this._syncSingleInputSubmission();
203
+
204
+ this._renderErrors();
203
205
  this._enrichAudioMetadata(this._files);
204
206
 
205
207
  return this._files;
@@ -280,47 +282,119 @@ class VGFilesBase extends BaseModule {
280
282
  return attrs;
281
283
  }
282
284
 
283
- _filterFiles(files) {
284
- this._errors.clear();
285
- const { count, sizes, total } = this._params.limits;
286
- const maxSize = sizes * 1024 * 1024;
287
- const maxTotalSize = total * 1024 * 1024;
288
-
289
- let currentTotalSize = 0;
290
- const filtered = [];
291
-
292
- for (const file of files) {
293
- if (count > 0 && filtered.length >= count) {
294
- this._errors.add('is-count');
295
- break;
296
- }
297
-
298
- let isValid = true;
299
-
300
- if (this._params.types.length && !this._params.types.includes(file.type)) {
301
- this._errors.add('is-types');
302
- isValid = false;
303
- }
304
-
305
- if (file.size > maxSize) {
306
- this._errors.add('is-sizes');
307
- isValid = false;
308
- }
309
-
310
- if (isValid && maxTotalSize > 0) {
311
- if (currentTotalSize + file.size > maxTotalSize) {
312
- this._errors.add('is-total-size');
313
- isValid = false;
314
- } else {
315
- currentTotalSize += file.size;
316
- }
317
- }
318
-
319
- if (isValid) filtered.push(file);
320
- }
321
-
322
- return filtered;
323
- }
285
+ _filterFiles(files) {
286
+ this._errors.clear();
287
+ this._validationErrorsByKey.clear();
288
+ const { count, sizes, total } = this._params.limits;
289
+ const maxSize = sizes * 1024 * 1024;
290
+ const maxTotalSize = total * 1024 * 1024;
291
+
292
+ let currentTotalSize = 0;
293
+ let validCount = 0;
294
+ const visibleFiles = [];
295
+
296
+ for (const file of files) {
297
+ const fileErrors = new Set();
298
+
299
+ if (count > 0 && validCount >= count) {
300
+ this._errors.add('is-count');
301
+ continue;
302
+ }
303
+
304
+ if (this._params.types.length && !this._params.types.includes(file.type)) {
305
+ fileErrors.add('is-types');
306
+ }
307
+
308
+ if (file.size > maxSize) {
309
+ fileErrors.add('is-sizes');
310
+ }
311
+
312
+ if (!fileErrors.size && maxTotalSize > 0 && currentTotalSize + file.size > maxTotalSize) {
313
+ fileErrors.add('is-total-size');
314
+ }
315
+
316
+ visibleFiles.push(file);
317
+
318
+ if (fileErrors.size) {
319
+ fileErrors.forEach(error => this._errors.add(error));
320
+ this._validationErrorsByKey.set(this._getFileKey(file), fileErrors);
321
+ continue;
322
+ }
323
+
324
+ validCount += 1;
325
+ currentTotalSize += file.size;
326
+ }
327
+
328
+ return visibleFiles;
329
+ }
330
+
331
+ _isFileValid(file) {
332
+ return !this._validationErrorsByKey.has(this._getFileKey(file));
333
+ }
334
+
335
+ _syncSingleInputSubmission() {
336
+ if (Number(this._params?.limits?.count) !== 1) return;
337
+
338
+ const selectedFile = this._files[0];
339
+ const canSubmit = !this._params.ajax && (!selectedFile || this._isFileValid(selectedFile));
340
+ this._preventOriginalInputFromSubmit(canSubmit);
341
+ }
342
+
343
+ _getFileValidationMessages(file) {
344
+ const errors = this._validationErrorsByKey.get(this._getFileKey(file));
345
+ if (!errors?.size) return [];
346
+
347
+ const messages = lang_messages(this._params.lang, 'files') || this._getFallbackErrors();
348
+ return Array.from(errors, error => messages[error] || error);
349
+ }
350
+
351
+ _getFileValidationLabel(file) {
352
+ const messages = this._getFileValidationMessages(file);
353
+ if (!messages.length) return '';
354
+
355
+ const fileName = this._getShortValidationFileName(file?.name);
356
+ return [fileName, messages.join('. ')].filter(Boolean).join(' — ');
357
+ }
358
+
359
+ _getFullFileValidationLabel(file) {
360
+ const messages = this._getFileValidationMessages(file);
361
+ if (!messages.length) return '';
362
+
363
+ const fileName = String(file?.name || '').trim();
364
+ return [fileName, messages.join('. ')].filter(Boolean).join(' — ');
365
+ }
366
+
367
+ _getShortValidationFileName(fileName) {
368
+ const name = String(fileName || '').trim();
369
+ if (name.length <= 24) return name;
370
+
371
+ const dotIndex = name.lastIndexOf('.');
372
+ const hasExtension = dotIndex > 0 && name.length - dotIndex <= 10;
373
+ const baseName = hasExtension ? name.slice(0, dotIndex) : name;
374
+ const extension = hasExtension ? name.slice(dotIndex) : '';
375
+
376
+ return `${baseName.slice(0, 5)}...${extension}`;
377
+ }
378
+
379
+ _decorateValidationItem(item, file) {
380
+ if (this._isFileValid(file)) return item;
381
+
382
+ Classes.add(item, 'failing');
383
+ Classes.add(item, 'validation-error');
384
+
385
+ return item;
386
+ }
387
+
388
+ _forgetFileValidation(file) {
389
+ if (!file) return;
390
+
391
+ this._validationErrorsByKey.delete(this._getFileKey(file));
392
+ this._errors.clear();
393
+ this._validationErrorsByKey.forEach(errors => {
394
+ errors.forEach(error => this._errors.add(error));
395
+ });
396
+ this._renderErrors();
397
+ }
324
398
 
325
399
  _getSizes(size, isArray = false) {
326
400
  const totalSize = isArray ? this._files.reduce((acc, f) => acc + f.size, 0) : size;
@@ -344,8 +418,12 @@ class VGFilesBase extends BaseModule {
344
418
  }
345
419
  }
346
420
 
347
- _renderErrors() {
348
- if (!this._errors.size) return;
421
+ _renderErrors() {
422
+ if (!this._errors.size) {
423
+ const $errorCont = Selectors.find(`.${this._getClass('errors')}`, this._element);
424
+ if ($errorCont) $errorCont.remove();
425
+ return;
426
+ }
349
427
 
350
428
  const messages = lang_messages(this._params.lang, 'files') || this._getFallbackErrors();
351
429
  let $errorCont = Selectors.find(`.${this._getClass('errors')}`, this._element);
@@ -357,10 +435,32 @@ class VGFilesBase extends BaseModule {
357
435
  $errorCont.innerHTML = '';
358
436
  }
359
437
 
360
- this._errors.forEach(errKey => {
361
- const msg = messages[errKey] || errKey;
362
- $errorCont.append(this._tpl.span({ class: 'error-item' }, msg));
363
- });
438
+ if (this._validationErrorsByKey.size) {
439
+ const renderedErrors = new Set();
440
+ this._files.forEach(file => {
441
+ const msg = this._getFileValidationLabel(file);
442
+ if (!msg) return;
443
+
444
+ const fileErrors = this._validationErrorsByKey.get(this._getFileKey(file));
445
+ fileErrors?.forEach(error => renderedErrors.add(error));
446
+ const fullMsg = this._getFullFileValidationLabel(file);
447
+ const attrs = { class: 'error-item' };
448
+ if (fullMsg && fullMsg !== msg) attrs.title = fullMsg;
449
+ $errorCont.append(this._tpl.span(attrs, msg));
450
+ });
451
+
452
+ this._errors.forEach(errKey => {
453
+ if (renderedErrors.has(errKey)) return;
454
+ const msg = messages[errKey] || errKey;
455
+ $errorCont.append(this._tpl.span({ class: 'error-item' }, msg));
456
+ });
457
+ return;
458
+ }
459
+
460
+ this._errors.forEach(errKey => {
461
+ const msg = messages[errKey] || errKey;
462
+ $errorCont.append(this._tpl.span({ class: 'error-item' }, msg));
463
+ });
364
464
  }
365
465
 
366
466
  _getFallbackErrors() {
@@ -466,7 +566,7 @@ class VGFilesBase extends BaseModule {
466
566
  parts
467
567
  );
468
568
 
469
- fragment.appendChild($li);
569
+ fragment.appendChild(this._decorateValidationItem($li, file));
470
570
  });
471
571
 
472
572
  $list.innerHTML = '';
@@ -641,10 +741,12 @@ class VGFilesBase extends BaseModule {
641
741
  liAttrs['data-vg-filepreview-display-name'] = displayName || file.name || '';
642
742
  }
643
743
 
644
- return this._tpl.li(
645
- this._buildFileDataAttributes(file, liAttrs),
646
- parts
647
- );
744
+ const item = this._tpl.li(
745
+ this._buildFileDataAttributes(file, liAttrs),
746
+ parts
747
+ );
748
+
749
+ return this._decorateValidationItem(item, file);
648
750
  }
649
751
 
650
752
  _renderTemplatePart(element, file, index = null, options = {}) {
@@ -926,7 +1028,7 @@ class VGFilesBase extends BaseModule {
926
1028
 
927
1029
  if (isSingle) return;
928
1030
 
929
- files.forEach((file, index) => {
1031
+ files.filter(file => this._isFileValid(file)).forEach((file, index) => {
930
1032
  const input = document.createElement('input');
931
1033
  input.type = 'file';
932
1034
  input.name = `${baseName}[${index}]`;
@@ -968,9 +1070,10 @@ class VGFilesBase extends BaseModule {
968
1070
  if (resetInput) {
969
1071
  this._resetFileInput();
970
1072
  }
971
- this._cleanupFakeInputs();
972
- this._cleanupErrors();
973
- this._files = [];
1073
+ this._cleanupFakeInputs();
1074
+ this._cleanupErrors();
1075
+ this._validationErrorsByKey.clear();
1076
+ this._files = [];
974
1077
 
975
1078
  if (this._nodes.info) {
976
1079
  Classes.remove(this._nodes.info, 'show');
@@ -5,8 +5,9 @@
5
5
  import { isElement } from "../../../utils/js/functions";
6
6
  import EventHandler from "../../../utils/js/dom/event";
7
7
  import Selectors from "../../../utils/js/dom/selectors";
8
- import { Classes } from "../../../utils/js/dom/manipulator";
9
- import BaseModule from "../../base-module";
8
+ import { Classes } from "../../../utils/js/dom/manipulator";
9
+ import BaseModule from "../../base-module";
10
+ import {isVGFilesSortableDragActive, VG_FILES_SORTABLE_DATA_TYPE} from "./sortable";
10
11
 
11
12
  const CLASS_NAME_CONTAINER = 'vg-files';
12
13
  const CLASS_NAME_DROP = `${CLASS_NAME_CONTAINER}-drop`;
@@ -253,14 +254,20 @@ class VGFilesDroppable extends BaseModule {
253
254
  }
254
255
  }
255
256
 
256
- _isSortableDrag(e) {
257
- if (document.querySelector('.dragging')) return true;
258
-
259
- try {
260
- return (e?.dataTransfer?.getData?.('text/plain') || '') === 'vgsortable';
261
- } catch (_) {
262
- return false;
263
- }
257
+ _isSortableDrag(e) {
258
+ if (isVGFilesSortableDragActive()) return true;
259
+ if (document.querySelector('.dragging')) return true;
260
+
261
+ try {
262
+ const dataTransfer = e?.dataTransfer;
263
+ const types = Array.from(dataTransfer?.types || []);
264
+ if (types.includes(VG_FILES_SORTABLE_DATA_TYPE)) return true;
265
+
266
+ return (dataTransfer?.getData?.(VG_FILES_SORTABLE_DATA_TYPE) || '') === '1' ||
267
+ (dataTransfer?.getData?.('text/plain') || '') === 'vgsortable';
268
+ } catch (_) {
269
+ return false;
270
+ }
264
271
  }
265
272
 
266
273
  _getVisibleDropZonesInViewport() {
@@ -1,9 +1,38 @@
1
- import Selectors from "../../../utils/js/dom/selectors";
2
- import {isElement, normalizeData} from "../../../utils/js/functions";
3
- import Ajax from "../../../utils/js/components/ajax";
4
- import VGToast from "../../vgtoast";
5
-
6
- class VGFilesSortable {
1
+ /**
2
+ * Описание: управляет сортировкой уже загруженных файлов VGFiles.
3
+ * Возможности: меняет порядок элементов, сохраняет ID на сервере и маркирует внутренний drag для защиты dropzone.
4
+ */
5
+ import Selectors from "../../../utils/js/dom/selectors";
6
+ import {isElement, normalizeData} from "../../../utils/js/functions";
7
+ import Ajax from "../../../utils/js/components/ajax";
8
+ import VGToast from "../../vgtoast";
9
+
10
+ export const VG_FILES_SORTABLE_DATA_TYPE = 'application/x-vg-files-sortable';
11
+
12
+ let isSortableDragActive = false;
13
+ let sortableDragClearTimer = null;
14
+
15
+ export function isVGFilesSortableDragActive() {
16
+ return isSortableDragActive;
17
+ }
18
+
19
+ function markSortableDragActive() {
20
+ if (sortableDragClearTimer) {
21
+ clearTimeout(sortableDragClearTimer);
22
+ sortableDragClearTimer = null;
23
+ }
24
+ isSortableDragActive = true;
25
+ }
26
+
27
+ function scheduleSortableDragClear() {
28
+ if (sortableDragClearTimer) clearTimeout(sortableDragClearTimer);
29
+ sortableDragClearTimer = setTimeout(() => {
30
+ isSortableDragActive = false;
31
+ sortableDragClearTimer = null;
32
+ }, 0);
33
+ }
34
+
35
+ class VGFilesSortable {
7
36
  constructor(vgFilesInstance, options = {}) {
8
37
  this._vg = vgFilesInstance;
9
38
  this._params = {
@@ -75,10 +104,12 @@ class VGFilesSortable {
75
104
  const item = e.target.closest(this._params.itemSelector) || e.target.closest('li');
76
105
  if (!item) return;
77
106
 
78
- this._draggedItem = item;
79
-
80
- e.dataTransfer.effectAllowed = 'move';
81
- e.dataTransfer.setData('text/plain', 'vgsortable');
107
+ this._draggedItem = item;
108
+ markSortableDragActive();
109
+
110
+ e.dataTransfer.effectAllowed = 'move';
111
+ e.dataTransfer.setData(VG_FILES_SORTABLE_DATA_TYPE, '1');
112
+ e.dataTransfer.setData('text/plain', 'vgsortable');
82
113
 
83
114
  item.classList.add('dragging');
84
115
  requestAnimationFrame(() => item.classList.add('dragging-transparent'));
@@ -87,9 +118,11 @@ class VGFilesSortable {
87
118
  _onDragEnd(e) {
88
119
  if (this._draggedItem) {
89
120
  this._draggedItem.classList.remove('dragging', 'dragging-transparent');
90
- this._draggedItem = null;
91
- this._saveOrder();
92
- }
121
+ this._draggedItem = null;
122
+ this._saveOrder();
123
+ }
124
+
125
+ scheduleSortableDragClear();
93
126
  }
94
127
 
95
128
  _onDragOver(e) {
@@ -142,14 +175,15 @@ class VGFilesSortable {
142
175
  }).filter(id => id !== null);
143
176
  }
144
177
 
145
- destroy() {
178
+ destroy() {
146
179
  this._list.removeEventListener('dragstart', this._boundOnDragStart);
147
180
  this._list.removeEventListener('dragend', this._boundOnDragEnd);
148
181
  this._list.removeEventListener('dragover', this._boundOnDragOver);
149
182
  this._list.removeEventListener('drop', this._boundOnDrop);
150
183
 
151
- this._draggedItem = null;
152
- }
153
- }
184
+ this._draggedItem = null;
185
+ scheduleSortableDragClear();
186
+ }
187
+ }
154
188
 
155
- export default VGFilesSortable
189
+ export default VGFilesSortable
@@ -273,8 +273,11 @@ class VGFiles extends VGFilesBase {
273
273
  this._failingUploadedKeys.clear();
274
274
  }
275
275
 
276
- const notUploadedFiles = files.filter(f => !this._uploadedKeys.has(this._getFileKey(f)));
277
- if (!notUploadedFiles.length) return;
276
+ const notUploadedFiles = files.filter(f =>
277
+ this._isFileValid(f) && !this._uploadedKeys.has(this._getFileKey(f))
278
+ );
279
+ this._setStatItem('failing', this._getFailingCount());
280
+ if (!notUploadedFiles.length) return;
278
281
 
279
282
  if (!this._uploader || this._uploader.isIdle()) {
280
283
  this._uploader = new FileUploader({
@@ -481,7 +484,7 @@ class VGFiles extends VGFilesBase {
481
484
  Classes.remove($item, CLASS_NAME_PENDING);
482
485
  Classes.replace($item, CLASS_NAME_LOADING, CLASS_NAME_FAILING);
483
486
 
484
- this._setStatItem('failing', this._failingUploadedKeys.size);
487
+ this._setStatItem('failing', this._getFailingCount());
485
488
  this._setStatItem('pending', this._pendingUploadedKeys.size);
486
489
 
487
490
  const button = this._getButtonElement(file);
@@ -509,7 +512,7 @@ class VGFiles extends VGFilesBase {
509
512
  this._triggerEvent('upload.allComplete');
510
513
  this._updateStat(false);
511
514
 
512
- if (!this._failingUploadedKeys.size) {
515
+ if (!this._getFailingCount()) {
513
516
  if (this._params.sortable?.enabled && this._params.sortable.route) {
514
517
  import('./sortable.js').then(module => {
515
518
  if (this._sortable && typeof this._sortable.destroy === 'function') {
@@ -525,7 +528,7 @@ class VGFiles extends VGFilesBase {
525
528
 
526
529
  const payload = {
527
530
  uploaded: this._uploadedKeys.size,
528
- failed: this._failingUploadedKeys.size,
531
+ failed: this._getFailingCount(),
529
532
  total: this._files.length
530
533
  };
531
534
 
@@ -693,12 +696,13 @@ class VGFiles extends VGFilesBase {
693
696
  };
694
697
 
695
698
  const fileToRemove = this._files.find(f => f.name === name && f.size === size);
696
- if (fileToRemove) {
697
- const key = this._getFileKey(fileToRemove);
698
- this._uploadedKeys.delete(key);
699
- this._pendingUploadedKeys.delete(key);
700
- this._failingUploadedKeys.delete(key);
701
- }
699
+ if (fileToRemove) {
700
+ const key = this._getFileKey(fileToRemove);
701
+ this._uploadedKeys.delete(key);
702
+ this._pendingUploadedKeys.delete(key);
703
+ this._failingUploadedKeys.delete(key);
704
+ this._forgetFileValidation(fileToRemove);
705
+ }
702
706
 
703
707
  this._getItemElement().forEach(el => {
704
708
  const btn = Selectors.find('button', el);
@@ -791,7 +795,7 @@ class VGFiles extends VGFilesBase {
791
795
  _updateStatsAfterRemove() {
792
796
  this._setStatItem('completed', this._uploadedKeys.size);
793
797
  this._setStatItem('pending', this._pendingUploadedKeys.size);
794
- this._setStatItem('failing', this._failingUploadedKeys.size);
798
+ this._setStatItem('failing', this._getFailingCount());
795
799
  this._updateStat();
796
800
  }
797
801
 
@@ -897,24 +901,35 @@ class VGFiles extends VGFilesBase {
897
901
  if ($value) $value.innerHTML = count;
898
902
  }
899
903
 
900
- _renderUIStatusDropInfoAjax(files) {
901
- if (!this._params.ajax) return;
902
-
903
- files.forEach(file => {
904
+ _renderUIStatusDropInfoAjax(files) {
905
+ if (!this._params.ajax) return;
906
+
907
+ files.forEach(file => {
904
908
  const $item = this._getItemElement(file);
905
909
  if (!$item) return;
906
910
 
907
- const key = this._getFileKey(file);
908
- if (this._uploadedKeys.has(key)) {
911
+ const key = this._getFileKey(file);
912
+ if (!this._isFileValid(file)) {
913
+ Classes.remove($item, CLASS_NAME_PENDING);
914
+ Classes.add($item, CLASS_NAME_FAILING);
915
+ } else if (this._uploadedKeys.has(key)) {
909
916
  Classes.replace($item, CLASS_NAME_PENDING, CLASS_NAME_COMPLETED);
910
917
  Classes.add($item, CLASS_NAME_LOADED);
911
918
  } else if (this._failingUploadedKeys.has(key)) {
912
919
  Classes.replace($item, CLASS_NAME_PENDING, CLASS_NAME_FAILING);
913
920
  } else {
914
- Classes.add($item, CLASS_NAME_PENDING);
915
- }
916
- });
917
- }
921
+ Classes.add($item, CLASS_NAME_PENDING);
922
+ }
923
+ });
924
+
925
+ this._setStatItem('failing', this._getFailingCount());
926
+ }
927
+
928
+ _getFailingCount() {
929
+ const keys = new Set(this._failingUploadedKeys);
930
+ this._validationErrorsByKey.forEach((errors, key) => keys.add(key));
931
+ return keys.size;
932
+ }
918
933
 
919
934
  _triggerCallback(name, data) {
920
935
  const cb = this._params.callbacks?.[name];
@@ -28,7 +28,8 @@
28
28
  &.pending,
29
29
  &.loading,
30
30
  &.failing {
31
- .file-actions {
31
+ .file-actions,
32
+ .file-custom > :not(.file-remove):not(.file-info) {
32
33
  display: none;
33
34
  }
34
35
  }
@@ -9,9 +9,16 @@ $files-map: (
9
9
  label-padding: 8px 24px,
10
10
  label-padding-icon: 8px 16px,
11
11
 
12
- // errors
13
- errors-padding-y: 10px,
14
- errors-font-size: 16px,
12
+ // errors
13
+ errors-padding-y: 10px,
14
+ errors-padding-x: 12px,
15
+ errors-font-size: 16px,
16
+ errors-gap: 4px,
17
+ errors-margin-bottom: 8px,
18
+ errors-bg: var(--vg-danger-subtle-bg, rgba(255, 42, 44, .06)),
19
+ errors-border-color: var(--vg-danger-color, var(--vg-danger, #ff2a2c)),
20
+ errors-border-width: 3px,
21
+ errors-border-radius: 6px,
15
22
 
16
23
  // drop zone
17
24
  drop-bg: var(--vg-secondary-bg, #{vg.$surface-muted-bg}),
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  *--------------------------------------------------------------------------
3
3
  * Модуль: VGFiles
4
4
  * Автор: Vegas DEV
@@ -61,12 +61,18 @@
61
61
  }
62
62
  }
63
63
 
64
- &-errors {
65
- padding: var(--vg-files-errors-padding-y) 0;
66
-
67
- span {
68
- font-size: var(--vg-files-errors-font-size);
69
- color: var(--vg-danger);
64
+ &-errors {
65
+ display: grid;
66
+ gap: var(--vg-files-errors-gap);
67
+ padding: var(--vg-files-errors-padding-y) var(--vg-files-errors-padding-x);
68
+ margin-bottom: var(--vg-files-errors-margin-bottom);
69
+ border-left: var(--vg-files-errors-border-width) solid var(--vg-files-errors-border-color);
70
+ border-radius: var(--vg-files-errors-border-radius);
71
+ background: var(--vg-files-errors-bg);
72
+
73
+ span {
74
+ font-size: var(--vg-files-errors-font-size);
75
+ color: var(--vg-danger-color, var(--vg-danger, #ff2a2c));
70
76
  display: block;
71
77
  overflow: hidden;
72
78
  text-overflow: ellipsis;
@@ -517,14 +523,18 @@
517
523
  }
518
524
  }
519
525
 
520
- .file-failing-actions {
526
+ .file-failing-actions {
521
527
  display: inline-flex;
522
528
  align-items: center;
523
529
  gap: var(--vg-files-info-failing-actions-gap);
524
- }
525
-
526
- &-info--list {
527
- .file.failing {
530
+ }
531
+
532
+ &-info--list {
533
+ .file.validation-error {
534
+ box-shadow: inset 0 0 0 2px var(--vg-danger-color, var(--vg-danger, #ff2a2c));
535
+ }
536
+
537
+ .file.failing {
528
538
  .file-remove {
529
539
  margin-left: var(--vg-files-info-failing-actions-margin-start);
530
540
  }
@@ -634,7 +644,7 @@
634
644
  }
635
645
  }
636
646
 
637
- &--list {
647
+ &--list {
638
648
  display: flex;
639
649
  align-items: center;
640
650
  gap: var(--vg-files-drop-list-gap);
@@ -646,7 +656,7 @@
646
656
  border-radius: var(--vg-files-drop-radius);
647
657
  overflow: hidden;
648
658
 
649
- .file {
659
+ .file {
650
660
  --vg-button-color: var(--vg-text-color, var(--vg-body-color, #000000));
651
661
  border-radius: var(--vg-files-drop-file-radius);
652
662
  position: relative;
@@ -655,7 +665,11 @@
655
665
  height: var(--vg-files-drop-file-height);
656
666
 
657
667
  @include files.vgfiles-sortable();
658
- @include files.vgfiles-state();
668
+ @include files.vgfiles-state();
669
+
670
+ &.validation-error {
671
+ box-shadow: inset 0 0 0 2px var(--vg-danger-color, var(--vg-danger, #ff2a2c));
672
+ }
659
673
 
660
674
  .file-image {
661
675
  width: 100%;