@dmitryvim/form-builder 0.1.8 → 0.1.9

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.
@@ -143,10 +143,89 @@
143
143
  uploadFile: null,
144
144
  downloadFile: null,
145
145
  getThumbnail: null,
146
+ getPreviewUrl: null,
146
147
  enableFilePreview: true,
147
148
  maxPreviewSize: '200px'
148
149
  };
149
150
 
151
+ // Lightweight preview modal
152
+ function ensurePreviewModal() {
153
+ let modal = document.getElementById('fb-preview-modal');
154
+ if (modal) return modal;
155
+ modal = document.createElement('div');
156
+ modal.id = 'fb-preview-modal';
157
+ modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.75);display:none;align-items:center;justify-content:center;z-index:9999;padding:24px;';
158
+ const inner = document.createElement('div');
159
+ inner.style.cssText = 'position:relative;max-width:90vw;max-height:90vh;background:#0b0b0b;border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:12px;';
160
+ const close = document.createElement('button');
161
+ close.textContent = '✕';
162
+ close.title = 'Close';
163
+ close.style.cssText = 'position:absolute;top:8px;right:8px;background:#111;color:#fff;border:1px solid #333;border-radius:6px;padding:4px 8px;cursor:pointer;';
164
+ close.addEventListener('click', () => hidePreviewModal());
165
+ const mediaWrap = document.createElement('div');
166
+ mediaWrap.style.cssText = 'display:flex;align-items:center;justify-content:center;max-height:70vh;';
167
+ const actions = document.createElement('div');
168
+ actions.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;';
169
+ const downloadBtn = document.createElement('button');
170
+ downloadBtn.textContent = 'Download';
171
+ downloadBtn.style.cssText = 'background:#e5e7eb;color:#111;border-radius:8px;padding:6px 10px;border:0;cursor:pointer;';
172
+ actions.appendChild(downloadBtn);
173
+ inner.appendChild(close);
174
+ inner.appendChild(mediaWrap);
175
+ inner.appendChild(actions);
176
+ modal.appendChild(inner);
177
+ modal.addEventListener('click', (e) => { if (e.target === modal) hidePreviewModal(); });
178
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hidePreviewModal(); });
179
+ document.body.appendChild(modal);
180
+ modal._mediaWrap = mediaWrap;
181
+ modal._downloadBtn = downloadBtn;
182
+ return modal;
183
+ }
184
+
185
+ async function showPreviewModal(resourceId, fileName, fileType) {
186
+ const modal = ensurePreviewModal();
187
+ const wrap = modal._mediaWrap;
188
+ wrap.innerHTML = '';
189
+ let src = null;
190
+ if (config.getPreviewUrl && typeof config.getPreviewUrl === 'function') {
191
+ try { src = await config.getPreviewUrl(resourceId); } catch {}
192
+ }
193
+ if (!src && config.getThumbnail && typeof config.getThumbnail === 'function') {
194
+ try { src = await config.getThumbnail(resourceId); } catch {}
195
+ }
196
+ if (fileType?.startsWith?.('image/')) {
197
+ const img = document.createElement('img');
198
+ if (src) img.src = src;
199
+ img.alt = fileName || resourceId;
200
+ img.style.cssText = 'max-width:85vw;max-height:80vh;border-radius:8px;object-fit:contain;background:#111';
201
+ wrap.appendChild(img);
202
+ } else if (fileType?.startsWith?.('video/')) {
203
+ const video = document.createElement('video');
204
+ video.controls = true;
205
+ if (src) video.src = src;
206
+ video.style.cssText = 'max-width:85vw;max-height:80vh;border-radius:8px;background:#000';
207
+ wrap.appendChild(video);
208
+ } else {
209
+ const box = document.createElement('div');
210
+ box.style.cssText = 'color:#e5e7eb;padding:24px;';
211
+ box.textContent = fileName || resourceId;
212
+ wrap.appendChild(box);
213
+ }
214
+ modal._downloadBtn.onclick = async () => {
215
+ if (config.downloadFile && typeof config.downloadFile === 'function') {
216
+ try { await config.downloadFile(resourceId, fileName || 'file'); } catch {}
217
+ } else {
218
+ console.log('Download simulated:', resourceId, fileName || 'file');
219
+ }
220
+ };
221
+ modal.style.display = 'flex';
222
+ }
223
+
224
+ function hidePreviewModal() {
225
+ const modal = document.getElementById('fb-preview-modal');
226
+ if (modal) modal.style.display = 'none';
227
+ }
228
+
150
229
  function setTextValueFromPrefill(input, element, prefillObj, key) {
151
230
  let v = undefined;
152
231
  if (prefillObj && Object.prototype.hasOwnProperty.call(prefillObj, key)) v = prefillObj[key];
@@ -243,6 +322,7 @@
243
322
 
244
323
  const preview = document.createElement('div');
245
324
  preview.className = 'flex items-center gap-3 p-2';
325
+ const isReadonly = container.closest('.file-container')?.dataset.readonly === 'true';
246
326
 
247
327
  // File icon/thumbnail
248
328
  const iconContainer = document.createElement('div');
@@ -313,21 +393,31 @@
313
393
  }
