@5minds/node-red-dashboard-2-processcube-dynamic-form 2.2.1-file-preview-2-247fbf-mfdx0ry5 → 2.2.1-file-preview-2-892712-mffeuvkr

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.
@@ -79,6 +79,53 @@
79
79
  {{ field.customForm ? field.customForm.hint : undefined }}
80
80
  </p>
81
81
  </div>
82
+ <div v-else-if="createComponent(field).isFileField" class="ui-dynamic-form-file-wrapper">
83
+ <div v-if="getFilePreviewsForField(field.id).length > 0" class="ui-dynamic-form-image-previews">
84
+ <h4>Current files:</h4>
85
+ <div class="ui-dynamic-form-preview-grid">
86
+ <div
87
+ v-for="preview in getFilePreviewsForField(field.id)"
88
+ :key="preview.name"
89
+ class="ui-dynamic-form-preview-item"
90
+ >
91
+ <img
92
+ :src="preview.url"
93
+ :alt="preview.name"
94
+ class="ui-dynamic-form-preview-image"
95
+ @click="openImageModal(preview)"
96
+ />
97
+ <div class="ui-dynamic-form-preview-info">
98
+ <span class="ui-dynamic-form-preview-name">{{ preview.name }}</span>
99
+ <span class="ui-dynamic-form-preview-size">{{ formatFileSize(preview.size) }}</span>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <FormKit
106
+ type="file"
107
+ :id="field.id"
108
+ :name="createComponent(field).name"
109
+ :label="field.label"
110
+ :required="field.required"
111
+ v-model="formData[field.id]"
112
+ :help="createComponent(field).hint"
113
+ innerClass="reset-background"
114
+ wrapperClass="$remove:formkit-wrapper"
115
+ labelClass="ui-dynamic-form-input-label"
116
+ :inputClass="`input-${theme}`"
117
+ :readonly="createComponent(field).isReadOnly"
118
+ :disabled="createComponent(field).isReadOnly"
119
+ :multiple="createComponent(field).multiple"
120
+ :validation="createComponent(field).validation"
121
+ validationVisibility="live"
122
+ :ref="
123
+ (el) => {
124
+ if (index === 0) firstFormFieldRef = el;
125
+ }
126
+ "
127
+ />
128
+ </div>
82
129
  <component
83
130
  :is="createComponent(field).type"
84
131
  v-else
@@ -121,6 +168,27 @@
121
168
  <div v-if="!props.actions_inside_card && hasUserTask && actions.length > 0" style="padding-top: 32px">
122
169
  <UIDynamicFormFooterAction :actions="actions" :actionCallback="actionFn" />
123
170
  </div>
171
+
172
+ <v-dialog v-model="imageModalOpen" max-width="800px">
173
+ <v-card v-if="modalImage">
174
+ <v-card-title class="headline">{{ modalImage.name }}</v-card-title>
175
+ <v-card-text>
176
+ <img
177
+ :src="modalImage.url"
178
+ :alt="modalImage.name"
179
+ style="width: 100%; max-height: 70vh; object-fit: contain"
180
+ />
181
+ <div style="margin-top: 16px">
182
+ <strong>Size:</strong> {{ formatFileSize(modalImage.size) }}<br />
183
+ <strong>Type:</strong> {{ modalImage.type }}
184
+ </div>
185
+ </v-card-text>
186
+ <v-card-actions>
187
+ <v-spacer></v-spacer>
188
+ <v-btn color="primary" text @click="closeImageModal">Close</v-btn>
189
+ </v-card-actions>
190
+ </v-card>
191
+ </v-dialog>
124
192
  </div>
125
193
  </template>
126
194
 
