webspresso 0.0.72 → 0.0.74

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.
@@ -32,6 +32,38 @@ const api = {
32
32
  delete(path) { return this.request(path, { method: 'DELETE' }); },
33
33
  };
34
34
 
35
+ /** POST /data-exchange/export/:model — validates JSON errors vs .xlsx blob */
36
+ async function downloadDataExchangeXlsx(modelName, payload) {
37
+ const adminPath = window.__ADMIN_PATH__ || '/_admin';
38
+ const res = await fetch(adminPath + '/api/data-exchange/export/' + modelName, {
39
+ method: 'POST',
40
+ credentials: 'include',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify(payload),
43
+ });
44
+ const ct = (res.headers.get('content-type') || '').toLowerCase();
45
+ if (!res.ok || ct.indexOf('spreadsheet') === -1) {
46
+ var msg = 'Export failed';
47
+ try {
48
+ if (ct.indexOf('json') !== -1) {
49
+ var j = await res.json();
50
+ msg = j.error || msg;
51
+ } else {
52
+ var t = await res.text();
53
+ if (t) msg = t.slice(0, 300);
54
+ }
55
+ } catch (e) {}
56
+ throw new Error(msg);
57
+ }
58
+ const blob = await res.blob();
59
+ var url = URL.createObjectURL(blob);
60
+ var a = document.createElement('a');
61
+ a.href = url;
62
+ a.download = modelName + '-export.xlsx';
63
+ a.click();
64
+ URL.revokeObjectURL(url);
65
+ }
66
+
35
67
  // Helper: Capitalize first letter of each word