314
394
  });
315
395
 
316
- // Remove button
317
- const removeBtn = document.createElement('button');
318
- removeBtn.className = 'px-2 py-1 text-xs bg-red-500 hover:bg-red-600 text-white rounded transition-colors';
319
- removeBtn.textContent = '✕';
320
- removeBtn.title = 'Remove';
321
- removeBtn.addEventListener('click', () => {
322
- const hiddenInput = container.closest('.file-container')?.querySelector('input[type="hidden"]');
323
- if (hiddenInput) {
324
- hiddenInput.value = '';
325
- }
326
- container.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">Click to upload</div></div>';
327
- });
328
-
329
396
  actions.appendChild(downloadBtn);
330
- actions.appendChild(removeBtn);
397
+ // Open button (preview modal)
398
+ const openBtn = document.createElement('button');
399
+ openBtn.className = 'px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded transition-colors';
400
+ openBtn.textContent = 'Open';
401
+ openBtn.title = 'Open preview';
402
+ openBtn.addEventListener('click', async () => {
403
+ await showPreviewModal(resourceId, fileName, fileType);
404
+ });
405
+ actions.appendChild(openBtn);
406
+ if (!isReadonly) {
407
+ // Remove button (editable only)
408
+ const removeBtn = document.createElement('button');
409
+ removeBtn.className = 'px-2 py-1 text-xs bg-red-500 hover:bg-red-600 text-white rounded transition-colors';
410
+ removeBtn.textContent = '✕';
411
+ removeBtn.title = 'Remove';
412
+ removeBtn.addEventListener('click', () => {
413
+ const hiddenInput = container.closest('.file-container')?.querySelector('input[type="hidden"]');
414
+ if (hiddenInput) {
415
+ hiddenInput.value = '';
416
+ }
417
+ container.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">Click to upload</div></div>';
418
+ });
419
+ actions.appendChild(removeBtn);
420
+ }
331
421
  preview.appendChild(actions);
332
422
 
333
423
  container.appendChild(preview);
@@ -466,13 +556,21 @@
466
556
 
467
557
  // Preview container
468
558
  const previewContainer = document.createElement('div');
469
- previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-dashed border-gray-300 hover:border-gray-400 transition-colors relative group cursor-pointer mb-3';
470
- previewContainer.onclick = () => picker.click();
559
+ if (options.readonly) {
560
+ container.dataset.readonly = 'true';
561
+ previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-gray-300 transition-colors relative mb-3';
562
+ } else {
563
+ previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-dashed border-gray-300 hover:border-gray-400 transition-colors relative group cursor-pointer mb-3';
564
+ }
471
565
 