@@ -215,6 +283,7 @@ export default {
215
283
  return {
216
284
  actions: [],
217
285
  formData: {},
286
+ originalFileData: {},
218
287
  userTask: null,
219
288
  theme: '',
220
289
  errorMsg: '',
@@ -222,6 +291,10 @@ export default {
222
291
  msg: null,
223
292
  collapsed: false,
224
293
  firstFormFieldRef: null,
294
+ imageModalOpen: false,
295
+ modalImage: null,
296
+ objectUrlsByField: {},
297
+ previewCache: {},
225
298
  };
226
299
  },
227
300
  computed: {
@@ -258,27 +331,55 @@ export default {
258
331
  watch: {
259
332
  formData: {
260
333
  handler(newData, oldData) {
334
+ if (!oldData) return;
335
+
336
+ if (this.userTask && this.userTask.userTaskConfig && this.userTask.userTaskConfig.formFields) {
337
+ const fileFields = this.userTask.userTaskConfig.formFields.filter((field) => field.type === 'file');
338
+
339
+ fileFields.forEach((field) => {
340
+ const fieldId = field.id;
341
+ const newValue = newData[fieldId];
342
+ const oldValue = oldData[fieldId];
343
+
344
+ if (newValue !== oldValue && this.originalFileData[fieldId]) {
345
+ const hasNewFiles = this.hasActualFileObjects(newValue);
346
+
347
+ if (hasNewFiles) {
348
+ delete this.originalFileData[fieldId];
349
+
350
+ Object.keys(this.previewCache).forEach((key) => {
351
+ if (key.startsWith(fieldId + '_')) {
352
+ delete this.previewCache[key];
353
+ }
354
+ });
355
+
356
+ this.cleanupObjectUrls(fieldId);
357
+ }
358
+ }
359
+ });
360
+ }
361
+
261
362
  if (this.props.trigger_on_change) {
262
363
  const res = { payload: { formData: newData, userTask: this.userTask } };
263
364
  this.send(res, this.totalOutputs - 1);
264
365
  }
265
366
  },
266
- collapsed(newVal) {
267
- if (!newVal && this.hasUserTask) {
268
- nextTick(() => {
269
- this.focusFirstFormField();
270
- });
271
- }
272
- },
273
- userTask(newVal) {
274
- if (newVal && !this.collapsed) {
275
- nextTick(() => {
276
- this.focusFirstFormField();
277
- });
278
- }
279
- },
280
367
  deep: true,
281
368
  },
369
+ collapsed(newVal) {
370
+ if (!newVal && this.hasUserTask) {
371
+ nextTick(() => {
372
+ this.focusFirstFormField();
373
+ });
374
+ }
375
+ },
376
+ userTask(newVal) {
377
+ if (newVal && !this.collapsed) {
378
+ nextTick(() => {
379
+ this.focusFirstFormField();
380
+ });
381
+ }
382
+ },
282
383
  },
283
384
  created() {
284
385
  const currentPath = window.location.pathname;
@@ -314,6 +415,7 @@ export default {
314
415
  },
315
416
  unmounted() {
316
417
  this.$socket?.off('msg-input:' + this.id);
418
+ this.cleanupObjectUrls();
317
419
  },
318
420
  methods: {
319
421
  createComponent(field) {
@@ -495,25 +597,17 @@ export default {
495
597
  case 'file':
496
598
  const multiple = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true' : false;
497
599
  return {
498
- type: 'FormKit',
600
+ type: 'div',
499
601
  props: {
500
- type: 'file',
501
- id: field.id,
502
- name,
503
- label: field.label,
504
- required: field.required,
505
- value: this.formData[field.id],
506
- help: hint,
507
- innerClass: 'reset-background',
508
- wrapperClass: '$remove:formkit-wrapper',
509
- labelClass: 'ui-dynamic-form-input-label',
510
- inputClass: `input-${this.theme}`,
511
- readonly: isReadOnly,
512
- disabled: isReadOnly,
513
- multiple,
514
- validation,
515
- validationVisibility: 'live',
602
+ class: 'ui-dynamic-form-file-wrapper',
516
603
  },
604
+ isFileField: true,
605
+ field: field,
606
+ multiple: multiple,
607
+ isReadOnly: isReadOnly,
608
+ hint: hint,
609
+ validation: validation,
610
+ name: name,
517
611
  };
518
612
  case 'checkbox':
519
613
  const options = JSON.parse(JSON.stringify(field.customForm)).entries.map((obj) => {
@@ -874,6 +968,9 @@ export default {
874
968
  return;
875
969
  }
876
970
 
971
+ this.originalFileData = {};
972
+ this.previewCache = {};
973
+ this.cleanupObjectUrls();
877
974
  this.actions = this.props.options;
878
975
 
879
976
  const hasTask = msg.payload && msg.payload.userTask;
@@ -883,14 +980,14 @@ export default {
883
980
  } else {
884
981
  this.userTask = null;
885
982
  this.formData = {};
983
+ this.originalFileData = {};
984
+ this.cleanupObjectUrls();
886
985
  return;
887
986
  }
888
987
 
889
988
  const formFields = this.userTask.userTaskConfig.formFields;
890
989
  const formFieldIds = formFields.map((ff) => ff.id);
891
990
  const initialValues = this.userTask.startToken;
892
- console.log('Initial Values:', initialValues);
893
- console.log('Form Fields:', formFields);
894
991
  const finishedFormData = msg.payload.formData;
895
992
  this.formIsFinished = !!msg.payload.formData;
896
993
  if (this.formIsFinished) {
@@ -972,7 +1069,12 @@ export default {
972
1069
  Object.keys(initialValues)
973
1070
  .filter((key) => formFieldIds.includes(key))
974
1071
  .forEach((key) => {
975
- this.formData[key] = initialValues[key];
1072
+ const field = formFields.find((f) => f.id === key);
1073
+ if (field && field.type === 'file') {
1074
+ this.formData[key] = this.transformBase64ToFormKitFormat(initialValues[key], field);
1075
+ } else {
1076
+ this.formData[key] = initialValues[key];
1077
+ }
976
1078
  });
977
1079
  }
978
1080
 
@@ -980,7 +1082,12 @@ export default {
980
1082
  Object.keys(finishedFormData)
981
1083
  .filter((key) => formFieldIds.includes(key))
982
1084
  .forEach((key) => {
983
- this.formData[key] = finishedFormData[key];
1085
+ const field = formFields.find((f) => f.id === key);
1086
+ if (field && field.type === 'file') {
1087
+ this.formData[key] = this.transformBase64ToFormKitFormat(finishedFormData[key], field);
1088
+ } else {
1089
+ this.formData[key] = finishedFormData[key];
1090
+ }
984
1091
  });
985
1092
  }
986
1093
 
@@ -1000,14 +1107,8 @@ export default {
1000
1107
  const formFields = this.userTask.userTaskConfig.formFields;
1001
1108
 
1002
1109
  try {
1003
- console.log(
1004
- 'Processing file fields:',
1005
- formFields.filter((f) => f.type === 'file')
1006
- );
1007
1110
  processedFormData = await this.processFileFields(processedFormData, formFields);
1008
- console.log('File processing completed successfully');
1009
1111
  } catch (error) {
1010
- console.error('Error processing file fields:', error);
1011
1112
  this.showError('Fehler beim Verarbeiten der Dateien');
1012
1113
  return;
1013
1114
  }
@@ -1091,6 +1192,88 @@ export default {
1091
1192
  }
1092
1193
  }
1093
1194
  },
1195
+ /**
1196
+ * Transforms base64 file data to FormKit-compatible format for display.
1197
+ *
1198
+ * FormKit file inputs expect an array of objects with 'name' property for display.
1199
+ * This function converts the base64 format (with 'data' property) to the display format,
1200
+ * while preserving the original data in originalFileData for form submission.
1201
+ *
1202
+ * Expected input format from initialValues:
1203
+ * {
1204
+ * name: "filename.ext",
1205
+ * size: 12345,
1206
+ * type: "image/png",
1207
+ * data: "base64string..."
1208
+ * }
1209
+ *
1210
+ * Output format for FormKit:
1211
+ * [
1212
+ * { name: "filename.ext" }
1213
+ * ]
1214
+ *
1215
+ * @param {Object|Array} fileData - The file data from initial values
1216
+ * @param {Object} field - The form field configuration
1217
+ * @returns {Array} FormKit-compatible file array
1218
+ */
1219
+ transformBase64ToFormKitFormat(fileData, field) {
1220
+ if (
1221
+ Array.isArray(fileData) &&
1222
+ fileData.length > 0 &&
1223
+ typeof fileData[0] === 'object' &&
1224
+ fileData[0].name &&
1225
+ !fileData[0].data
1226
+ ) {
1227
+ return fileData;
1228
+ }
1229
+
1230
+ const fieldId = field.id;
1231
+ const multiple = field.customForm ? JSON.parse(JSON.stringify(field.customForm)).multiple === 'true' : false;
1232
+
1233
+ if (fileData && typeof fileData === 'object' && fileData.name && fileData.data) {
1234
+ this.originalFileData[fieldId] = fileData;
1235
+
1236
+ const formKitFile = {
1237
+ name: fileData.name,
1238
+ };
1239
+
1240
+ return multiple ? [formKitFile] : [formKitFile];
1241
+ }
1242
+
1243
+ if (Array.isArray(fileData)) {
1244
+ this.originalFileData[fieldId] = fileData;
1245
+
1246
+ const formKitFiles = fileData
1247
+ .filter((file) => file && typeof file === 'object' && file.name)
1248
+ .map((file) => ({
1249
+ name: file.name,
1250
+ }));
1251
+
1252
+ return formKitFiles;
1253
+ }
1254
+
1255
+ return [];
1256
+ },
1257
+ hasActualFileObjects(value) {
1258
+ if (!value) return false;
1259
+
1260
+ if (Array.isArray(value)) {
1261
+ return value.some((item) => {
1262
+ return (
1263
+ item instanceof File ||
1264
+ (item && item.file instanceof File) ||
1265
+ (item && item.file instanceof FileList && item.file.length > 0)
1266
+ );
1267
+ });
1268
+ }
1269
+
1270
+ return (
1271
+ value instanceof File ||
1272
+ (value instanceof FileList && value.length > 0) ||
1273
+ (value && value.file instanceof File) ||
1274
+ (value && value.file instanceof FileList && value.file.length > 0)
1275
+ );
1276
+ },
1094
1277
  async convertFileToBase64(file) {
1095
1278
  return new Promise((resolve, reject) => {
1096
1279
  const reader = new FileReader();
@@ -1114,6 +1297,12 @@ export default {
1114
1297
  for (const field of formFields) {
1115
1298
  if (field.type === 'file') {
1116
1299
  const fieldValue = processedData[field.id];
1300
+ const fieldId = field.id;
1301
+
1302
+ if (this.originalFileData[fieldId]) {
1303
+ processedData[fieldId] = this.originalFileData[fieldId];
1304
+ continue;
1305
+ }
1117
1306
 
1118
1307
  if (fieldValue) {
1119
1308
  const multiple = field.customForm
@@ -1121,7 +1310,6 @@ export default {
1121
1310
  : false;
1122
1311
 
1123
1312
  if (multiple && Array.isArray(fieldValue)) {
1124
- // Handle multiple files
1125
1313
  const base64Files = [];
1126
1314
  for (const file of fieldValue) {
1127
1315
  const processedFile = await this.processIndividualFile(file);
@@ -1184,6 +1372,158 @@ export default {
1184
1372
 
1185
1373
  return fileData;
1186
1374
  },
1375
+ isImageFile(fileData) {
1376
+ if (!fileData || typeof fileData !== 'object') return false;
1377
+
1378
+ const mimeType = fileData.type;
1379
+ if (!mimeType) return false;
1380
+
1381
+ return mimeType.startsWith('image/');
1382
+ },
1383
+ generateImagePreviewUrl(fileData) {
1384
+ if (!fileData || !fileData.data || !fileData.type) return null;
1385
+
1386
+ return `data:${fileData.type};base64,${fileData.data}`;
1387
+ },
1388
+ getFilePreviewsForField(fieldId) {
1389
+ const currentData = this.formData[fieldId] || null;
1390
+ const cachedOriginalData = this.originalFileData[fieldId] || null;
1391
+ const cacheKey = `${fieldId}_${JSON.stringify(currentData)}_${JSON.stringify(cachedOriginalData)}`;
1392
+
1393
+ if (this.previewCache[cacheKey]) {
1394
+ return this.previewCache[cacheKey];
1395
+ }
1396
+
1397
+ const currentFormData = this.formData[fieldId];
1398
+ const originalFileData = this.originalFileData[fieldId];
1399
+
1400
+ if (!currentFormData && !originalFileData) {
1401
+ this.previewCache[cacheKey] = [];
1402
+ return [];
1403
+ }
1404
+
1405
+ let previews = [];
1406
+
1407
+ const hasNewFileObjects =
1408
+ currentFormData &&
1409
+ Array.isArray(currentFormData) &&
1410
+ currentFormData.length > 0 &&
1411
+ this.hasActualFileObjects(currentFormData);
1412
+
1413
+ if (hasNewFileObjects) {
1414
+ previews = this.generatePreviewsFromCurrentFiles(currentFormData, fieldId);
1415
+ } else if (originalFileData) {
1416
+ const fileArray = Array.isArray(originalFileData) ? originalFileData : [originalFileData];
1417
+ previews = fileArray
1418
+ .filter((file) => this.isImageFile(file))
1419
+ .map((file) => ({
1420
+ name: file.name,
1421
+ url: this.generateImagePreviewUrl(file),
1422
+ size: file.size,
1423
+ type: file.type,
1424
+ }));
1425
+ }
1426
+
1427
+ this.previewCache[cacheKey] = previews;
1428
+
1429
+ return previews;
1430
+ },
1431
+ generatePreviewsFromCurrentFiles(fileArray, fieldId = null) {
1432
+ if (fieldId && this.objectUrlsByField[fieldId]) {
1433
+ this.objectUrlsByField[fieldId].forEach((url) => URL.revokeObjectURL(url));
1434
+ this.objectUrlsByField[fieldId] = [];
1435
+ } else if (fieldId) {
1436
+ this.objectUrlsByField[fieldId] = [];
1437
+ }
1438
+
1439
+ const previews = fileArray
1440
+ .filter((fileItem) => {
1441
+ let actualFile = null;
1442
+
1443
+ if (fileItem instanceof File) {
1444
+ actualFile = fileItem;
1445
+ } else if (fileItem && fileItem.file instanceof File) {
1446
+ actualFile = fileItem.file;
1447
+ } else if (Array.isArray(fileItem) && fileItem[0] instanceof File) {
1448
+ actualFile = fileItem[0];
1449
+ }
1450
+
1451
+ return actualFile && actualFile.type && actualFile.type.startsWith('image/');
1452
+ })
1453
+ .map((fileItem) => {
1454
+ let actualFile = null;
1455
+
1456
+ if (fileItem instanceof File) {
1457
+ actualFile = fileItem;
1458
+ } else if (fileItem && fileItem.file instanceof File) {
1459
+ actualFile = fileItem.file;
1460
+ } else if (Array.isArray(fileItem) && fileItem[0] instanceof File) {
1461
+ actualFile = fileItem[0];
1462
+ }
1463
+
1464
+ const url = actualFile ? URL.createObjectURL(actualFile) : null;
1465
+
1466
+ if (url && fieldId) {
1467
+ if (!this.objectUrlsByField[fieldId]) {
1468
+ this.objectUrlsByField[fieldId] = [];
1469
+ }
1470
+ this.objectUrlsByField[fieldId].push(url);
1471
+ }
1472
+
1473
+ return {
1474
+ name: actualFile.name,
1475
+ url: url,
1476
+ size: actualFile.size,
1477
+ type: actualFile.type,
1478
+ isObjectUrl: true,
1479
+ };
1480
+ })
1481
+ .filter((preview) => preview.url !== null);
1482
+
1483
+ return previews;
1484
+ },
1485
+ formatFileSize(bytes) {
1486
+ if (!bytes) return '0 B';
1487
+
1488
+ const k = 1024;
1489
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1490
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1491
+
1492
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1493
+ },
1494
+ openImageModal(preview) {
1495
+ this.modalImage = preview;
1496
+ this.imageModalOpen = true;
1497
+ },
1498
+ closeImageModal() {
1499
+ this.imageModalOpen = false;
1500
+ this.modalImage = null;
1501
+ },
1502
+ cleanupObjectUrls(fieldId = null) {
1503
+ if (fieldId) {
1504
+ if (this.objectUrlsByField[fieldId]) {
1505
+ this.objectUrlsByField[fieldId].forEach((url) => {
1506
+ URL.revokeObjectURL(url);
1507
+ });
1508
+ this.objectUrlsByField[fieldId] = [];
1509
+ }
1510
+
1511
+ Object.keys(this.previewCache).forEach((key) => {
1512
+ if (key.startsWith(fieldId + '_')) {
1513
+ delete this.previewCache[key];
1514
+ }
1515
+ });
1516
+ } else {
1517
+ Object.keys(this.objectUrlsByField).forEach((fieldId) => {
1518
+ this.objectUrlsByField[fieldId].forEach((url) => {
1519
+ URL.revokeObjectURL(url);
1520
+ });
1521
+ });
1522
+ this.objectUrlsByField = {};
1523
+
1524
+ this.previewCache = {};
1525
+ }
1526
+ },
1187
1527
  },
1188
1528
  };
1189
1529
 
@@ -68,7 +68,7 @@ code {
68
68
  .ui-dynamic-form-title-default {
69
69
  background: linear-gradient(131deg, #18181a 26.76%, #242326 100.16%);
70
70
  padding: 32px;
71
- color: #ffffff;
71
+ color: #fff;
72
72
  font-size: 36px;
73
73
  font-weight: 700;
74
74
  border-radius: 8px 8px 0px 0px;
@@ -593,3 +593,111 @@ code {
593
593
  margin: revert;
594
594
  padding: revert;
595
595
  }
596
+
597
+ .ui-dynamic-form-image-previews {
598
+ margin-bottom: 16px;
599
+ padding: 16px;
600
+ border: 1px solid #e0e0e0;
601
+ border-radius: 8px;
602
+ background-color: #f9f9f9;
603
+ }
604
+
605
+ .ui-dynamic-form-image-previews h4 {
606
+ margin: 0 0 12px 0;
607
+ color: #333;
608
+ font-size: 14px;
609
+ font-weight: 600;
610
+ }
611
+
612
+ .ui-dynamic-form-preview-grid {
613
+ display: flex;
614
+ flex-wrap: wrap;
615
+ gap: 12px;
616
+ }
617
+
618
+ .ui-dynamic-form-preview-item {
619
+ display: flex;
620
+ flex-direction: column;
621
+ align-items: center;
622
+ padding: 8px;
623
+ border: 1px solid #ddd;
624
+ border-radius: 6px;
625
+ background-color: white;
626
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
627
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
628
+ max-width: 150px;
629
+ }
630
+
631
+ .ui-dynamic-form-preview-item:hover {
632
+ transform: translateY(-2px);
633
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
634
+ }
635
+
636
+ .ui-dynamic-form-preview-image {
637
+ width: 120px;
638
+ height: 80px;
639
+ object-fit: cover;
640
+ border-radius: 4px;
641
+ cursor: pointer;
642
+ border: 2px solid transparent;
643
+ transition: border-color 0.2s ease;
644
+ }
645
+
646
+ .ui-dynamic-form-preview-image:hover {
647
+ border-color: #1976d2;
648
+ }
649
+
650
+ .ui-dynamic-form-preview-info {
651
+ display: flex;
652
+ flex-direction: column;
653
+ align-items: center;
654
+ margin-top: 8px;
655
+ text-align: center;
656
+ }
657
+
658
+ .ui-dynamic-form-preview-name {
659
+ font-size: 12px;
660
+ font-weight: 500;
661
+ color: #333;
662
+ word-break: break-word;
663
+ max-width: 120px;
664
+ overflow: hidden;
665
+ text-overflow: ellipsis;
666
+ display: -webkit-box;
667
+ -webkit-line-clamp: 2;
668
+ line-clamp: 2;
669
+ -webkit-box-orient: vertical;
670
+ }
671
+
672
+ .ui-dynamic-form-preview-size {
673
+ font-size: 10px;
674
+ color: #666;
675
+ margin-top: 4px;
676
+ }
677
+
678
+ .ui-dynamic-form-file-wrapper {
679
+ width: 100%;
680
+ }
681
+
682
+ /* Dark theme adjustments */
683
+ .ui-dynamic-form-dark .ui-dynamic-form-image-previews {
684
+ background-color: #2d2d2d;
685
+ border-color: #555;
686
+ }
687
+
688
+ .ui-dynamic-form-dark .ui-dynamic-form-image-previews h4 {
689
+ color: #fff;
690
+ }
691
+
692
+ .ui-dynamic-form-dark .ui-dynamic-form-preview-item {
693
+ background-color: #3d3d3d;
694
+ border-color: #555;
695
+ }
696
+
697
+ .ui-dynamic-form-dark .ui-dynamic-form-preview-name {
698
+ color: #fff;
699
+ }
700
+
701
+ .ui-dynamic-form-dark .ui-dynamic-form-preview-size {
702
+ color: #ccc;
703
+ }