36
68
  function capitalizeWords(str) {
37
69
  if (!str) return '';
@@ -659,7 +691,7 @@ const FieldRenderers = {
659
691
 
660
692
  return m('.mb-4', [
661
693
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
662
- m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
694
+ m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
663
695
  id: col.name,
664
696
  name: col.name,
665
697
  type: inputType,
@@ -671,7 +703,7 @@ const FieldRenderers = {
671
703
  required: !col.nullable && !readonly,
672
704
  readonly: readonly,
673
705
  disabled: readonly,
674
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
706
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
675
707
  oninput: (e) => onChange(e.target.value),
676
708
  }),
677
709
  hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
@@ -689,7 +721,7 @@ const FieldRenderers = {
689
721
 
690
722
  return m('.mb-4', [
691
723
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
692
- m('textarea.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
724
+ m('textarea.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
693
725
  id: col.name,
694
726
  name: col.name,
695
727
  rows: rows,
@@ -699,7 +731,7 @@ const FieldRenderers = {
699
731
  required: !col.nullable && !readonly,
700
732
  readonly: readonly,
701
733
  disabled: readonly,
702
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
734
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
703
735
  oninput: (e) => onChange(e.target.value),
704
736
  }, value || ''),
705
737
  hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
@@ -716,7 +748,7 @@ const FieldRenderers = {
716
748
 
717
749
  return m('.mb-4', [
718
750
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
719
- m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
751
+ m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
720
752
  id: col.name,
721
753
  name: col.name,
722
754
  type: 'number',
@@ -728,7 +760,7 @@ const FieldRenderers = {
728
760
  required: !col.nullable && !readonly,
729
761
  readonly: readonly,
730
762
  disabled: readonly,
731
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
763
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
732
764
  oninput: (e) => onChange(e.target.value === '' ? null : parseInt(e.target.value, 10)),
733
765
  }),
734
766
  hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
@@ -745,7 +777,7 @@ const FieldRenderers = {
745
777
 
746
778
  return m('.mb-4', [
747
779
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
748
- m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
780
+ m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
749
781
  id: col.name,
750
782
  name: col.name,
751
783
  type: 'number',
@@ -757,7 +789,7 @@ const FieldRenderers = {
757
789
  required: !col.nullable && !readonly,
758
790
  readonly: readonly,
759
791
  disabled: readonly,
760
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
792
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
761
793
  oninput: (e) => onChange(e.target.value === '' ? null : parseFloat(e.target.value)),
762
794
  }),
763
795
  hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
@@ -772,14 +804,14 @@ const FieldRenderers = {
772
804
 
773
805
  return m('.mb-4', [
774
806
  m('label.flex.items-center.cursor-pointer', { class: readonly ? 'cursor-not-allowed' : '' }, [
775
- m('input.mr-2.w-4.h-4', {
807
+ m('input.mr-2.w-4.h-4.rounded.border-gray-300.dark:border-slate-600.text-indigo-600.focus:ring-indigo-500', {
776
808
  type: 'checkbox',
777
809
  name: col.name,
778
810
  checked: Boolean(value),
779
811
  disabled: readonly,
780
812
  onchange: (e) => onChange(e.target.checked),
781
813
  }),
782
- m('span.text-sm.font-medium.text-gray-700', label),
814
+ m('span.text-sm.font-medium.text-gray-700.dark:text-slate-300', label),
783
815
  ]),
784
816
  hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
785
817
  ]);
@@ -796,7 +828,7 @@ const FieldRenderers = {
796
828
 
797
829
  return m('.mb-4', [
798
830
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
799
- m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
831
+ m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
800
832
  id: col.name,
801
833
  name: col.name,
802
834
  type: 'date',
@@ -807,7 +839,7 @@ const FieldRenderers = {
807
839
  required: !col.nullable && !readonly,
808
840
  readonly: readonly,
809
841
  disabled: readonly,
810
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
842
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
811
843
  oninput: (e) => onChange(e.target.value),
812
844
  }),
813
845
  hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
@@ -825,7 +857,7 @@ const FieldRenderers = {
825
857
 
826
858
  return m('.mb-4', [
827
859
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
828
- m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
860
+ m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
829
861
  id: col.name,
830
862
  name: col.name,
831
863
  type: 'datetime-local',
@@ -836,7 +868,7 @@ const FieldRenderers = {
836
868
  required: !col.nullable && !readonly,
837
869
  readonly: readonly,
838
870
  disabled: readonly,
839
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
871
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
840
872
  oninput: (e) => onChange(e.target.value),
841
873
  }),
842
874
  hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
@@ -852,13 +884,13 @@ const FieldRenderers = {
852
884
 
853
885
  return m('.mb-4', [
854
886
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
855
- m('select.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
887
+ m('select.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
856
888
  id: col.name,
857
889
  name: col.name,
858
890
  value: value || '',
859
891
  required: !col.nullable && !readonly,
860
892
  disabled: readonly,
861
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
893
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
862
894
  onchange: (e) => onChange(e.target.value),
863
895
  }, [
864
896
  col.nullable ? m('option', { value: '' }, '-- Select --') : null,
@@ -879,7 +911,7 @@ const FieldRenderers = {
879
911
 
880
912
  return m('.mb-4', [
881
913
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
882
- m('textarea.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.font-mono.text-sm.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
914
+ m('textarea.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.font-mono.text-sm.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
883
915
  id: col.name,
884
916
  name: col.name,
885
917
  rows: rows,
@@ -887,7 +919,7 @@ const FieldRenderers = {
887
919
  required: !col.nullable && !readonly,
888
920
  readonly: readonly,
889
921
  disabled: readonly,
890
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
922
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
891
923
  oninput: (e) => {
892
924
  try {
893
925
  const parsed = JSON.parse(e.target.value);
@@ -912,7 +944,7 @@ const FieldRenderers = {
912
944
 
913
945
  return m('.mb-4', [
914
946
  m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
915
- m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.focus:outline-none.focus:ring-2.focus:ring-blue-500', {
947
+ m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
916
948
  id: col.name,
917
949
  name: col.name,
918
950
  type: 'text',
@@ -923,7 +955,7 @@ const FieldRenderers = {
923
955
  required: !col.nullable && !readonly,
924
956
  readonly: readonly,
925
957
  disabled: readonly,
926
- class: readonly ? 'bg-gray-100 cursor-not-allowed' : '',
958
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
927
959
  oninput: (e) => {
928
960
  const arr = e.target.value.split(',').map(s => s.trim()).filter(s => s);
929
961
  onChange(arr);
@@ -1035,9 +1067,9 @@ const RichTextField = {
1035
1067
  label,
1036
1068
  required ? m('span.text-red-500', ' *') : null
1037
1069
  ),
1038
- m('div.border.border-gray-300 dark:border-slate-600.rounded', {
1070
+ m('div.border.border-gray-300.dark:border-slate-600.rounded.bg-white.dark:bg-slate-900/50', {
1039
1071
  id: editorId,
1040
- class: readonly ? 'bg-gray-100 opacity-50' : '',
1072
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 opacity-50' : '',
1041
1073
  style: 'min-height: 200px;'
1042
1074
  }),
1043
1075
  m('input[type=hidden]', {
@@ -1115,7 +1147,7 @@ const FileUploadField = {
1115
1147
  if (readonly) {
1116
1148
  return m('.mb-4', [
1117
1149
  m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label, required ? m('span.text-red-500', ' *') : null),
1118
- value ? m('a.text-indigo-600.break-all', { href: value, target: '_blank', rel: 'noopener noreferrer' }, value) : m('span.text-gray-400', '—'),
1150
+ value ? m('a.text-indigo-600.dark:text-indigo-400.break-all', { href: value, target: '_blank', rel: 'noopener noreferrer' }, value) : m('span.text-gray-400.dark:text-slate-500', '—'),
1119
1151
  hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
1120
1152
  ]);
1121
1153
  }
@@ -1123,7 +1155,7 @@ const FileUploadField = {
1123
1155
  return m('.mb-4', [
1124
1156
  m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, label, required ? m('span.text-red-500', ' *') : null),
1125
1157
  m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured. Enter a public URL or path manually.'),
1126
- m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded.bg-white.dark:bg-slate-800', {
1158
+ m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500', {
1127
1159
  type: 'text',
1128
1160
  id: col.name,
1129
1161
  name: col.name,
@@ -1137,7 +1169,7 @@ const FileUploadField = {
1137
1169
  }
1138
1170
  return m('.mb-4', [
1139
1171
  m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label, required ? m('span.text-red-500', ' *') : null),
1140
- m('div#' + dropZoneId + '.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-8.text-center', { style: 'cursor: pointer;' }, [
1172
+ m('div#' + dropZoneId + '.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded-lg.p-8.text-center.bg-gray-50.dark:bg-slate-900/40', { style: 'cursor: pointer;' }, [
1141
1173
  m('input[type=file].hidden', {
1142
1174
  id: 'file-input-' + col.name,
1143
1175
  accept: accept,
@@ -1153,7 +1185,7 @@ const FileUploadField = {
1153
1185
  ]),
1154
1186
  value ? m('.mt-4.text-left', [
1155
1187
  m('p.text-sm.text-gray-600.dark:text-slate-400.break-all', 'Current: ' + value),
1156
- m('button.text-red-600.hover:text-red-800.text-sm.mt-2', {
1188
+ m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm.mt-2', {
1157
1189
  type: 'button',
1158
1190
  onclick: function () { if (onChange) onChange(''); },
1159
1191
  }, 'Remove'),
@@ -1285,8 +1317,8 @@ const LoginForm = {
1285
1317
  m('.w-full.max-w-md', [
1286
1318
  m('.bg-white dark:bg-slate-800.rounded-2xl.shadow-2xl.p-6.sm:p-8', [
1287
1319
  m('div.text-center.mb-6', [
1288
- m('h1.text-2xl.sm:text-3xl.font-bold.text-gray-900', 'Admin Login'),
1289
- m('p.text-gray-500 dark:text-slate-400.text-sm.mt-1', 'Sign in to your account'),
1320
+ m('h1.text-2xl.sm:text-3xl.font-bold.text-gray-900.dark:text-slate-50', 'Admin Login'),
1321
+ m('p.text-gray-500.dark:text-slate-400.text-sm.mt-1', 'Sign in to your account'),
1290
1322
  ]),
1291
1323
  m('form', {
1292
1324
  onsubmit: async (e) => {
@@ -1308,10 +1340,10 @@ const LoginForm = {
1308
1340
  }
1309
1341
  }
1310
1342
  }, [
1311
- state.error ? m('.bg-red-50.border.border-red-200.text-red-700.px-4.py-3.rounded-lg.mb-4.text-sm', state.error) : null,
1343
+ state.error ? m('.bg-red-50.border.border-red-200.text-red-700.dark:bg-red-950/50.dark:border-red-800/80.dark:text-red-200.px-4.py-3.rounded-lg.mb-4.text-sm', state.error) : null,
1312
1344
  m('.mb-4', [
1313
- m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-2', { for: 'email' }, 'Email'),
1314
- m('input#email.w-full.px-3.py-2.5.border.border-gray-300 dark:border-slate-600.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors', {
1345
+ m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-2', { for: 'email' }, 'Email'),
1346
+ m('input#email.w-full.px-3.py-2.5.bg-white.text-gray-900.border.border-gray-300.rounded-lg.placeholder-gray-400.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors.dark:bg-slate-900/80.dark:border-slate-500.dark:text-slate-100.dark:placeholder-slate-500', {
1315
1347
  type: 'email',
1316
1348
  name: 'email',
1317
1349
  required: true,
@@ -1319,14 +1351,14 @@ const LoginForm = {
1319
1351
  }),
1320
1352
  ]),
1321
1353
  m('.mb-6', [
1322
- m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-2', { for: 'password' }, 'Password'),
1323
- m('input#password.w-full.px-3.py-2.5.border.border-gray-300 dark:border-slate-600.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors', {
1354
+ m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-2', { for: 'password' }, 'Password'),
1355
+ m('input#password.w-full.px-3.py-2.5.bg-white.text-gray-900.border.border-gray-300.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors.dark:bg-slate-900/80.dark:border-slate-500.dark:text-slate-100', {
1324
1356
  type: 'password',
1325
1357
  name: 'password',
1326
1358
  required: true,
1327
1359
  }),
1328
1360
  ]),
1329
- m('button.w-full.bg-blue-600.text-white.py-2.5.px-4.rounded-lg.font-medium.hover:bg-blue-700.focus:ring-2.focus:ring-blue-500.focus:ring-offset-2.disabled:opacity-50.transition-colors', {
1361
+ m('button.w-full.bg-blue-600.text-white.py-2.5.px-4.rounded-lg.font-medium.hover:bg-blue-700.focus:ring-2.focus:ring-blue-500.focus:ring-offset-2.dark:focus:ring-offset-slate-800.disabled:opacity-50.transition-colors', {
1330
1362
  type: 'submit',
1331
1363
  disabled: state.loading,
1332
1364
  }, state.loading ? 'Logging in...' : 'Sign in'),
@@ -1342,8 +1374,8 @@ const SetupForm = {
1342
1374
  m('.w-full.max-w-md', [
1343
1375
  m('.bg-white dark:bg-slate-800.rounded-2xl.shadow-2xl.p-6.sm:p-8', [
1344
1376
  m('div.text-center.mb-6', [
1345
- m('h1.text-2xl.sm:text-3xl.font-bold.text-gray-900', 'Setup Admin Account'),
1346
- m('p.text-gray-500 dark:text-slate-400.text-sm.mt-1', 'Create the first admin user account.'),
1377
+ m('h1.text-2xl.sm:text-3xl.font-bold.text-gray-900.dark:text-slate-50', 'Setup Admin Account'),
1378
+ m('p.text-gray-500.dark:text-slate-400.text-sm.mt-1', 'Create the first admin user account.'),
1347
1379
  ]),
1348
1380
  m('form', {
1349
1381
  onsubmit: async (e) => {
@@ -1367,18 +1399,18 @@ const SetupForm = {
1367
1399
  }
1368
1400
  }
1369
1401
  }, [
1370
- state.error ? m('.bg-red-50.border.border-red-200.text-red-700.px-4.py-3.rounded-lg.mb-4.text-sm', state.error) : null,
1402
+ state.error ? m('.bg-red-50.border.border-red-200.text-red-700.dark:bg-red-950/50.dark:border-red-800/80.dark:text-red-200.px-4.py-3.rounded-lg.mb-4.text-sm', state.error) : null,
1371
1403
  m('.mb-4', [
1372
- m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-2', { for: 'name' }, 'Name'),
1373
- m('input#name.w-full.px-3.py-2.5.border.border-gray-300 dark:border-slate-600.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors', {
1404
+ m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-2', { for: 'name' }, 'Name'),
1405
+ m('input#name.w-full.px-3.py-2.5.bg-white.text-gray-900.border.border-gray-300.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors.dark:bg-slate-900/80.dark:border-slate-500.dark:text-slate-100', {
1374
1406
  type: 'text',
1375
1407
  name: 'name',
1376
1408
  required: true,
1377
1409
  }),
1378
1410
  ]),
1379
1411
  m('.mb-4', [
1380
- m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-2', { for: 'email' }, 'Email'),
1381
- m('input#email.w-full.px-3.py-2.5.border.border-gray-300 dark:border-slate-600.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors', {
1412
+ m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-2', { for: 'email' }, 'Email'),
1413
+ m('input#email.w-full.px-3.py-2.5.bg-white.text-gray-900.border.border-gray-300.rounded-lg.placeholder-gray-400.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors.dark:bg-slate-900/80.dark:border-slate-500.dark:text-slate-100.dark:placeholder-slate-500', {
1382
1414
  type: 'email',
1383
1415
  name: 'email',
1384
1416
  required: true,
@@ -1386,14 +1418,14 @@ const SetupForm = {
1386
1418
  }),
1387
1419
  ]),
1388
1420
  m('.mb-6', [
1389
- m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-2', { for: 'password' }, 'Password'),
1390
- m('input#password.w-full.px-3.py-2.5.border.border-gray-300 dark:border-slate-600.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors', {
1421
+ m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-2', { for: 'password' }, 'Password'),
1422
+ m('input#password.w-full.px-3.py-2.5.bg-white.text-gray-900.border.border-gray-300.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors.dark:bg-slate-900/80.dark:border-slate-500.dark:text-slate-100', {
1391
1423
  type: 'password',
1392
1424
  name: 'password',
1393
1425
  required: true,
1394
1426
  }),
1395
1427
  ]),
1396
- m('button.w-full.bg-blue-600.text-white.py-2.5.px-4.rounded-lg.font-medium.hover:bg-blue-700.focus:ring-2.focus:ring-blue-500.focus:ring-offset-2.disabled:opacity-50.transition-colors', {
1428
+ m('button.w-full.bg-blue-600.text-white.py-2.5.px-4.rounded-lg.font-medium.hover:bg-blue-700.focus:ring-2.focus:ring-blue-500.focus:ring-offset-2.dark:focus:ring-offset-slate-800.disabled:opacity-50.transition-colors', {
1397
1429
  type: 'submit',
1398
1430
  disabled: state.loading,
1399
1431
  }, state.loading ? 'Creating...' : 'Create Admin Account'),
@@ -1985,29 +2017,13 @@ const RecordList = {
1985
2017
  }, 'Trash'),
1986
2018
  ]) : null,
1987
2019
  ]),
1988
- !state.trashedView ? m('.flex.items-center.gap-2', [
2020
+ m('.flex.flex-wrap.items-center.gap-2', [
1989
2021
  m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-medium.text-indigo-700.bg-white.dark:bg-slate-800.border.border-indigo-200.rounded-lg.hover:bg-indigo-50.transition-colors', {
1990
2022
  onclick: async () => {
1991
- const adminPath = window.__ADMIN_PATH__ || '/_admin';
1992
2023
  try {
1993
2024
  const payload = { selectAll: true, filters: state.filters };
1994
- const res = await fetch(adminPath + '/api/data-exchange/export/' + modelName, {
1995
- method: 'POST',
1996
- credentials: 'include',
1997
- headers: { 'Content-Type': 'application/json' },
1998
- body: JSON.stringify(payload),
1999
- });
2000
- if (!res.ok) {
2001
- const err = await res.json().catch(function () { return {}; });
2002
- throw new Error(err.error || 'Export failed');
2003
- }
2004
- const blob = await res.blob();
2005
- const url = URL.createObjectURL(blob);
2006
- const a = document.createElement('a');
2007
- a.href = url;
2008
- a.download = modelName + '-export.xlsx';
2009
- a.click();
2010
- URL.revokeObjectURL(url);
2025
+ if (state.trashedView) payload.trashed = 'only';
2026
+ await downloadDataExchangeXlsx(modelName, payload);
2011
2027
  } catch (err) {
2012
2028
  alert('Error: ' + err.message);
2013
2029
  }
@@ -2018,7 +2034,7 @@ const RecordList = {
2018
2034
  ),
2019
2035
  'Export Excel',
2020
2036
  ]),
2021
- m('input[type=file]', {
2037
+ !state.trashedView ? m('input[type=file]', {
2022
2038
  id: 'data-exchange-import-' + modelName,
2023
2039
  style: 'display:none',
2024
2040
  accept: '.csv,.xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv',
@@ -2043,7 +2059,7 @@ const RecordList = {
2043
2059
  }
2044
2060
  var msg = 'Import finished: created ' + body.created + ', updated ' + (body.updated || 0) + ', failed ' + (body.failed || 0);
2045
2061
  if (body.errors && body.errors.length) {
2046
- msg += '\nFirst errors: ' + body.errors.slice(0, 3).map(function (x) { return 'row ' + x.row + ': ' + x.message; }).join('; ');
2062
+ msg += 'First errors: ' + body.errors.slice(0, 3).map(function (x) { return 'row ' + x.row + ': ' + x.message; }).join('; ');
2047
2063
  }
2048
2064
  alert(msg);
2049
2065
  loadRecords(modelName, state.pagination.page, state.filters);
@@ -2051,8 +2067,8 @@ const RecordList = {
2051
2067
  alert('Error: ' + err.message);
2052
2068
  }
2053
2069
  },
2054
- }),
2055
- m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-medium.text-indigo-700.bg-white.dark:bg-slate-800.border.border-indigo-200.rounded-lg.hover:bg-indigo-50.transition-colors', {
2070
+ }) : null,
2071
+ !state.trashedView ? m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-medium.text-indigo-700.bg-white.dark:bg-slate-800.border.border-indigo-200.rounded-lg.hover:bg-indigo-50.transition-colors', {
2056
2072
  onclick: function () {
2057
2073
  var el = document.getElementById('data-exchange-import-' + modelName);
2058
2074
  if (el) el.click();
@@ -2062,8 +2078,8 @@ const RecordList = {
2062
2078
  m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M7 10l5 5m0 0l5-5m-5 5V4' })
2063
2079
  ),
2064
2080
  'Import',
2065
- ]),
2066
- m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-medium.text-white.bg-indigo-600.rounded-lg.hover:bg-indigo-700.focus:outline-none.focus:ring-2.focus:ring-indigo-500', {
2081
+ ]) : null,
2082
+ !state.trashedView ? m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-medium.text-white.bg-indigo-600.rounded-lg.hover:bg-indigo-700.focus:outline-none.focus:ring-2.focus:ring-indigo-500', {
2067
2083
  onclick: () => {
2068
2084
  state.currentRecord = null;
2069
2085
  state.editing = true;
@@ -2074,8 +2090,8 @@ const RecordList = {
2074
2090
  m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M12 4v16m8-8H4' }),
2075
2091
  ]),
2076
2092
  'New Record',
2077
- ]),
2078
- ]) : null,
2093
+ ]) : null,
2094
+ ]),
2079
2095
  ]),
2080
2096
 
2081
2097
  // Quick Filters Bar
@@ -2281,33 +2297,17 @@ const RecordList = {
2281
2297
  ),
2282
2298
  'Export CSV',
2283
2299
  ]) : null,
2284
- !state.trashedView ? m('button.inline-flex.items-center.gap-1.px-3.py-1.5.text-sm.font-medium.text-violet-600.bg-white dark:bg-slate-800.border.border-violet-200.rounded.hover:bg-violet-50.transition-colors', {
2300
+ m('button.inline-flex.items-center.gap-1.px-3.py-1.5.text-sm.font-medium.text-violet-600.bg-white dark:bg-slate-800.border.border-violet-200.rounded.hover:bg-violet-50.transition-colors', {
2285
2301
  disabled: state.bulkActionInProgress,
2286
2302
  onclick: async () => {
2287
2303
  state.bulkActionInProgress = true;
2288
2304
  m.redraw();
2289
2305
  try {
2290
- const adminPath = window.__ADMIN_PATH__ || '/_admin';
2291
2306
  const payload = state.selectAllMode
2292
2307
  ? { selectAll: true, filters: state.filters }
2293
2308
  : { ids: Array.from(state.selectedRecords) };
2294
- const res = await fetch(adminPath + '/api/data-exchange/export/' + modelName, {
2295
- method: 'POST',
2296
- credentials: 'include',
2297
- headers: { 'Content-Type': 'application/json' },
2298
- body: JSON.stringify(payload),
2299
- });
2300
- if (!res.ok) {
2301
- const err = await res.json().catch(function () { return {}; });
2302
- throw new Error(err.error || 'Export failed');
2303
- }
2304
- const blob = await res.blob();
2305
- const url = URL.createObjectURL(blob);
2306
- const a = document.createElement('a');
2307
- a.href = url;
2308
- a.download = modelName + '-export.xlsx';
2309
- a.click();
2310
- URL.revokeObjectURL(url);
2309
+ if (state.trashedView) payload.trashed = 'only';
2310
+ await downloadDataExchangeXlsx(modelName, payload);
2311
2311
  } catch (err) {
2312
2312
  alert('Error: ' + err.message);
2313
2313
  } finally {
@@ -2320,7 +2320,7 @@ const RecordList = {
2320
2320
  m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4' })
2321
2321
  ),
2322
2322
  'Export Excel',
2323
- ]) : null,
2323
+ ]),
2324
2324
  !state.trashedView ? m(BulkFieldUpdateDropdown, {
2325
2325
  modelName: modelName,
2326
2326
  selectedIds: state.selectAllMode ? null : Array.from(state.selectedRecords),
@@ -2350,7 +2350,7 @@ const RecordList = {
2350
2350
  m('thead.bg-gray-50.dark:bg-slate-900', { style: 'position: sticky; top: 0; z-index: 20;' }, [
2351
2351
  m('tr', [
2352
2352
  // Checkbox column header (sticky left, box-shadow on right)
2353
- m('th.px-4.py-3.text-left.bg-gray-50 dark:bg-slate-900.border-b.border-gray-200', { style: 'width: 40px; position: sticky; left: 0; z-index: 15; box-shadow: 4px 0 8px -4px rgba(0,0,0,0.08);' }, [
2353
+ m('th.px-4.py-3.text-left.bg-gray-50 dark:bg-slate-900.border-b.border-gray-200 dark:border-slate-700', { style: 'width: 40px; position: sticky; left: 0; z-index: 15; box-shadow: 4px 0 8px -4px rgba(0,0,0,0.08);' }, [
2354
2354
  m('input[type=checkbox].rounded.border-gray-300 dark:border-slate-600.text-indigo-600.focus:ring-indigo-500', {
2355
2355
  checked: state.records.length > 0 && state.selectedRecords && state.selectedRecords.size === state.records.length,
2356
2356
  indeterminate: state.selectedRecords && state.selectedRecords.size > 0 && state.selectedRecords.size < state.records.length,
@@ -2366,23 +2366,23 @@ const RecordList = {
2366
2366
  ]),
2367
2367
  // Dynamic column headers (first column sticky left with box-shadow)
2368
2368
  ...displayColumns.map((col, i) =>
2369
- m('th.px-4.py-3.text-left.text-xs.font-medium.text-gray-500 dark:text-slate-400.uppercase.tracking-wider.whitespace-nowrap.bg-gray-50 dark:bg-slate-900.border-b.border-gray-200',
2369
+ m('th.px-4.py-3.text-left.text-xs.font-medium.text-gray-500 dark:text-slate-400.uppercase.tracking-wider.whitespace-nowrap.bg-gray-50 dark:bg-slate-900.border-b.border-gray-200 dark:border-slate-700',
2370
2370
  i === 0 ? { style: 'position: sticky; left: 40px; z-index: 15; box-shadow: 4px 0 8px -4px rgba(0,0,0,0.08);' } : {},
2371
2371
  formatColumnLabel(col.name)
2372
2372
  )
2373
2373
  ),
2374
2374
  // Sticky actions header (sticky right, box-shadow on left)
2375
- m('th.px-4.py-3.text-right.text-xs.font-medium.text-gray-500 dark:text-slate-400.uppercase.tracking-wider.bg-gray-50 dark:bg-slate-900.border-b.border-gray-200', {
2375
+ m('th.px-4.py-3.text-right.text-xs.font-medium.text-gray-500 dark:text-slate-400.uppercase.tracking-wider.bg-gray-50 dark:bg-slate-900.border-b.border-gray-200 dark:border-slate-700', {
2376
2376
  style: 'position: sticky; right: 0; min-width: 120px; z-index: 15; box-shadow: -4px 0 8px -4px rgba(0,0,0,0.08);',
2377
2377
  }, 'Actions'),
2378
2378
  ]),
2379
2379
  ]),
2380
- m('tbody.divide-y.divide-gray-100', state.records.map(record =>
2381
- m('tr.hover:bg-gray-50 dark:hover:bg-slate-800/50 dark:hover:bg-slate-800/50.transition-colors', {
2382
- class: state.selectedRecords && state.selectedRecords.has(record[primaryKey]) ? 'bg-indigo-50' : '',
2380
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700', state.records.map(record =>
2381
+ m('tr.hover:bg-gray-50 dark:hover:bg-slate-800/50.transition-colors', {
2382
+ class: state.selectedRecords && state.selectedRecords.has(record[primaryKey]) ? 'bg-indigo-50 dark:bg-indigo-950/50' : '',
2383
2383
  }, [
2384
2384
  // Checkbox cell (sticky left, box-shadow on right)
2385
- m('td.px-4.py-3.bg-white', {
2385
+ m('td.px-4.py-3.bg-white.dark:bg-slate-800', {
2386
2386
  style: 'position: sticky; left: 0; z-index: 5; box-shadow: 4px 0 8px -4px rgba(0,0,0,0.08);',
2387
2387
  }, [
2388
2388
  m('input[type=checkbox].rounded.border-gray-300 dark:border-slate-600.text-indigo-600.focus:ring-indigo-500', {
@@ -2400,17 +2400,17 @@ const RecordList = {
2400
2400
  ]),
2401
2401
  // Dynamic cell values (first column sticky left with box-shadow)
2402
2402
  ...displayColumns.map((col, i) =>
2403
- m('td.px-4.py-3.text-sm.whitespace-nowrap.text-gray-700 dark:text-slate-300.bg-white',
2403
+ m('td.px-4.py-3.text-sm.whitespace-nowrap.text-gray-700 dark:text-slate-300.bg-white dark:bg-slate-800',
2404
2404
  i === 0 ? { style: 'position: sticky; left: 40px; z-index: 5; box-shadow: 4px 0 8px -4px rgba(0,0,0,0.08);' } : {},
2405
2405
  formatCellValue(record[col.name], col)
2406
2406
  )
2407
2407
  ),
2408
2408
  // Sticky actions cell (sticky right, box-shadow on left)
2409
- m('td.px-4.py-3.text-sm.text-right.whitespace-nowrap.bg-white', {
2409
+ m('td.px-4.py-3.text-sm.text-right.whitespace-nowrap.text-gray-700 dark:text-slate-300.bg-white dark:bg-slate-800', {
2410
2410
  style: 'position: sticky; right: 0; z-index: 5; box-shadow: -4px 0 8px -4px rgba(0,0,0,0.08);',
2411
2411
  }, [
2412
2412
  state.trashedView && modelMeta?.softDelete
2413
- ? m('button.inline-flex.items-center.px-2.py-1.text-sm.text-green-600.hover:text-green-800.hover:bg-green-50.rounded.transition-colors', {
2413
+ ? m('button.inline-flex.items-center.px-2.py-1.text-sm.text-green-600.dark:text-green-400.hover:text-green-800.dark:hover:text-green-300.hover:bg-green-50.dark:hover:bg-green-950/40.rounded.transition-colors', {
2414
2414
  onclick: async () => {
2415
2415
  try {
2416
2416
  await api.post('/models/' + modelName + '/records/' + record[primaryKey] + '/restore');
@@ -2421,14 +2421,14 @@ const RecordList = {
2421
2421
  },
2422
2422
  }, 'Restore')
2423
2423
  : [
2424
- m('button.inline-flex.items-center.px-2.py-1.text-sm.text-indigo-600.hover:text-indigo-800.hover:bg-indigo-50.rounded.mr-1.transition-colors', {
2424
+ m('button.inline-flex.items-center.px-2.py-1.text-sm.text-indigo-600.dark:text-indigo-400.hover:text-indigo-800.dark:hover:text-indigo-300.hover:bg-indigo-50.dark:hover:bg-indigo-950/40.rounded.mr-1.transition-colors', {
2425
2425
  onclick: () => {
2426
2426
  state.currentRecord = record;
2427
2427
  state.editing = true;
2428
2428
  m.route.set('/models/' + modelName + '/edit/' + record[primaryKey]);
2429
2429
  },
2430
2430
  }, 'Edit'),
2431
- m('button.inline-flex.items-center.px-2.py-1.text-sm.text-red-600.hover:text-red-800.hover:bg-red-50.rounded.transition-colors', {
2431
+ m('button.inline-flex.items-center.px-2.py-1.text-sm.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.hover:bg-red-50.dark:hover:bg-red-950/40.rounded.transition-colors', {
2432
2432
  onclick: async () => {
2433
2433
  if (confirm('Are you sure you want to delete this record?')) {
2434
2434
  try {
@@ -2516,14 +2516,14 @@ const RecordForm = {
2516
2516
 
2517
2517
  return m(Layout, { breadcrumbs }, [
2518
2518
  m('.flex.items-center.justify-between.mb-6', [
2519
- m('h2.text-2xl.font-bold', isNew ? 'New Record' : 'Edit Record'),
2520
- modelMeta ? m('span.text-gray-500', modelMeta.label || modelMeta.name) : null,
2519
+ m('h2.text-2xl.font-bold.text-gray-900.dark:text-slate-100', isNew ? 'New Record' : 'Edit Record'),
2520
+ modelMeta ? m('span.text-gray-500.dark:text-slate-400', modelMeta.label || modelMeta.name) : null,
2521
2521
  ]),
2522
2522
 
2523
- state.loading ? m('p.text-gray-600', 'Loading...') :
2524
- state.error && !modelMeta ? m('.bg-red-100.border.border-red-400.text-red-700.px-4.py-3.rounded', state.error) :
2523
+ state.loading ? m('p.text-gray-600.dark:text-slate-400', 'Loading...') :
2524
+ state.error && !modelMeta ? m('.bg-red-50.dark:bg-red-950/40.border.border-red-200.dark:border-red-800.text-red-800.dark:text-red-200.px-4.py-3.rounded-lg', state.error) :
2525
2525
 
2526
- m('form.bg-white dark:bg-slate-800.rounded.shadow.flex.flex-col', {
2526
+ m('form.bg-white.dark:bg-slate-800.rounded-lg.shadow-sm.border.border-gray-200.dark:border-slate-700.flex.flex-col', {
2527
2527
  style: 'min-height: calc(100vh - 280px);',
2528
2528
  onsubmit: async (e) => {
2529
2529
  e.preventDefault();
@@ -2592,7 +2592,7 @@ const RecordForm = {
2592
2592
  }, [
2593
2593
  // Form content (scrollable)
2594
2594
  m('.p-6.flex-1.overflow-y-auto', [
2595
- state.error ? m('.bg-red-100.border.border-red-400.text-red-700.px-4.py-3.rounded.mb-4', state.error) : null,
2595
+ state.error ? m('.bg-red-50.dark:bg-red-950/40.border.border-red-200.dark:border-red-800.text-red-800.dark:text-red-200.px-4.py-3.rounded-lg.mb-4', state.error) : null,
2596
2596
 
2597
2597
  // Render form fields based on model columns
2598
2598
  modelMeta && modelMeta.columns ? modelMeta.columns.map(col => {
@@ -2616,8 +2616,8 @@ const RecordForm = {
2616
2616
  ]),
2617
2617
 
2618
2618
  // Sticky footer buttons
2619
- m('.flex.gap-4.p-4.border-t.bg-gray-50 dark:bg-slate-900.sticky.bottom-0', [
2620
- m('button.bg-blue-600.text-white.px-6.py-2.rounded.hover:bg-blue-700.disabled:opacity-50', {
2619
+ m('.flex.gap-4.p-4.border-t.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.sticky.bottom-0', [
2620
+ m('button.bg-blue-600.dark:bg-blue-500.text-white.px-6.py-2.rounded-lg.hover:bg-blue-700.dark:hover:bg-blue-600.disabled:opacity-50', {
2621
2621
  type: 'submit',
2622
2622
  disabled: state.loading,
2623
2623
  }, state.loading ? 'Saving...' : 'Save'),