472
- const picker = document.createElement('input');
473
- picker.type = 'file';
474
- if (element.accept?.extensions) {
475
- picker.accept = element.accept.extensions.map(ext => `.${ext}`).join(',');
566
+ let picker = null;
567
+ if (!options.readonly) {
568
+ picker = document.createElement('input');
569
+ picker.type = 'file';
570
+ if (element.accept?.extensions) {
571
+ picker.accept = element.accept.extensions.map(ext => `.${ext}`).join(',');
572
+ }
573
+ previewContainer.onclick = () => picker.click();
476
574
  }
477
575
 
478
576
  const handleFileSelect = async (file) => {
@@ -502,11 +600,13 @@
502
600
  }
503
601
  };
504
602
 
505
- picker.addEventListener('change', async () => {
506
- if (picker.files && picker.files[0]) {
507
- await handleFileSelect(picker.files[0]);
508
- }
509
- });
603
+ if (!options.readonly) {
604
+ picker.addEventListener('change', async () => {
605
+ if (picker.files && picker.files[0]) {
606
+ await handleFileSelect(picker.files[0]);
607
+ }
608
+ });
609
+ }
510
610
 
511
611
  // Handle prefilled values
512
612
  const pv = ctx.prefill && ctx.prefill[element.key];
@@ -516,22 +616,29 @@
516
616
  const fileName = `file_${pv.slice(-8)}`;
517
617
  renderFilePreview(previewContainer, pv, fileName, 'application/octet-stream');
518
618
  } else {
519
- // Show upload prompt
520
- previewContainer.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">Click to upload</div></div>';
619
+ // Show upload prompt (editable only)
620
+ if (!options.readonly) {
621
+ previewContainer.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">Click to upload</div></div>';
622
+ } else {
623
+ previewContainer.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">No file</div></div>';
624
+ }
521
625
  }
522
626
 
523
- const helpText = document.createElement('p');
524
- helpText.className = 'text-xs text-gray-600 mt-2 text-center';
525
- helpText.innerHTML = '<span class="underline cursor-pointer">Upload</span> or drag and drop file';
526
- helpText.onclick = () => picker.click();
627
+ let helpText = null;
628
+ if (!options.readonly) {
629
+ helpText = document.createElement('p');
630
+ helpText.className = 'text-xs text-gray-600 mt-2 text-center';
631
+ helpText.innerHTML = '<span class="underline cursor-pointer">Upload</span> or drag and drop file';
632
+ helpText.onclick = () => picker.click();
633
+ }
527
634
 
528
635
  container.appendChild(previewContainer);
529
- container.appendChild(helpText);
530
- container.appendChild(picker);
636
+ if (helpText) container.appendChild(helpText);
637
+ if (picker) container.appendChild(picker);
531
638
  container.appendChild(hid);
532
639
 
533
640
  wrapper.appendChild(container);
534
- wrapper.appendChild(makeFieldHint(element, 'Returns resource ID for download/submission'));
641
+ wrapper.appendChild(makeFieldHint(element, options.readonly ? 'Read-only' : 'Returns resource ID for download/submission'));
535
642
  break;
536
643
  }
