4bnode 4.0.3 → 4.0.5

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.
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect, useRef } from "react";
2
2
  import { LuCheck, LuCopy, LuPlug, LuShare2 } from "react-icons/lu";
3
- import { html, api, Icons } from "../lib.js";
3
+ import { html, api, Icons, copyToClipboard } from "../lib.js";
4
4
  import { useApp } from "../context.js";
5
5
 
6
6
  export function RoutesPage() {
@@ -162,7 +162,7 @@ export function RoutesPage() {
162
162
  }
163
163
 
164
164
  function copyShareMarkdown() {
165
- navigator.clipboard.writeText(generateShareMarkdown());
165
+ copyToClipboard(generateShareMarkdown());
166
166
  setShareCopied(true);
167
167
  toast('Copied to clipboard', 'success');
168
168
  setTimeout(() => setShareCopied(false), 2000);
@@ -986,11 +986,11 @@ function CreateRoutePanel({ onCreated }) {
986
986
  registerFields:
987
987
  routeType === "register" ? registerFields : undefined,
988
988
  selectedFields:
989
- routeType === "methods" && selectedFields.length > 0
989
+ (routeType === "methods" || routeType === "crud") && selectedFields.length > 0
990
990
  ? selectedFields
991
991
  : undefined,
992
992
  customFields:
993
- routeType === "methods" && customFields.length > 0
993
+ (routeType === "methods" || routeType === "crud") && customFields.length > 0
994
994
  ? customFields
995
995
  : undefined,
996
996
  },
@@ -1132,95 +1132,194 @@ function CreateRoutePanel({ onCreated }) {
1132
1132
  )}
1133
1133
  </div>
1134
1134
  </div>`}
1135
- ${routeType === "methods" &&
1135
+ ${routeType === "crud" &&
1136
1136
  html`<div className="form-group">
1137
1137
  <label className="form-label"
1138
- >Fields from Model
1138
+ >CRUD Methods
1139
1139
  <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
1140
- >(optional — select fields to destructure from
1141
- request)</span
1140
+ >(select which operations to include)</span
1142
1141
  ></label
1143
1142
  >
1144
- ${data.models?.length > 0
1145
- ? html`<select
1146
- className="form-select"
1147
- style=${{ marginBottom: "8px" }}
1148
- value=${modelName}
1149
- onChange=${(e) => {
1150
- setModelName(e.target.value);
1151
- setSelectedFields([]);
1143
+ <div className="flex gap-2">
1144
+ ${["get", "post", "put", "patch", "delete"].map((m) => {
1145
+ const labels = {
1146
+ get: "READ",
1147
+ post: "CREATE",
1148
+ put: "UPDATE",
1149
+ patch: "PATCH",
1150
+ delete: "DELETE",
1151
+ };
1152
+ return html`<button
1153
+ key=${m}
1154
+ className=${`route-method ${m}`}
1155
+ style=${{
1156
+ cursor: "pointer",
1157
+ border: methods.includes(m)
1158
+ ? "2px solid " + methodColors[m]
1159
+ : "2px solid transparent",
1160
+ padding: "6px 12px",
1161
+ fontSize: "11px",
1162
+ opacity: methods.includes(m) ? 1 : 0.35,
1163
+ borderRadius: "6px",
1164
+ transition: "all 0.15s",
1165
+ }}
1166
+ onClick=${() => toggleMethod(m)}
1167
+ >
1168
+ ${labels[m]}
1169
+ </button>`;
1170
+ })}
1171
+ </div>
1172
+ </div>`}
1173
+ ${isAuthType &&
1174
+ html`<div className="form-group">
1175
+ <label className="form-label">Model</label>
1176
+ <select
1177
+ className="form-select"
1178
+ value=${modelName}
1179
+ onChange=${(e) => {
1180
+ setModelName(e.target.value);
1181
+ setLoginField("");
1182
+ setPasswordField("");
1183
+ setRegisterFields([]);
1184
+ setSelectedFields([]);
1185
+ }}
1186
+ >
1187
+ <option value="">Select a model...</option>
1188
+ ${(data.models || []).map(
1189
+ (m) =>
1190
+ html`<option key=${m.name} value=${m.name}>
1191
+ ${m.name}
1192
+ </option>`,
1193
+ )}
1194
+ </select>
1195
+ ${!data.models?.length &&
1196
+ html`<div
1197
+ style=${{
1198
+ fontSize: "11px",
1199
+ color: "var(--warning)",
1200
+ marginTop: "4px",
1201
+ }}
1202
+ >
1203
+ No models found. Create a schema first.
1204
+ </div>`}
1205
+ </div>`}
1206
+ ${(routeType === "methods" || routeType === "crud") &&
1207
+ html`<div className="form-group">
1208
+ <label className="form-label"
1209
+ >Model Fields
1210
+ <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
1211
+ >(select fields to include in request)</span
1212
+ ></label
1213
+ >
1214
+ <select
1215
+ className="form-select"
1216
+ style=${{ marginBottom: "8px" }}
1217
+ value=${modelName}
1218
+ onChange=${(e) => {
1219
+ setModelName(e.target.value);
1220
+ setSelectedFields([]);
1221
+ }}
1222
+ >
1223
+ <option value="">Select a model...</option>
1224
+ ${(data.models || []).map(
1225
+ (m) =>
1226
+ html`<option key=${m.name} value=${m.name}>
1227
+ ${m.name}
1228
+ </option>`,
1229
+ )}
1230
+ </select>
1231
+ ${modelName &&
1232
+ modelFields.length > 0 &&
1233
+ html`<div>
1234
+ <div className="flex gap-2" style=${{ flexWrap: "wrap" }}>
1235
+ ${modelFields.map((f) => {
1236
+ const active = selectedFields.includes(f.name);
1237
+ return html`<button
1238
+ key=${f.name}
1239
+ style=${{
1240
+ padding: "5px 12px",
1241
+ borderRadius: "6px",
1242
+ fontSize: "12px",
1243
+ cursor: "pointer",
1244
+ border: active
1245
+ ? "1.5px solid var(--accent)"
1246
+ : "1.5px solid var(--border)",
1247
+ background: active
1248
+ ? "var(--accent-glow)"
1249
+ : "var(--bg-input)",
1250
+ color: active
1251
+ ? "var(--accent-light)"
1252
+ : "var(--text-secondary)",
1253
+ fontWeight: active ? 600 : 400,
1254
+ transition: "all 0.15s",
1152
1255
  }}
1256
+ onClick=${() =>
1257
+ setSelectedFields((prev) =>
1258
+ active
1259
+ ? prev.filter((x) => x !== f.name)
1260
+ : [...prev, f.name],
1261
+ )}
1153
1262
  >
1154
- <option value="">Select a model...</option>
1155
- ${data.models.map(
1156
- (m) =>
1157
- html`<option key=${m.name} value=${m.name}>
1158
- ${m.name}
1159
- </option>`,
1160
- )}
1161
- </select>
1162
- ${modelName &&
1163
- modelFields.length > 0 &&
1164
- html`<div
1165
- className="flex gap-2"
1166
- style=${{ flexWrap: "wrap" }}
1167
- >
1168
- ${modelFields.map((f) => {
1169
- const active = selectedFields.includes(f.name);
1170
- return html`<button
1171
- key=${f.name}
1172
- style=${{
1173
- padding: "5px 12px",
1174
- borderRadius: "6px",
1175
- fontSize: "12px",
1176
- cursor: "pointer",
1177
- border: active
1178
- ? "1.5px solid var(--accent)"
1179
- : "1.5px solid var(--border)",
1180
- background: active
1181
- ? "var(--accent-glow)"
1182
- : "var(--bg-input)",
1183
- color: active
1184
- ? "var(--accent-light)"
1185
- : "var(--text-secondary)",
1186
- fontWeight: active ? 600 : 400,
1187
- transition: "all 0.15s",
1188
- }}
1189
- onClick=${() =>
1190
- setSelectedFields((prev) =>
1191
- active
1192
- ? prev.filter((x) => x !== f.name)
1193
- : [...prev, f.name],
1194
- )}
1195
- >
1196
- ${f.name}
1197
- <span
1198
- style=${{
1199
- fontSize: "10px",
1200
- color: "var(--text-muted)",
1201
- marginLeft: "4px",
1202
- }}
1203
- >${f.type}</span
1204
- >
1205
- </button>`;
1206
- })}
1207
- </div>`}`
1208
- : html`<div
1263
+ ${f.name}
1264
+ <span
1265
+ style=${{
1266
+ fontSize: "10px",
1267
+ color: "var(--text-muted)",
1268
+ marginLeft: "4px",
1269
+ }}
1270
+ >${f.type}</span
1271
+ >
1272
+ </button>`;
1273
+ })}
1274
+ </div>
1275
+ <div style=${{ display: "flex", gap: "8px", marginTop: "8px" }}>
1276
+ <button
1209
1277
  style=${{
1210
- fontSize: "11px",
1278
+ padding: "3px 10px",
1279
+ fontSize: "10px",
1280
+ borderRadius: "4px",
1281
+ border: "1px solid var(--border)",
1282
+ background: "transparent",
1283
+ color: "var(--accent)",
1284
+ cursor: "pointer",
1285
+ }}
1286
+ onClick=${() => setSelectedFields(modelFields.map((f) => f.name))}
1287
+ >
1288
+ Select All
1289
+ </button>
1290
+ <button
1291
+ style=${{
1292
+ padding: "3px 10px",
1293
+ fontSize: "10px",
1294
+ borderRadius: "4px",
1295
+ border: "1px solid var(--border)",
1296
+ background: "transparent",
1211
1297
  color: "var(--text-muted)",
1212
- marginTop: "4px",
1298
+ cursor: "pointer",
1213
1299
  }}
1300
+ onClick=${() => setSelectedFields([])}
1214
1301
  >
1215
- No models found. You can still add custom fields below.
1216
- </div>`}
1302
+ Clear
1303
+ </button>
1304
+ </div>
1305
+ </div>`}
1306
+ ${!data.models?.length &&
1307
+ html`<div
1308
+ style=${{
1309
+ fontSize: "11px",
1310
+ color: "var(--text-muted)",
1311
+ marginTop: "4px",
1312
+ }}
1313
+ >
1314
+ No models found. You can still add custom fields below.
1315
+ </div>`}
1217
1316
  </div>`}
1218
- ${routeType === "methods" &&
1317
+ ${(routeType === "methods" || routeType === "crud") &&
1219
1318
  html`<div className="form-group">
1220
1319
  <label className="form-label"
1221
1320
  >Custom Fields
1222
1321
  <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
1223
- >(add fields received from client)</span
1322
+ >(add extra fields received from client)</span
1224
1323
  ></label
1225
1324
  >
1226
1325
  ${customFields.length > 0 &&
@@ -1338,76 +1437,6 @@ function CreateRoutePanel({ onCreated }) {
1338
1437
  </button>
1339
1438
  </div>
1340
1439
  </div>`}
1341
- ${routeType === "crud" &&
1342
- html`<div className="form-group">
1343
- <label className="form-label"
1344
- >CRUD Methods
1345
- <span style=${{ color: "var(--text-muted)", fontWeight: 400 }}
1346
- >(select which operations to include)</span
1347
- ></label
1348
- >
1349
- <div className="flex gap-2">
1350
- ${["get", "post", "put", "patch", "delete"].map((m) => {
1351
- const labels = {
1352
- get: "READ",
1353
- post: "CREATE",
1354
- put: "UPDATE",
1355
- patch: "PATCH",
1356
- delete: "DELETE",
1357
- };
1358
- return html`<button
1359
- key=${m}
1360
- className=${`route-method ${m}`}
1361
- style=${{
1362
- cursor: "pointer",
1363
- border: methods.includes(m)
1364
- ? "2px solid " + methodColors[m]
1365
- : "2px solid transparent",
1366
- padding: "6px 12px",
1367
- fontSize: "11px",
1368
- opacity: methods.includes(m) ? 1 : 0.35,
1369
- borderRadius: "6px",
1370
- transition: "all 0.15s",
1371
- }}
1372
- onClick=${() => toggleMethod(m)}
1373
- >
1374
- ${labels[m]}
1375
- </button>`;
1376
- })}
1377
- </div>
1378
- </div>`}
1379
- ${needsModel &&
1380
- html`<div className="form-group">
1381
- <label className="form-label">Model</label>
1382
- <select
1383
- className="form-select"
1384
- value=${modelName}
1385
- onChange=${(e) => {
1386
- setModelName(e.target.value);
1387
- setLoginField("");
1388
- setPasswordField("");
1389
- setRegisterFields([]);
1390
- }}
1391
- >
1392
- <option value="">Select a model...</option>
1393
- ${(data.models || []).map(
1394
- (m) =>
1395
- html`<option key=${m.name} value=${m.name}>
1396
- ${m.name}
1397
- </option>`,
1398
- )}
1399
- </select>
1400
- ${!data.models?.length &&
1401
- html`<div
1402
- style=${{
1403
- fontSize: "11px",
1404
- color: "var(--warning)",
1405
- marginTop: "4px",
1406
- }}
1407
- >
1408
- No models found. Create a schema first.
1409
- </div>`}
1410
- </div>`}
1411
1440
  ${routeType === "login" &&
1412
1441
  modelName &&
1413
1442
  modelFields.length > 0 &&
@@ -341,7 +341,7 @@ function SchemaDetailPanel({ model, reqCount, uniqCount, onEdit, onDelete }) {
341
341
  </div>
342
342
  </div>
343
343
  <div className="flex gap-3">
344
- ${[['Fields', model.fields?.length || 0, null], ['Required', reqCount, reqCount > 0 ? 'var(--success)' : null], ['Unique', uniqCount, uniqCount > 0 ? 'var(--warning)' : null]].map(([label, count, color]) =>
344
+ ${[['Fields', model.fields?.length || 0, null], ['Required', reqCount, reqCount > 0 ? 'var(--success)' : null], ['Unique', uniqCount, uniqCount > 0 ? 'var(--warning)' : null], ['Indexes', (model.fields?.filter(f => f.index).length || 0) + (model.indexes?.length || 0), ((model.fields?.filter(f => f.index).length || 0) + (model.indexes?.length || 0)) > 0 ? 'var(--info)' : null]].map(([label, count, color]) =>
345
345
  html`<div key=${label} style=${{ padding: '8px 14px', background: 'var(--bg-input)', borderRadius: 'var(--radius)', fontSize: '13px' }}>
346
346
  <span style=${{ color: 'var(--text-muted)' }}>${label} </span>
347
347
  <span style=${{ fontWeight: 600, color: color || 'var(--text-secondary)' }}>${count}</span>
@@ -379,7 +379,8 @@ function SchemaDetailPanel({ model, reqCount, uniqCount, onEdit, onDelete }) {
379
379
  <td style=${tdStyle}><div className="flex gap-1">
380
380
  ${f.required && html`<span className="badge badge-success" style=${{ fontSize: '10px' }}>required</span>`}
381
381
  ${f.unique && html`<span className="badge badge-warning" style=${{ fontSize: '10px' }}>unique</span>`}
382
- ${!f.required && !f.unique && html`<span style=${{ fontSize: '12px', color: 'var(--text-muted)' }}>—</span>`}
382
+ ${f.index && html`<span className="badge badge-info" style=${{ fontSize: '10px' }}>indexed</span>`}
383
+ ${!f.required && !f.unique && !f.index && html`<span style=${{ fontSize: '12px', color: 'var(--text-muted)' }}>—</span>`}
383
384
  </div></td>
384
385
  </tr>`)}
385
386
  </tbody>
@@ -485,6 +486,11 @@ function SchemaBuilderPanel({ model, onDone }) {
485
486
  const [freq, setFreq] = useState(false);
486
487
  const [funiq, setFuniq] = useState(false);
487
488
  const [fref, setFref] = useState("");
489
+ const [findex, setFindex] = useState(false);
490
+ const [indexes, setIndexes] = useState(model?.indexes || []);
491
+ const [idxFields, setIdxFields] = useState([]);
492
+ const [idxType, setIdxType] = useState('compound');
493
+ const [idxUnique, setIdxUnique] = useState(false);
488
494
  const nameRef = useRef(null);
489
495
  const fieldNameRef = useRef(null);
490
496
 
@@ -516,6 +522,7 @@ function SchemaBuilderPanel({ model, onDone }) {
516
522
  setFreq(false);
517
523
  setFuniq(false);
518
524
  setFref("");
525
+ setFindex(false);
519
526
  setEditIndex(null);
520
527
  }
521
528
 
@@ -528,6 +535,7 @@ function SchemaBuilderPanel({ model, onDone }) {
528
535
  unique: funiq,
529
536
  };
530
537
  if (ftype === "ObjectId" && fref) fieldData.ref = fref;
538
+ if (findex) fieldData.index = true;
531
539
 
532
540
  if (editIndex !== null) {
533
541
  setFields((prev) =>
@@ -549,6 +557,7 @@ function SchemaBuilderPanel({ model, onDone }) {
549
557
  setFreq(!!f.required);
550
558
  setFuniq(!!f.unique);
551
559
  setFref(f.ref || "");
560
+ setFindex(!!f.index);
552
561
  setEditIndex(index);
553
562
  setTimeout(() => fieldNameRef.current?.focus(), 50);
554
563
  }
@@ -573,13 +582,13 @@ function SchemaBuilderPanel({ model, onDone }) {
573
582
  if (isEdit) {
574
583
  await api("/models/" + model.name, {
575
584
  method: "PUT",
576
- body: { fields },
585
+ body: { fields, indexes },
577
586
  });
578
587
  toast('Model "' + model.name + '" updated', "success");
579
588
  } else {
580
589
  await api("/models", {
581
590
  method: "POST",
582
- body: { name: name.trim(), fields },
591
+ body: { name: name.trim(), fields, indexes },
583
592
  });
584
593
  toast('Model "' + name.trim() + '" created', "success");
585
594
  }
@@ -732,6 +741,25 @@ function SchemaBuilderPanel({ model, onDone }) {
732
741
  </div>
733
742
  Unique
734
743
  </label>
744
+ <label
745
+ className="flex items-center gap-2"
746
+ style=${{
747
+ cursor: "pointer",
748
+ fontSize: "12px",
749
+ whiteSpace: "nowrap",
750
+ color: findex ? "var(--info)" : "var(--text-muted)",
751
+ }}
752
+ >
753
+ <div className="form-toggle">
754
+ <input
755
+ type="checkbox"
756
+ checked=${findex}
757
+ onChange=${(e) => setFindex(e.target.checked)}
758
+ />
759
+ <span className="slider" />
760
+ </div>
761
+ Index
762
+ </label>
735
763
  </div>
736
764
  ${editIndex !== null &&
737
765
  html`<button
@@ -858,6 +886,18 @@ function SchemaBuilderPanel({ model, onDone }) {
858
886
  }}
859
887
  >UNQ</span
860
888
  >`}
889
+ ${f.index &&
890
+ html`<span
891
+ style=${{
892
+ fontSize: "9px",
893
+ padding: "2px 6px",
894
+ borderRadius: "4px",
895
+ background: "rgba(59,130,246,0.15)",
896
+ color: "var(--info)",
897
+ fontWeight: 600,
898
+ }}
899
+ >IDX</span
900
+ >`}
861
901
  <button
862
902
  className="btn btn-ghost btn-sm btn-icon"
863
903
  onClick=${() => startEdit(i)}
@@ -897,5 +937,92 @@ function SchemaBuilderPanel({ model, onDone }) {
897
937
  Add fields above to define your schema
898
938
  </div>
899
939
  </div>`}
940
+
941
+ <div className="card mb-4" style=${{ padding: '16px' }}>
942
+ <div style=${{ fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)', marginBottom: '12px' }}>
943
+ Compound Indexes
944
+ </div>
945
+
946
+ <div className="flex items-center gap-3 mb-3" style=${{ flexWrap: 'wrap' }}>
947
+ <select className="form-select" value=${idxType}
948
+ onChange=${(e) => setIdxType(e.target.value)}
949
+ style=${{ fontSize: '13px', width: '120px' }}>
950
+ <option value="compound">Compound</option>
951
+ <option value="text">Text</option>
952
+ </select>
953
+
954
+ ${idxType === 'compound' && html`<label className="flex items-center gap-2" style=${{ cursor: 'pointer', fontSize: '12px', color: idxUnique ? 'var(--warning)' : 'var(--text-muted)' }}>
955
+ <div className="form-toggle">
956
+ <input type="checkbox" checked=${idxUnique} onChange=${(e) => setIdxUnique(e.target.checked)} />
957
+ <span className="slider" />
958
+ </div>
959
+ Unique
960
+ </label>`}
961
+
962
+ <button className="btn btn-primary btn-sm"
963
+ disabled=${idxFields.length < (idxType === 'text' ? 1 : 2)}
964
+ onClick=${() => {
965
+ const newIdx = {
966
+ fields: idxFields.map(f => ({ field: f.field, direction: idxType === 'text' ? 'text' : f.direction })),
967
+ unique: idxType === 'compound' ? idxUnique : false,
968
+ type: idxType,
969
+ };
970
+ setIndexes(prev => [...prev, newIdx]);
971
+ setIdxFields([]);
972
+ setIdxUnique(false);
973
+ setSaved(false);
974
+ }}>
975
+ Add Index
976
+ </button>
977
+ </div>
978
+
979
+ <div className="flex gap-2 mb-3" style=${{ flexWrap: 'wrap' }}>
980
+ ${fields.map(f => {
981
+ const selected = idxFields.find(i => i.field === f.name);
982
+ return html`<button key=${f.name}
983
+ onClick=${() => {
984
+ if (selected) {
985
+ if (idxType !== 'text') {
986
+ if (selected.direction === 1) {
987
+ setIdxFields(prev => prev.map(i => i.field === f.name ? { ...i, direction: -1 } : i));
988
+ } else {
989
+ setIdxFields(prev => prev.filter(i => i.field !== f.name));
990
+ }
991
+ } else {
992
+ setIdxFields(prev => prev.filter(i => i.field !== f.name));
993
+ }
994
+ } else {
995
+ setIdxFields(prev => [...prev, { field: f.name, direction: 1 }]);
996
+ }
997
+ }}
998
+ style=${{ padding: '4px 10px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--border)', cursor: 'pointer', fontFamily: 'monospace',
999
+ background: selected ? 'var(--accent-glow)' : 'transparent',
1000
+ color: selected ? 'var(--accent-light)' : 'var(--text-muted)' }}>
1001
+ ${f.name}${selected && idxType !== 'text' ? (selected.direction === 1 ? ' ASC' : ' DESC') : ''}
1002
+ </button>`;
1003
+ })}
1004
+ </div>
1005
+ ${idxFields.length > 0 && html`<div style=${{ fontSize: '11px', color: 'var(--text-muted)', marginBottom: '8px' }}>
1006
+ ${idxType === 'text' ? 'Click to select/deselect fields' : 'Click once = ASC, twice = DESC, third = remove'}
1007
+ </div>`}
1008
+
1009
+ ${indexes.length > 0 && html`<div style=${{ marginTop: '12px' }}>
1010
+ ${indexes.map((idx, i) => html`<div key=${i} className="flex items-center gap-2" style=${{ padding: '6px 12px', background: 'var(--bg-input)', borderRadius: '6px', marginBottom: '4px', fontSize: '12px' }}>
1011
+ <span style=${{ fontSize: '9px', padding: '2px 6px', borderRadius: '4px', fontWeight: 600,
1012
+ background: idx.type === 'text' ? 'rgba(59,130,246,0.15)' : 'rgba(124,58,237,0.15)',
1013
+ color: idx.type === 'text' ? 'var(--info)' : 'var(--accent-light)' }}>
1014
+ ${idx.type === 'text' ? 'TEXT' : 'COMPOUND'}
1015
+ </span>
1016
+ <span style=${{ fontFamily: 'monospace', flex: 1 }}>
1017
+ { ${idx.fields.map(f => f.field + ': ' + (f.direction === 'text' ? "'text'" : f.direction)).join(', ')} }
1018
+ </span>
1019
+ ${idx.unique && html`<span style=${{ fontSize: '9px', padding: '2px 6px', borderRadius: '4px', background: 'var(--warning-glow)', color: 'var(--warning)', fontWeight: 600 }}>UNIQUE</span>`}
1020
+ <button className="btn btn-ghost btn-sm btn-icon" onClick=${() => { setIndexes(prev => prev.filter((_, j) => j !== i)); setSaved(false); }}
1021
+ style=${{ color: 'var(--error)', opacity: 0.5, padding: '4px' }}>
1022
+ ${Icons.trash}
1023
+ </button>
1024
+ </div>`)}
1025
+ </div>`}
1026
+ </div>
900
1027
  </div>`;
901
1028
  }