537
644
  case 'files': {
@@ -541,71 +648,90 @@
541
648
  hid.dataset.type = 'files';
542
649
 
543
650
  const list = document.createElement('div');
544
- list.className = 'flex flex-wrap gap-1.5 mt-2';
651
+ list.className = options.readonly ? 'flex flex-col gap-2 mt-2' : 'flex flex-wrap gap-1.5 mt-2';
545
652
 
546
- const picker = document.createElement('input');
547
- picker.type = 'file';
548
- picker.multiple = true;
549
- if (element.accept?.extensions) {
550
- picker.accept = element.accept.extensions.map(ext => `.${ext}`).join(',');
653
+ let picker = null;
654
+ if (!options.readonly) {
655
+ picker = document.createElement('input');
656
+ picker.type = 'file';
657
+ picker.multiple = true;
658
+ if (element.accept?.extensions) {
659
+ picker.accept = element.accept.extensions.map(ext => `.${ext}`).join(',');
660
+ }
551
661
  }
552
662
 
553
- picker.addEventListener('change', async () => {
554
- let arr = parseJSONSafe(hid.value, []);
555
- if (!Array.isArray(arr)) arr = [];
556
-
557
- if (picker.files && picker.files.length) {
558
- for (const file of picker.files) {
559
- const err = fileValidationError(element, file);
560
- if (err) {
561
- markValidity(picker, err);
562
- return;
563
- }
564
- }
663
+ if (picker) {
664
+ picker.addEventListener('change', async () => {
665
+ let arr = parseJSONSafe(hid.value, []);
666
+ if (!Array.isArray(arr)) arr = [];
565
667
 
566
- for (const file of picker.files) {
567
- try {
568
- let resourceId;
569
- if (config.uploadFile && typeof config.uploadFile === 'function') {
570
- resourceId = await config.uploadFile(file);
571
- } else {
572
- resourceId = await makeResourceIdFromFile(file);
668
+ if (picker.files && picker.files.length) {
669
+ for (const file of picker.files) {
670
+ const err = fileValidationError(element, file);
671
+ if (err) {
672
+ markValidity(picker, err);
673
+ return;
573
674
  }
574
- resourceIndex.set(resourceId, { name: file.name, type: file.type, size: file.size });
575
- arr.push(resourceId);
576
- } catch (error) {
577
- markValidity(picker, `Upload failed: ${error.message}`);
578
- return;
579
675
  }
676
+
677
+ for (const file of picker.files) {
678
+ try {
679
+ let resourceId;
680
+ if (config.uploadFile && typeof config.uploadFile === 'function') {
681
+ resourceId = await config.uploadFile(file);
682
+ } else {
683
+ resourceId = await makeResourceIdFromFile(file);
684
+ }
685
+ resourceIndex.set(resourceId, { name: file.name, type: file.type, size: file.size });
686
+ arr.push(resourceId);
687
+ } catch (error) {
688
+ markValidity(picker, `Upload failed: ${error.message}`);
689
+ return;
690
+ }
691
+ }
692
+
693
+ hid.value = JSON.stringify(arr);
694
+ renderResourcePills(list, arr, (ridToRemove) => {
695
+ const next = arr.filter(x => x !== ridToRemove);
696
+ hid.value = JSON.stringify(next);
697
+ arr = next;
698
+ renderResourcePills(list, next, arguments.callee);
699
+ });
700
+ markValidity(picker, null);
580
701
  }
581
-
582
- hid.value = JSON.stringify(arr);
583
- renderResourcePills(list, arr, (ridToRemove) => {
584
- const next = arr.filter(x => x !== ridToRemove);
585
- hid.value = JSON.stringify(next);
586
- arr = next;
587
- renderResourcePills(list, next, arguments.callee);
588
- });
589
- markValidity(picker, null);
590
- }
591
- });
702
+ });
703
+ }
592
704
 
593
705
  const pv = ctx.prefill && ctx.prefill[element.key];
594
706
  let initial = Array.isArray(pv) ? pv.filter(Boolean) : [];
595
707
  if (initial.length) {
596
708
  hid.value = JSON.stringify(initial);
597
- renderResourcePills(list, initial, (ridToRemove) => {
598
- const next = initial.filter(x => x !== ridToRemove);
599
- hid.value = JSON.stringify(next);
600
- initial = next;
601
- renderResourcePills(list, next, arguments.callee);
602
- });
709
+ if (options.readonly) {
710
+ // Render each as small preview with download
711
+ list.innerHTML = '';
712
+ for (const rid of initial) {
713
+ const row = document.createElement('div');
714
+ row.className = 'file-container';
715
+ row.dataset.readonly = 'true';
716
+ const itemPreview = document.createElement('div');
717
+ row.appendChild(itemPreview);
718
+ renderFilePreview(itemPreview, rid, `file_${rid.slice(-8)}`, 'application/octet-stream');
719
+ list.appendChild(row);
720
+ }
721
+ } else {
722
+ renderResourcePills(list, initial, (ridToRemove) => {
723
+ const next = initial.filter(x => x !== ridToRemove);
724
+ hid.value = JSON.stringify(next);
725
+ initial = next;
726
+ renderResourcePills(list, next, arguments.callee);
727
+ });
728
+ }
603
729
  }
604
730
 
605
- wrapper.appendChild(picker);
731
+ if (picker) wrapper.appendChild(picker);
606
732
  wrapper.appendChild(list);
607
733
  wrapper.appendChild(hid);
608
- wrapper.appendChild(makeFieldHint(element, 'Multiple files return resource ID array'));
734
+ wrapper.appendChild(makeFieldHint(element, options.readonly ? 'Read-only' : 'Multiple files return resource ID array'));
609
735
  break;
610
736
  }
611
737
  case 'videos': {
package/dist/sample.html CHANGED
@@ -420,7 +420,7 @@
420
420
  formEl.id = 'dynamicForm';
421
421
  formEl.addEventListener('submit', (e) => e.preventDefault());
422
422
 
423
- const ctx = { path: '', prefill: prefill || {} };
423
+ const ctx = { path: '', prefill: prefill || {}, readonly: state.config.readonly || false };
424
424
  schema.elements.forEach(element => {
425
425
  const block = renderElement(element, ctx);
426
426
  formEl.appendChild(block);
@@ -531,14 +531,34 @@
531
531
 
532
532
  // Preview container
533
533
  const previewContainer = document.createElement('div');
534
- previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-dashed border-gray-300 hover:border-gray-400 transition-colors relative group cursor-pointer mb-3';
535
- previewContainer.onclick = () => picker.click();
534
+ previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-gray-300 transition-colors relative mb-3';
536
535
 
536
+ // Read-only rendering: show preview + download only
537
+ if (ctx.readonly === true) {
538
+ previewContainer.dataset.readonly = 'true';
539
+ const pv = ctx.prefill && ctx.prefill[element.key];
540
+ if (typeof pv === 'string' && pv) {
541
+ hid.value = pv;
542
+ const fileName = `file_${pv.slice(-8)}`;
543
+ renderFilePreview(previewContainer, pv, fileName, 'application/octet-stream');
544
+ } else {
545
+ previewContainer.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">No file</div></div>';
546
+ }
547
+ container.appendChild(previewContainer);
548
+ container.appendChild(hid);
549
+ wrapper.appendChild(container);
550
+ wrapper.appendChild(makeFieldHint(element, 'Read-only'));
551
+ break;
552
+ }
553
+
554
+ // Editable mode
555
+ previewContainer.className += ' border-dashed hover:border-gray-400 cursor-pointer group';
537
556
  const picker = document.createElement('input');
538
557
  picker.type = 'file';
539
558
  if (element.accept?.extensions) {
540
559
  picker.accept = element.accept.extensions.map(ext => `.${ext}`).join(',');
541
560
  }
561
+ previewContainer.onclick = () => picker.click();
542
562
 
543
563
  const handleFileSelect = async (file) => {
544
564
  const err = fileValidationError(element, file);
@@ -607,7 +627,73 @@
607
627
 
608
628
  const list = document.createElement('div');
609
629
  list.className = 'list';
610
-
630
+
631
+ // Read-only rendering: show list of previews with download buttons
632
+ if (ctx.readonly === true) {
633
+ const pv = ctx.prefill && ctx.prefill[element.key];
634
+ const rids = Array.isArray(pv) ? pv.filter(Boolean) : [];
635
+ const renderReadonlyList = async (ridsArr) => {
636
+ list.innerHTML = '';
637
+ ridsArr.forEach(async (rid) => {
638
+ const row = document.createElement('div');
639
+ row.className = 'flex items-center gap-3 p-2 border border-gray-200 rounded';
640
+ const icon = document.createElement('div');
641
+ icon.className = 'w-12 h-12 rounded-lg flex items-center justify-center bg-blue-600 text-white text-xl flex-shrink-0';
642
+ // Try thumbnail
643
+ if (state.config.getThumbnail && typeof state.config.getThumbnail === 'function') {
644
+ try {
645
+ const thumb = await state.config.getThumbnail(rid);
646
+ if (thumb) {
647
+ const img = document.createElement('img');
648
+ img.className = 'w-12 h-12 object-cover rounded-lg';
649
+ img.src = thumb;
650
+ icon.innerHTML = '';
651
+ icon.appendChild(img);
652
+ } else {
653
+ icon.textContent = '📎';
654
+ }
655
+ } catch {
656
+ icon.textContent = '📎';
657
+ }
658
+ } else {
659
+ icon.textContent = '📎';
660
+ }
661
+ const info = document.createElement('div');
662
+ info.className = 'flex-1 text-sm text-gray-700';
663
+ info.textContent = rid;
664
+ const actions = document.createElement('div');
665
+ actions.className = 'flex items-center gap-2';
666
+ const downloadBtn = document.createElement('button');
667
+ downloadBtn.type = 'button';
668
+ downloadBtn.className = 'px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded';
669
+ downloadBtn.textContent = 'Download';
670
+ downloadBtn.addEventListener('click', async () => {
671
+ if (state.config.downloadFile && typeof state.config.downloadFile === 'function') {
672
+ try { await state.config.downloadFile(rid, rid); } catch(_) {}
673
+ } else {
674
+ console.log('Download simulated:', rid);
675
+ }
676
+ });
677
+ actions.appendChild(downloadBtn);
678
+ row.appendChild(icon);
679
+ row.appendChild(info);
680
+ row.appendChild(actions);
681
+ list.appendChild(row);
682
+ });
683
+ };
684
+ if (rids.length) {
685
+ hid.value = JSON.stringify(rids);
686
+ renderReadonlyList(rids);
687
+ } else {
688
+ list.innerHTML = '<div class="text-gray-400 text-sm">No files</div>';
689
+ }
690
+ wrapper.appendChild(list);
691
+ wrapper.appendChild(hid);
692
+ wrapper.appendChild(makeFieldHint(element, 'Read-only'));
693
+ break;
694
+ }
695
+
696
+ // Editable mode
611
697
  const picker = document.createElement('input');
612
698
  picker.type = 'file';
613
699
  picker.multiple = true;
@@ -789,6 +875,7 @@
789
875
 
790
876
  const preview = document.createElement('div');
791
877
  preview.style.cssText = 'display: flex; align-items: center; gap: 12px; padding: 8px;';
878
+ const isReadonly = state.config.readonly || container.dataset.readonly === 'true';
792
879
 
793
880
  // File icon/thumbnail
794
881
  const iconContainer = document.createElement('div');
@@ -860,22 +947,23 @@
860
947
  }
861
948
  });
862
949
 
863
- // Remove button
864
- const removeBtn = document.createElement('button');
865
- removeBtn.className = 'btn bad';
866
- removeBtn.style.cssText = 'padding: 6px 10px; font-size: 12px;';
867
- removeBtn.textContent = '✕';
868
- removeBtn.title = 'Remove';
869
- removeBtn.addEventListener('click', () => {
870
- const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
871
- if (hiddenInput) {
872
- hiddenInput.value = '';
873
- }
874
- container.innerHTML = '<div style="color: var(--muted); font-size: 14px;">📁 Click "Choose File" to upload</div>';
875
- });
876
-
877
950
  actions.appendChild(downloadBtn);
878
- actions.appendChild(removeBtn);
951
+ if (!isReadonly) {
952
+ // Remove button (editable only)
953
+ const removeBtn = document.createElement('button');
954
+ removeBtn.className = 'btn bad';
955
+ removeBtn.style.cssText = 'padding: 6px 10px; font-size: 12px;';
956
+ removeBtn.textContent = '✕';
957
+ removeBtn.title = 'Remove';
958
+ removeBtn.addEventListener('click', () => {
959
+ const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
960
+ if (hiddenInput) {
961
+ hiddenInput.value = '';
962
+ }
963
+ container.innerHTML = '<div style="color: var(--muted); font-size: 14px;">📁 Click "Choose File" to upload</div>';
964
+ });
965
+ actions.appendChild(removeBtn);
966
+ }
879
967
  preview.appendChild(actions);
880
968
 
881
969
  container.appendChild(preview);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.8",
6
+ "version": "0.1.9",
7
7
  "description": "A reusable JSON schema form builder library",
8
8
  "main": "dist/form-builder.js",
9
9
  "files": [