benresnic commited on
Commit
9dcdd68
·
verified ·
1 Parent(s): 1381b2a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +304 -382
app.py CHANGED
@@ -139,10 +139,6 @@ def _check_password(request: Request, payload: dict | None = None) -> bool:
139
 
140
  # -----------------------------------------------------------------------------
141
  # API: published cards only
142
- # GET /api/cards
143
- # POST /api/cards (password) -> publish new
144
- # PUT /api/cards/{id} (password) -> republish existing
145
- # DELETE /api/cards/{id} (password) -> delete published
146
  # -----------------------------------------------------------------------------
147
 
148
  async def api_cards(request: Request) -> Response:
@@ -219,7 +215,7 @@ async def api_card_id(request: Request) -> Response:
219
 
220
  app_ui = ui.page_fluid(
221
  ui.tags.style(
222
- """
223
  :root{
224
  --navy:#062a4d;
225
  --navy2:#041f3a;
@@ -231,6 +227,43 @@ app_ui = ui.page_fluid(
231
  }
232
  body { background: #f3f6fb; }
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  /* Top bar */
235
  .topbar{
236
  background: linear-gradient(0deg, var(--navy2), var(--navy));
@@ -278,7 +311,6 @@ app_ui = ui.page_fluid(
278
  color: var(--navy);
279
  border-color: rgba(0,0,0,.08);
280
  }
281
-
282
  #clear-filters{
283
  border-radius: 12px;
284
  padding: 9px 12px;
@@ -290,7 +322,6 @@ app_ui = ui.page_fluid(
290
  color: #fff;
291
  border: 1px solid rgba(255,255,255,.25);
292
  }
293
-
294
  #add-btn{
295
  background: white;
296
  color: var(--navy);
@@ -361,7 +392,6 @@ app_ui = ui.page_fluid(
361
  flex: 1 1 auto;
362
  min-width: 0;
363
  }
364
-
365
  .title-input, .title-text{
366
  flex: 1 1 auto;
367
  min-width: 140px;
@@ -392,7 +422,6 @@ app_ui = ui.page_fluid(
392
  padding-bottom: 2px;
393
  }
394
  .tag-bar::-webkit-scrollbar{ display:none; }
395
-
396
  .tag-chip{
397
  border: 1px solid #d8e1ee;
398
  background: #fff;
@@ -408,7 +437,6 @@ app_ui = ui.page_fluid(
408
  white-space: nowrap;
409
  }
410
  .tag-chip .rm{ font-weight: 950; opacity: .6; }
411
-
412
  .tag-btn{
413
  border: 1px dashed #cfd9ea;
414
  background: transparent;
@@ -426,7 +454,6 @@ app_ui = ui.page_fluid(
426
  gap: 8px;
427
  flex: 0 0 auto;
428
  }
429
-
430
  .draft-badge{
431
  font-size: 11px;
432
  font-weight: 950;
@@ -437,7 +464,6 @@ app_ui = ui.page_fluid(
437
  color: #9a3412;
438
  user-select:none;
439
  }
440
-
441
  .post-btn, .edit-btn{
442
  border-radius: 12px;
443
  padding: 8px 12px;
@@ -445,17 +471,14 @@ app_ui = ui.page_fluid(
445
  border: 1px solid rgba(0,0,0,.08);
446
  cursor:pointer;
447
  white-space:nowrap;
 
 
448
  }
449
  .post-btn{
450
  background: #eaf7ef;
451
  color: var(--good);
452
  }
453
  .post-btn:disabled{ opacity: .55; cursor: not-allowed; }
454
- .edit-btn{
455
- background: #ffffff;
456
- color: #1f2a44;
457
- }
458
-
459
  .del-btn{
460
  border: none;
461
  background: transparent;
@@ -482,72 +505,50 @@ app_ui = ui.page_fluid(
482
  flex: 1 1 0;
483
  min-height: 0;
484
  }
485
- label{
486
- display:block;
487
- font-size: 12px;
488
- color: var(--muted);
489
- margin: 0 0 5px;
490
- font-weight: 750;
491
- letter-spacing: .2px;
492
- }
493
- textarea{
494
- width:100%;
495
- box-sizing:border-box;
496
- border: 1px solid #d8e1ee;
497
- border-radius: 12px;
498
- padding: 10px 12px;
499
- font-size: 14px;
500
- outline: none;
501
- background: white;
502
  flex: 1 1 0;
503
  min-height: 0;
504
- resize: none;
505
- overflow: auto;
506
- line-height: 1.25;
507
  }
508
- textarea[readonly]{ background:#f8fafc; }
509
 
510
  .card-footer{
511
  padding: 10px 12px 12px 12px;
512
  border-top: 1px solid #eef2f8;
513
  flex: 0 0 auto;
514
  }
515
- input{
516
- width:100%;
517
- box-sizing:border-box;
518
- border: 1px solid #d8e1ee;
519
- border-radius: 12px;
520
- padding: 10px 12px;
521
- font-size: 14px;
522
- outline: none;
523
- background: white;
524
- }
525
- input[readonly]{ background:#f8fafc; }
526
- .resource-link{ display:inline-block; margin-top: 8px; font-size: 13px; }
527
 
528
- .by-row{
 
 
 
 
 
 
 
 
529
  display:flex;
530
- justify-content:flex-end;
531
  align-items:center;
532
  gap: 8px;
533
- margin-top: 10px;
534
- color:#1f2a44;
535
  font-size: 12px;
536
- font-weight: 850;
 
537
  }
538
  .by-input{
539
- width: 180px;
540
- padding: 7px 10px;
541
  border-radius: 10px;
542
  font-size: 12px;
543
  }
544
  .by-display{
 
545
  text-align:right;
546
- color:#1f2a44;
547
  font-size: 12px;
548
  font-weight: 900;
549
- margin-top: 10px;
 
 
550
  }
 
551
 
552
  .empty{
553
  margin: 0 16px;
@@ -558,65 +559,8 @@ app_ui = ui.page_fluid(
558
  color: #41516b;
559
  }
560
 
561
- /* Checklist popover for tags */
562
- .pop{
563
- position: fixed;
564
- background: white;
565
- border: 1px solid #d8e1ee;
566
- border-radius: 14px;
567
- box-shadow: 0 10px 30px rgba(0,0,0,.18);
568
- padding: 10px;
569
- z-index: 999999;
570
- width: 340px;
571
- max-width: calc(100vw - 24px);
572
- }
573
- .pop h4{
574
- margin: 2px 0 10px 0;
575
- font-size: 13px;
576
- font-weight: 950;
577
- color:#1f2a44;
578
- }
579
- .pop .close{
580
- position:absolute;
581
- top: 8px;
582
- right: 10px;
583
- border:none;
584
- background:transparent;
585
- font-size: 18px;
586
- font-weight: 900;
587
- cursor:pointer;
588
- color:#7b879c;
589
- }
590
- .pop .list{
591
- max-height: 260px;
592
- overflow:auto;
593
- padding-right: 4px;
594
- }
595
- .pop .checkrow{
596
- display:flex;
597
- align-items:center;
598
- gap: 8px;
599
- padding: 6px 6px;
600
- border-radius: 10px;
601
- }
602
- .pop .checkrow:hover{ background:#f6f8fe; }
603
- .pop .row{
604
- display:flex;
605
- gap: 8px;
606
- align-items:center;
607
- margin-top: 10px;
608
- }
609
- .pop .btn{
610
- border-radius: 12px;
611
- padding: 9px 12px;
612
- font-weight: 900;
613
- cursor:pointer;
614
- border: 1px solid #d8e1ee;
615
- background: #ffffff;
616
- }
617
-
618
- /* Generic modal shell (shorter, scrolls internally) */
619
- .modal-backdrop{
620
  position: fixed;
621
  inset: 0;
622
  background: rgba(0,0,0,.35);
@@ -624,10 +568,11 @@ app_ui = ui.page_fluid(
624
  align-items:center;
625
  justify-content:center;
626
  z-index: 999999;
 
627
  }
628
- .modal{
629
- width: min(620px, calc(100vw - 28px));
630
- max-height: 85vh;
631
  background: white;
632
  border-radius: 14px;
633
  box-shadow: 0 10px 30px rgba(0,0,0,.25);
@@ -635,18 +580,18 @@ app_ui = ui.page_fluid(
635
  flex-direction:column;
636
  overflow:hidden;
637
  }
638
- .modal .head{
639
- padding: 14px 14px 10px 14px;
640
  border-bottom: 1px solid #eef2f8;
641
  }
642
- .modal .head h3{ margin: 0 0 6px 0; font-size: 16px; }
643
- .modal .muted{ color: var(--muted); font-size: 13px; }
644
- .modal .body{
645
  padding: 12px 14px;
646
  overflow:auto;
647
  }
648
- .modal .foot{
649
- padding: 12px 14px;
650
  border-top: 1px solid #eef2f8;
651
  display:flex;
652
  gap: 10px;
@@ -655,7 +600,7 @@ app_ui = ui.page_fluid(
655
  .btn{
656
  border-radius: 12px;
657
  padding: 9px 12px;
658
- font-weight: 850;
659
  cursor:pointer;
660
  border: 1px solid #d8e1ee;
661
  background: #ffffff;
@@ -664,35 +609,51 @@ app_ui = ui.page_fluid(
664
  background:#eaf7ef;
665
  border-color:#bbf7d0;
666
  color: var(--good);
667
- font-weight: 950;
668
  }
669
  .btn-danger{
670
  border: 1px solid #fecdd3;
671
  background: #fff1f2;
672
  color: #9f1239;
673
- font-weight: 950;
674
  }
675
  .btn[disabled]{ opacity: .55; cursor: not-allowed; }
676
 
677
- /* Edit form */
678
- .edit-grid{
679
- display:grid;
680
- grid-template-columns: 1fr;
 
 
 
 
 
681
  gap: 10px;
 
 
 
 
 
 
 
 
 
 
682
  }
683
- .edit-tags{
 
684
  display:flex;
685
- flex-wrap: wrap;
686
- gap: 8px;
687
  align-items:center;
 
688
  }
689
  """
690
  ),
691
  ui.tags.div(
692
  {"class": "topbar"},
693
- ui.tags.div({"class": "top-left"},
694
- ui.tags.div({"id": "chips-row", "class": "chips-row"}),
695
- ui.tags.button("Clear", {"id": "clear-filters", "type": "button"})),
 
 
696
  ui.tags.button("+", {"id": "add-btn", "type": "button"}),
697
  ),
698
  ui.tags.div({"id": "err", "class": "err"}),
@@ -700,7 +661,7 @@ app_ui = ui.page_fluid(
700
  ui.tags.script(
701
  r"""
702
  (() => {
703
- const DRAFTS_KEY = "model_cards_drafts_v4";
704
 
705
  const grid = document.getElementById("grid");
706
  const addBtn = document.getElementById("add-btn");
@@ -721,8 +682,6 @@ app_ui = ui.page_fluid(
721
  const saveTimers = new Map();
722
  const filterTags = new Set();
723
 
724
- let openPop = null;
725
-
726
  function showErr(msg){
727
  if(!msg){ errBox.style.display="none"; errBox.textContent=""; return; }
728
  errBox.style.display="block";
@@ -841,169 +800,139 @@ app_ui = ui.page_fluid(
841
  });
842
 
843
  // ------------------------------------------------------------
844
- // Tag checklist popover
845
- // ------------------------------------------------------------
846
- function closePop(){
847
- if(openPop){ openPop.remove(); openPop = null; }
848
- }
849
-
850
- function openPopoverNear(anchorEl, html){
851
- closePop();
852
- const pop = document.createElement("div");
853
- pop.className = "pop";
854
- pop.innerHTML = html;
855
- document.body.appendChild(pop);
856
- openPop = pop;
857
-
858
- const r = anchorEl.getBoundingClientRect();
859
- const pad = 10;
860
- let left = Math.min(r.left, window.innerWidth - pop.offsetWidth - pad);
861
- left = Math.max(pad, left);
862
- let top = r.bottom + 10;
863
- if(top + pop.offsetHeight + pad > window.innerHeight){
864
- top = Math.max(pad, r.top - pop.offsetHeight - 10);
865
- }
866
- pop.style.left = `${left}px`;
867
- pop.style.top = `${top}px`;
868
-
869
- const closeBtn = pop.querySelector(".close");
870
- if(closeBtn) closeBtn.addEventListener("click", closePop);
871
-
872
- setTimeout(() => {
873
- const onDoc = (e) => {
874
- if(!openPop) return;
875
- if(openPop.contains(e.target)) return;
876
- if(anchorEl.contains(e.target)) return;
877
- closePop();
878
- document.removeEventListener("mousedown", onDoc);
879
- };
880
- document.addEventListener("mousedown", onDoc);
881
- }, 0);
882
-
883
- return pop;
884
- }
885
-
886
- function openTagChecklist(anchorEl, currentTags, onChange){
887
- const all = tagUniverse();
888
- const html = `
889
- <button class="close" type="button">×</button>
890
- <h4>Tags</h4>
891
- <div class="list">
892
- ${all.map(t => {
893
- const checked = currentTags.includes(t) ? "checked" : "";
894
- return `
895
- <div class="checkrow">
896
- <input type="checkbox" class="tg" data-tag="${escapeAttr(t)}" ${checked}/>
897
- <div style="font-weight:850; font-size:12px; color:#1f2a44;">${escapeHtml(t)}</div>
898
- </div>
899
- `;
900
- }).join("")}
901
- </div>
902
-
903
- <div class="row">
904
- <input id="custom" type="text" placeholder="Add custom tag…" />
905
- <button class="btn" id="addCustom" type="button">Add</button>
906
- </div>
907
- `;
908
- const pop = openPopoverNear(anchorEl, html);
909
-
910
- pop.querySelectorAll(".tg").forEach(cb => {
911
- cb.addEventListener("change", () => {
912
- const t = cb.getAttribute("data-tag");
913
- if(!t) return;
914
- const next = new Set(currentTags);
915
- if(cb.checked) next.add(t);
916
- else next.delete(t);
917
- currentTags = Array.from(next);
918
- onChange(currentTags);
919
- });
920
- });
921
-
922
- const custom = pop.querySelector("#custom");
923
- const addCustom = pop.querySelector("#addCustom");
924
-
925
- function doAdd(){
926
- const t = (custom.value || "").trim();
927
- if(!t) return;
928
- if(!currentTags.includes(t)){
929
- currentTags = [...currentTags, t];
930
- onChange(currentTags);
931
- }
932
- custom.value = "";
933
- renderFilterChips(); // new tag becomes filterable
934
- closePop();
935
- }
936
-
937
- addCustom.addEventListener("click", doAdd);
938
- custom.addEventListener("keydown", (e) => {
939
- if(e.key === "Enter"){
940
- e.preventDefault();
941
- doAdd();
942
- }
943
- });
944
-
945
- custom.focus();
946
- }
947
-
948
- // ------------------------------------------------------------
949
- // Generic modal helpers + Enter-to-confirm
950
  // ------------------------------------------------------------
951
- function openModal({title, subtitle, bodyHtml, primaryText, primaryClass, isPrimaryEnabled, onPrimary, onMount}){
952
  const backdrop = document.createElement("div");
953
- backdrop.className = "modal-backdrop";
954
  backdrop.innerHTML = `
955
- <div class="modal" role="dialog" aria-modal="true">
956
  <div class="head">
957
  <h3>${escapeHtml(title)}</h3>
958
  <div class="muted">${escapeHtml(subtitle || "")}</div>
959
  </div>
960
  <div class="body">${bodyHtml}</div>
961
  <div class="foot">
962
- <button class="btn" id="cancel" type="button">Cancel</button>
963
- <button class="btn ${primaryClass || ""}" id="primary" type="button" disabled>${escapeHtml(primaryText || "Confirm")}</button>
964
  </div>
965
  </div>
966
  `;
967
  document.body.appendChild(backdrop);
968
 
969
- const cancel = backdrop.querySelector("#cancel");
970
- const primary = backdrop.querySelector("#primary");
 
971
 
972
  function close(){ backdrop.remove(); }
973
 
974
- cancel.addEventListener("click", (e) => { e.preventDefault(); close(); });
975
- backdrop.addEventListener("click", (e) => { if(e.target === backdrop) close(); });
 
 
 
 
 
976
 
977
- async function doPrimary(){
978
- if(primary.disabled) return;
979
- try{ await onPrimary(); }
980
- catch(e){ showErr(String(e.message || e)); }
 
 
 
 
981
  close();
982
  }
983
 
984
- primary.addEventListener("click", (e) => { e.preventDefault(); doPrimary(); });
985
-
986
- function setEnabled(ok){ primary.disabled = !ok; }
987
- function refreshEnabled(){
988
- try{ setEnabled(!!isPrimaryEnabled()); } catch { setEnabled(false); }
989
- }
990
 
991
- // Enter triggers primary if enabled
992
  backdrop.addEventListener("keydown", (e) => {
993
  if(e.key === "Enter"){
994
  const active = document.activeElement;
995
- // allow Enter in textarea without submitting
996
  if(active && active.tagName === "TEXTAREA") return;
997
- if(!primary.disabled){
998
  e.preventDefault();
999
- doPrimary();
1000
  }
1001
  }
1002
  });
1003
 
1004
- if(onMount) onMount({backdrop, primary, refreshEnabled, close});
1005
- refreshEnabled();
1006
- return { close };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1007
  }
1008
 
1009
  // ------------------------------------------------------------
@@ -1030,7 +959,7 @@ app_ui = ui.page_fluid(
1030
 
1031
  grid.innerHTML = "";
1032
 
1033
- // ---------------- Drafts (private) ----------------
1034
  visibleDrafts.forEach((d) => {
1035
  const card = document.createElement("div");
1036
  card.className = "card";
@@ -1069,12 +998,15 @@ app_ui = ui.page_fluid(
1069
 
1070
  <div class="card-footer">
1071
  <label>Link</label>
1072
- <input type="text" class="link" value="${escapeHtml(d.link)}" />
1073
- <div class="linkwrap"></div>
1074
-
1075
- <div class="by-row">
1076
- <span>By:</span>
1077
- <input class="by-input" type="text" value="${escapeHtml(d.author)}" placeholder="name" />
 
 
 
1078
  </div>
1079
  </div>
1080
  `;
@@ -1153,26 +1085,29 @@ app_ui = ui.page_fluid(
1153
  }, 150));
1154
  }
1155
 
1156
- // Tags checklist
1157
  tagBtn.addEventListener("click", (e) => {
1158
  e.preventDefault();
1159
  e.stopPropagation();
1160
  if(!(titleEl.value || "").trim()) return;
1161
- openTagChecklist(tagBtn, tags, (next) => {
1162
- tags = next.slice();
1163
- setDraft({ tags });
1164
- renderTags();
1165
- renderFilterChips();
1166
- updateHeader();
 
 
 
1167
  });
1168
  });
1169
 
1170
- // Post (password modal; Enter works)
1171
  postBtn.addEventListener("click", () => {
1172
  const cur = getDraft();
1173
  if(!cur || !draftFilled(cur)) return;
1174
 
1175
- openModal({
1176
  title: "Post this card?",
1177
  subtitle: "This will publish the card for everyone to see.",
1178
  bodyHtml: `
@@ -1181,13 +1116,9 @@ app_ui = ui.page_fluid(
1181
  `,
1182
  primaryText: "Post",
1183
  primaryClass: "btn-primary",
1184
- isPrimaryEnabled: () => {
1185
- const pw = document.getElementById("pw");
1186
- return pw && (pw.value || "").trim().length > 0;
1187
- },
1188
- onPrimary: async () => {
1189
- const pw = document.getElementById("pw");
1190
- const password = (pw.value || "").trim();
1191
  const payload = {
1192
  password,
1193
  model: cur.model,
@@ -1213,10 +1144,10 @@ app_ui = ui.page_fluid(
1213
  renderFilterChips();
1214
  render();
1215
  },
1216
- onMount: ({backdrop, refreshEnabled}) => {
1217
  const pw = backdrop.querySelector("#pw");
1218
  pw.focus();
1219
- pw.addEventListener("input", refreshEnabled);
1220
  }
1221
  });
1222
  });
@@ -1230,7 +1161,6 @@ app_ui = ui.page_fluid(
1230
  render();
1231
  });
1232
 
1233
- // Enter should not submit from textareas; handled by modal only.
1234
  [titleEl, howEl, useEl, linkEl, byEl].forEach(el => el.addEventListener("input", scheduleDraftSave));
1235
 
1236
  updateLink();
@@ -1239,7 +1169,7 @@ app_ui = ui.page_fluid(
1239
  grid.appendChild(card);
1240
  });
1241
 
1242
- // ---------------- Published (shared) ----------------
1243
  visiblePublished.forEach((c) => {
1244
  const card = document.createElement("div");
1245
  card.className = "card";
@@ -1272,9 +1202,15 @@ app_ui = ui.page_fluid(
1272
 
1273
  <div class="card-footer">
1274
  <label>Link</label>
1275
- <input readonly value="${escapeHtml(c.link || "")}" />
1276
- <div class="linkwrap"></div>
1277
- <div class="by-display">${c.author ? ("By: " + escapeHtml(c.author)) : ""}</div>
 
 
 
 
 
 
1278
  </div>
1279
  `;
1280
 
@@ -1287,34 +1223,32 @@ app_ui = ui.page_fluid(
1287
  ? `<a class="resource-link" href="${escapeAttr(url)}" target="_blank" rel="noopener noreferrer">Open link</a>`
1288
  : "";
1289
 
1290
- // Delete published (password + 2 checks; Enter works)
1291
  delBtn.addEventListener("click", () => {
1292
- openModal({
1293
  title: "Delete published card?",
1294
  subtitle: "This is permanent and affects everyone viewing this Space.",
1295
  bodyHtml: `
1296
- <div style="display:flex; flex-direction:column; gap:8px; margin-bottom:10px;">
1297
- <label style="display:flex; gap:8px; align-items:center; font-weight:700; color:#1f2a44;">
1298
- <input type="checkbox" id="c1" /> I understand this is permanent
1299
- </label>
1300
- <label style="display:flex; gap:8px; align-items:center; font-weight:700; color:#1f2a44;">
1301
- <input type="checkbox" id="c2" /> I confirm I want to delete this card
1302
- </label>
 
 
1303
  </div>
1304
  <label>Password</label>
1305
  <input type="password" id="pw" value="" />
1306
  `,
1307
  primaryText: "Delete",
1308
  primaryClass: "btn-danger",
1309
- isPrimaryEnabled: () => {
1310
- const c1 = document.getElementById("c1");
1311
- const c2 = document.getElementById("c2");
1312
- const pw = document.getElementById("pw");
1313
- return c1?.checked && c2?.checked && pw && (pw.value || "").trim().length > 0;
1314
- },
1315
- onPrimary: async () => {
1316
- const pw = document.getElementById("pw");
1317
- const password = (pw.value || "").trim();
1318
  const data = await api(`/api/cards/${encodeURIComponent(c.id)}`, {
1319
  method: "DELETE",
1320
  headers: { "X-Password": password },
@@ -1325,26 +1259,26 @@ app_ui = ui.page_fluid(
1325
  renderFilterChips();
1326
  render();
1327
  },
1328
- onMount: ({backdrop, refreshEnabled}) => {
1329
  const pw = backdrop.querySelector("#pw");
1330
  const c1 = backdrop.querySelector("#c1");
1331
  const c2 = backdrop.querySelector("#c2");
1332
  pw.focus();
1333
- [pw, c1, c2].forEach(el => el.addEventListener("input", refreshEnabled));
1334
- [c1, c2].forEach(el => el.addEventListener("change", refreshEnabled));
1335
  }
1336
  });
1337
  });
1338
 
1339
- // Edit & Republish (short modal; internal scroll; tags checklist; Enter republish)
1340
  editBtn.addEventListener("click", () => {
1341
  let tagsLocal = tags.slice();
1342
 
1343
- openModal({
1344
  title: "Edit & Republish",
1345
  subtitle: "Edits go live only after you enter the password and republish.",
1346
  bodyHtml: `
1347
- <div class="edit-grid">
1348
  <div>
1349
  <label>Model</label>
1350
  <input id="m" type="text" value="${escapeHtml(c.model || "")}" />
@@ -1352,30 +1286,31 @@ app_ui = ui.page_fluid(
1352
 
1353
  <div>
1354
  <label>Tags</label>
1355
- <div class="edit-tags" id="tags"></div>
1356
  <div style="margin-top:8px;">
1357
- <button class="tag-btn" id="tagChecklist" type="button">Tags</button>
1358
  </div>
1359
  </div>
1360
 
1361
  <div>
1362
  <label>How it Works</label>
1363
- <textarea id="h">${escapeHtml(c.how_it_works || "")}</textarea>
1364
  </div>
1365
 
1366
  <div>
1367
  <label>Use Cases</label>
1368
- <textarea id="u">${escapeHtml(c.use_cases || "")}</textarea>
1369
- </div>
1370
-
1371
- <div>
1372
- <label>Link</label>
1373
- <input id="l" type="text" value="${escapeHtml(c.link || "")}" />
1374
  </div>
1375
 
1376
- <div style="display:flex; justify-content:flex-end; gap:8px; align-items:center;">
1377
- <label style="margin:0; font-weight:900;">By:</label>
1378
- <input id="a" type="text" style="width:220px;" value="${escapeHtml(c.author || "")}" placeholder="name" />
 
 
 
 
 
 
1379
  </div>
1380
 
1381
  <div>
@@ -1386,33 +1321,20 @@ app_ui = ui.page_fluid(
1386
  `,
1387
  primaryText: "Republish",
1388
  primaryClass: "btn-primary",
1389
- isPrimaryEnabled: () => {
1390
- const m = document.getElementById("m");
1391
- const h = document.getElementById("h");
1392
- const u = document.getElementById("u");
1393
- const pw = document.getElementById("pw");
1394
- return (pw?.value || "").trim().length > 0
1395
- && (m?.value || "").trim().length > 0
1396
- && (h?.value || "").trim().length > 0
1397
- && (u?.value || "").trim().length > 0;
1398
- },
1399
- onPrimary: async () => {
1400
- const m = document.getElementById("m");
1401
- const h = document.getElementById("h");
1402
- const u = document.getElementById("u");
1403
- const l = document.getElementById("l");
1404
- const a = document.getElementById("a");
1405
- const pw = document.getElementById("pw");
1406
- const password = (pw.value || "").trim();
1407
-
1408
  const payload = {
1409
  password,
1410
- model: m.value || "",
1411
  tags: tagsLocal,
1412
- how_it_works: h.value || "",
1413
- use_cases: u.value || "",
1414
- link: l.value || "",
1415
- author: a.value || "",
1416
  };
1417
 
1418
  const data = await api(`/api/cards/${encodeURIComponent(c.id)}`, {
@@ -1427,13 +1349,13 @@ app_ui = ui.page_fluid(
1427
  renderFilterChips();
1428
  render();
1429
  },
1430
- onMount: ({backdrop, refreshEnabled}) => {
1431
- const tagsEl = backdrop.querySelector("#tags");
1432
- const tagBtn = backdrop.querySelector("#tagChecklist");
1433
  const pw = backdrop.querySelector("#pw");
1434
 
1435
  function renderEditTags(){
1436
- tagsEl.innerHTML = "";
1437
  tagsLocal.forEach(t => {
1438
  const chip = document.createElement("span");
1439
  chip.className = "tag-chip";
@@ -1443,32 +1365,32 @@ app_ui = ui.page_fluid(
1443
  tagsLocal = tagsLocal.filter(x => x !== t);
1444
  renderEditTags();
1445
  renderFilterChips();
1446
- refreshEnabled();
1447
  });
1448
- tagsEl.appendChild(chip);
1449
  });
1450
  }
1451
 
1452
  tagBtn.addEventListener("click", (e) => {
1453
  e.preventDefault();
1454
- e.stopPropagation();
1455
- openTagChecklist(tagBtn, tagsLocal, (next) => {
1456
- tagsLocal = next.slice();
1457
- renderEditTags();
1458
- renderFilterChips();
1459
- refreshEnabled();
 
 
1460
  });
1461
  });
1462
 
1463
- // enable/disable republish as user types
1464
  ["m","h","u","l","a","pw"].forEach(id => {
1465
- const el = backdrop.querySelector("#" + id);
1466
- el.addEventListener("input", refreshEnabled);
1467
  });
1468
 
1469
  pw.focus();
1470
  renderEditTags();
1471
- refreshEnabled();
1472
  }
1473
  });
1474
  });
 
139
 
140
  # -----------------------------------------------------------------------------
141
  # API: published cards only
 
 
 
 
142
  # -----------------------------------------------------------------------------
143
 
144
  async def api_cards(request: Request) -> Response:
 
215
 
216
  app_ui = ui.page_fluid(
217
  ui.tags.style(
218
+ r"""
219
  :root{
220
  --navy:#062a4d;
221
  --navy2:#041f3a;
 
227
  }
228
  body { background: #f3f6fb; }
229
 
230
+ /* IMPORTANT: do NOT force checkboxes to width:100% */
231
+ input:not([type="checkbox"]):not([type="radio"]) {
232
+ width: 100%;
233
+ box-sizing: border-box;
234
+ border: 1px solid #d8e1ee;
235
+ border-radius: 12px;
236
+ padding: 10px 12px;
237
+ font-size: 14px;
238
+ outline: none;
239
+ background: white;
240
+ }
241
+ input[readonly]:not([type="checkbox"]):not([type="radio"]) { background:#f8fafc; }
242
+
243
+ textarea{
244
+ width:100%;
245
+ box-sizing:border-box;
246
+ border: 1px solid #d8e1ee;
247
+ border-radius: 12px;
248
+ padding: 10px 12px;
249
+ font-size: 14px;
250
+ outline: none;
251
+ background: white;
252
+ resize: none;
253
+ overflow: auto;
254
+ line-height: 1.25;
255
+ }
256
+ textarea[readonly]{ background:#f8fafc; }
257
+
258
+ label{
259
+ display:block;
260
+ font-size: 12px;
261
+ color: var(--muted);
262
+ margin: 0 0 5px;
263
+ font-weight: 750;
264
+ letter-spacing: .2px;
265
+ }
266
+
267
  /* Top bar */
268
  .topbar{
269
  background: linear-gradient(0deg, var(--navy2), var(--navy));
 
311
  color: var(--navy);
312
  border-color: rgba(0,0,0,.08);
313
  }
 
314
  #clear-filters{
315
  border-radius: 12px;
316
  padding: 9px 12px;
 
322
  color: #fff;
323
  border: 1px solid rgba(255,255,255,.25);
324
  }
 
325
  #add-btn{
326
  background: white;
327
  color: var(--navy);
 
392
  flex: 1 1 auto;
393
  min-width: 0;
394
  }
 
395
  .title-input, .title-text{
396
  flex: 1 1 auto;
397
  min-width: 140px;
 
422
  padding-bottom: 2px;
423
  }
424
  .tag-bar::-webkit-scrollbar{ display:none; }
 
425
  .tag-chip{
426
  border: 1px solid #d8e1ee;
427
  background: #fff;
 
437
  white-space: nowrap;
438
  }
439
  .tag-chip .rm{ font-weight: 950; opacity: .6; }
 
440
  .tag-btn{
441
  border: 1px dashed #cfd9ea;
442
  background: transparent;
 
454
  gap: 8px;
455
  flex: 0 0 auto;
456
  }
 
457
  .draft-badge{
458
  font-size: 11px;
459
  font-weight: 950;
 
464
  color: #9a3412;
465
  user-select:none;
466
  }
 
467
  .post-btn, .edit-btn{
468
  border-radius: 12px;
469
  padding: 8px 12px;
 
471
  border: 1px solid rgba(0,0,0,.08);
472
  cursor:pointer;
473
  white-space:nowrap;
474
+ background: #fff;
475
+ color: #1f2a44;
476
  }
477
  .post-btn{
478
  background: #eaf7ef;
479
  color: var(--good);
480
  }
481
  .post-btn:disabled{ opacity: .55; cursor: not-allowed; }
 
 
 
 
 
482
  .del-btn{
483
  border: none;
484
  background: transparent;
 
505
  flex: 1 1 0;
506
  min-height: 0;
507
  }
508
+ .panel textarea{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  flex: 1 1 0;
510
  min-height: 0;
 
 
 
511
  }
 
512
 
513
  .card-footer{
514
  padding: 10px 12px 12px 12px;
515
  border-top: 1px solid #eef2f8;
516
  flex: 0 0 auto;
517
  }
 
 
 
 
 
 
 
 
 
 
 
 
518
 
519
+ /* Link + By on same line */
520
+ .footer-row{
521
+ display:flex;
522
+ align-items:center;
523
+ gap: 10px;
524
+ }
525
+ .link-col{ flex: 1 1 auto; min-width: 0; }
526
+ .by-col{
527
+ flex: 0 0 auto;
528
  display:flex;
 
529
  align-items:center;
530
  gap: 8px;
531
+ white-space: nowrap;
 
532
  font-size: 12px;
533
+ font-weight: 900;
534
+ color:#1f2a44;
535
  }
536
  .by-input{
537
+ width: 160px;
538
+ padding: 8px 10px;
539
  border-radius: 10px;
540
  font-size: 12px;
541
  }
542
  .by-display{
543
+ width: 200px;
544
  text-align:right;
 
545
  font-size: 12px;
546
  font-weight: 900;
547
+ color:#1f2a44;
548
+ overflow:hidden;
549
+ text-overflow: ellipsis;
550
  }
551
+ .resource-link{ display:inline-block; margin-top: 8px; font-size: 13px; }
552
 
553
  .empty{
554
  margin: 0 16px;
 
559
  color: #41516b;
560
  }
561
 
562
+ /* Centered dialogs (renamed to avoid collisions) */
563
+ .dlg-backdrop{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  position: fixed;
565
  inset: 0;
566
  background: rgba(0,0,0,.35);
 
568
  align-items:center;
569
  justify-content:center;
570
  z-index: 999999;
571
+ padding: 14px;
572
  }
573
+ .dlg{
574
+ width: min(560px, calc(100vw - 28px));
575
+ max-height: 80vh;
576
  background: white;
577
  border-radius: 14px;
578
  box-shadow: 0 10px 30px rgba(0,0,0,.25);
 
580
  flex-direction:column;
581
  overflow:hidden;
582
  }
583
+ .dlg .head{
584
+ padding: 12px 14px 10px 14px;
585
  border-bottom: 1px solid #eef2f8;
586
  }
587
+ .dlg .head h3{ margin: 0 0 6px 0; font-size: 16px; }
588
+ .dlg .muted{ color: var(--muted); font-size: 13px; }
589
+ .dlg .body{
590
  padding: 12px 14px;
591
  overflow:auto;
592
  }
593
+ .dlg .foot{
594
+ padding: 10px 14px 12px 14px;
595
  border-top: 1px solid #eef2f8;
596
  display:flex;
597
  gap: 10px;
 
600
  .btn{
601
  border-radius: 12px;
602
  padding: 9px 12px;
603
+ font-weight: 900;
604
  cursor:pointer;
605
  border: 1px solid #d8e1ee;
606
  background: #ffffff;
 
609
  background:#eaf7ef;
610
  border-color:#bbf7d0;
611
  color: var(--good);
 
612
  }
613
  .btn-danger{
614
  border: 1px solid #fecdd3;
615
  background: #fff1f2;
616
  color: #9f1239;
 
617
  }
618
  .btn[disabled]{ opacity: .55; cursor: not-allowed; }
619
 
620
+ /* Checklist layout aligned */
621
+ .checklist{
622
+ display:flex;
623
+ flex-direction:column;
624
+ gap: 6px;
625
+ }
626
+ .checkrow{
627
+ display:flex;
628
+ align-items:center;
629
  gap: 10px;
630
+ padding: 6px 8px;
631
+ border-radius: 10px;
632
+ }
633
+ .checkrow:hover{ background:#f6f8fe; }
634
+ .checkrow input[type="checkbox"]{ width:auto !important; }
635
+ .checkrow .lbl{
636
+ font-weight: 850;
637
+ font-size: 12px;
638
+ color:#1f2a44;
639
+ line-height: 1.1;
640
  }
641
+
642
+ .row{
643
  display:flex;
644
+ gap: 10px;
 
645
  align-items:center;
646
+ margin-top: 10px;
647
  }
648
  """
649
  ),
650
  ui.tags.div(
651
  {"class": "topbar"},
652
+ ui.tags.div(
653
+ {"class": "top-left"},
654
+ ui.tags.div({"id": "chips-row", "class": "chips-row"}),
655
+ ui.tags.button("Clear", {"id": "clear-filters", "type": "button"}),
656
+ ),
657
  ui.tags.button("+", {"id": "add-btn", "type": "button"}),
658
  ),
659
  ui.tags.div({"id": "err", "class": "err"}),
 
661
  ui.tags.script(
662
  r"""
663
  (() => {
664
+ const DRAFTS_KEY = "model_cards_drafts_v5";
665
 
666
  const grid = document.getElementById("grid");
667
  const addBtn = document.getElementById("add-btn");
 
682
  const saveTimers = new Map();
683
  const filterTags = new Set();
684
 
 
 
685
  function showErr(msg){
686
  if(!msg){ errBox.style.display="none"; errBox.textContent=""; return; }
687
  errBox.style.display="block";
 
800
  });
801
 
802
  // ------------------------------------------------------------
803
+ // Centered dialog helper (Enter triggers primary unless textarea)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
  // ------------------------------------------------------------
805
+ function openDialog({title, subtitle, bodyHtml, primaryText, primaryClass, canSubmit, onSubmit, onOpen}){
806
  const backdrop = document.createElement("div");
807
+ backdrop.className = "dlg-backdrop";
808
  backdrop.innerHTML = `
809
+ <div class="dlg" role="dialog" aria-modal="true">
810
  <div class="head">
811
  <h3>${escapeHtml(title)}</h3>
812
  <div class="muted">${escapeHtml(subtitle || "")}</div>
813
  </div>
814
  <div class="body">${bodyHtml}</div>
815
  <div class="foot">
816
+ <button class="btn" data-act="cancel" type="button">Cancel</button>
817
+ <button class="btn ${primaryClass || ""}" data-act="primary" type="button" disabled>${escapeHtml(primaryText || "Confirm")}</button>
818
  </div>
819
  </div>
820
  `;
821
  document.body.appendChild(backdrop);
822
 
823
+ const dlg = backdrop.querySelector(".dlg");
824
+ const btnCancel = backdrop.querySelector('[data-act="cancel"]');
825
+ const btnPrimary = backdrop.querySelector('[data-act="primary"]');
826
 
827
  function close(){ backdrop.remove(); }
828
 
829
+ function refresh(){
830
+ try{
831
+ btnPrimary.disabled = !(canSubmit ? canSubmit(backdrop) : true);
832
+ }catch{
833
+ btnPrimary.disabled = true;
834
+ }
835
+ }
836
 
837
+ async function submit(){
838
+ if(btnPrimary.disabled) return;
839
+ try{
840
+ await onSubmit(backdrop);
841
+ }catch(e){
842
+ showErr(String(e.message || e));
843
+ return;
844
+ }
845
  close();
846
  }
847
 
848
+ btnCancel.addEventListener("click", (e) => { e.preventDefault(); close(); });
849
+ backdrop.addEventListener("click", (e) => { if(e.target === backdrop) close(); });
850
+ btnPrimary.addEventListener("click", (e) => { e.preventDefault(); submit(); });
 
 
 
851
 
 
852
  backdrop.addEventListener("keydown", (e) => {
853
  if(e.key === "Enter"){
854
  const active = document.activeElement;
 
855
  if(active && active.tagName === "TEXTAREA") return;
856
+ if(!btnPrimary.disabled){
857
  e.preventDefault();
858
+ submit();
859
  }
860
  }
861
  });
862
 
863
+ // initial
864
+ refresh();
865
+ if(onOpen) onOpen({backdrop, dlg, refresh, close});
866
+ return { close, refresh };
867
+ }
868
+
869
+ // ------------------------------------------------------------
870
+ // Tags checklist dialog (centered)
871
+ // ------------------------------------------------------------
872
+ function openTagsDialog({selected, onApply}){
873
+ const all = tagUniverse();
874
+ const selectedSet = new Set(selected);
875
+
876
+ const listHtml = all.map(t => {
877
+ const checked = selectedSet.has(t) ? "checked" : "";
878
+ return `
879
+ <div class="checkrow">
880
+ <input type="checkbox" class="tg" data-tag="${escapeAttr(t)}" ${checked}/>
881
+ <div class="lbl">${escapeHtml(t)}</div>
882
+ </div>
883
+ `;
884
+ }).join("");
885
+
886
+ openDialog({
887
+ title: "Tags",
888
+ subtitle: "Check all that apply.",
889
+ bodyHtml: `
890
+ <div class="checklist">${listHtml}</div>
891
+ <div class="row">
892
+ <input id="custom" type="text" placeholder="Add custom tag…" />
893
+ <button class="btn" id="addCustom" type="button">Add</button>
894
+ </div>
895
+ `,
896
+ primaryText: "Apply",
897
+ primaryClass: "btn-primary",
898
+ canSubmit: () => true,
899
+ onSubmit: async (backdrop) => {
900
+ const checked = [];
901
+ backdrop.querySelectorAll(".tg").forEach(cb => {
902
+ if(cb.checked) checked.push(cb.getAttribute("data-tag"));
903
+ });
904
+ onApply(checked.filter(Boolean));
905
+ },
906
+ onOpen: ({backdrop, refresh}) => {
907
+ // custom add
908
+ const custom = backdrop.querySelector("#custom");
909
+ const addBtn = backdrop.querySelector("#addCustom");
910
+
911
+ function addCustom(){
912
+ const t = (custom.value || "").trim();
913
+ if(!t) return;
914
+ // add to UI by appending a new row checked
915
+ const wrap = backdrop.querySelector(".checklist");
916
+ const row = document.createElement("div");
917
+ row.className = "checkrow";
918
+ row.innerHTML = `<input type="checkbox" class="tg" data-tag="${escapeAttr(t)}" checked/><div class="lbl">${escapeHtml(t)}</div>`;
919
+ wrap.prepend(row);
920
+ custom.value = "";
921
+ renderFilterChips();
922
+ refresh();
923
+ }
924
+
925
+ addBtn.addEventListener("click", (e) => { e.preventDefault(); addCustom(); });
926
+ custom.addEventListener("keydown", (e) => {
927
+ if(e.key === "Enter"){
928
+ e.preventDefault();
929
+ addCustom();
930
+ }
931
+ });
932
+
933
+ custom.focus();
934
+ }
935
+ });
936
  }
937
 
938
  // ------------------------------------------------------------
 
959
 
960
  grid.innerHTML = "";
961
 
962
+ // ---------------- Drafts ----------------
963
  visibleDrafts.forEach((d) => {
964
  const card = document.createElement("div");
965
  card.className = "card";
 
998
 
999
  <div class="card-footer">
1000
  <label>Link</label>
1001
+ <div class="footer-row">
1002
+ <div class="link-col">
1003
+ <input type="text" class="link" value="${escapeHtml(d.link)}" placeholder="https://..." />
1004
+ <div class="linkwrap"></div>
1005
+ </div>
1006
+ <div class="by-col">
1007
+ <span>By:</span>
1008
+ <input class="by-input" type="text" value="${escapeHtml(d.author)}" placeholder="name" />
1009
+ </div>
1010
  </div>
1011
  </div>
1012
  `;
 
1085
  }, 150));
1086
  }
1087
 
1088
+ // Tags dialog
1089
  tagBtn.addEventListener("click", (e) => {
1090
  e.preventDefault();
1091
  e.stopPropagation();
1092
  if(!(titleEl.value || "").trim()) return;
1093
+ openTagsDialog({
1094
+ selected: tags,
1095
+ onApply: (next) => {
1096
+ tags = next.slice();
1097
+ setDraft({ tags });
1098
+ renderTags();
1099
+ renderFilterChips();
1100
+ updateHeader();
1101
+ }
1102
  });
1103
  });
1104
 
1105
+ // Post dialog
1106
  postBtn.addEventListener("click", () => {
1107
  const cur = getDraft();
1108
  if(!cur || !draftFilled(cur)) return;
1109
 
1110
+ openDialog({
1111
  title: "Post this card?",
1112
  subtitle: "This will publish the card for everyone to see.",
1113
  bodyHtml: `
 
1116
  `,
1117
  primaryText: "Post",
1118
  primaryClass: "btn-primary",
1119
+ canSubmit: (backdrop) => (backdrop.querySelector("#pw").value || "").trim().length > 0,
1120
+ onSubmit: async (backdrop) => {
1121
+ const password = (backdrop.querySelector("#pw").value || "").trim();
 
 
 
 
1122
  const payload = {
1123
  password,
1124
  model: cur.model,
 
1144
  renderFilterChips();
1145
  render();
1146
  },
1147
+ onOpen: ({backdrop, refresh}) => {
1148
  const pw = backdrop.querySelector("#pw");
1149
  pw.focus();
1150
+ pw.addEventListener("input", refresh);
1151
  }
1152
  });
1153
  });
 
1161
  render();
1162
  });
1163
 
 
1164
  [titleEl, howEl, useEl, linkEl, byEl].forEach(el => el.addEventListener("input", scheduleDraftSave));
1165
 
1166
  updateLink();
 
1169
  grid.appendChild(card);
1170
  });
1171
 
1172
+ // ---------------- Published ----------------
1173
  visiblePublished.forEach((c) => {
1174
  const card = document.createElement("div");
1175
  card.className = "card";
 
1202
 
1203
  <div class="card-footer">
1204
  <label>Link</label>
1205
+ <div class="footer-row">
1206
+ <div class="link-col">
1207
+ <input readonly value="${escapeHtml(c.link || "")}" />
1208
+ <div class="linkwrap"></div>
1209
+ </div>
1210
+ <div class="by-col">
1211
+ <div class="by-display">${c.author ? ("By: " + escapeHtml(c.author)) : ""}</div>
1212
+ </div>
1213
+ </div>
1214
  </div>
1215
  `;
1216
 
 
1223
  ? `<a class="resource-link" href="${escapeAttr(url)}" target="_blank" rel="noopener noreferrer">Open link</a>`
1224
  : "";
1225
 
1226
+ // Delete published (centered + compact)
1227
  delBtn.addEventListener("click", () => {
1228
+ openDialog({
1229
  title: "Delete published card?",
1230
  subtitle: "This is permanent and affects everyone viewing this Space.",
1231
  bodyHtml: `
1232
+ <div class="checklist" style="margin-bottom:10px;">
1233
+ <div class="checkrow">
1234
+ <input type="checkbox" id="c1"/>
1235
+ <div class="lbl">I understand this is permanent</div>
1236
+ </div>
1237
+ <div class="checkrow">
1238
+ <input type="checkbox" id="c2"/>
1239
+ <div class="lbl">I confirm I want to delete this card</div>
1240
+ </div>
1241
  </div>
1242
  <label>Password</label>
1243
  <input type="password" id="pw" value="" />
1244
  `,
1245
  primaryText: "Delete",
1246
  primaryClass: "btn-danger",
1247
+ canSubmit: (b) => b.querySelector("#c1").checked
1248
+ && b.querySelector("#c2").checked
1249
+ && (b.querySelector("#pw").value || "").trim().length > 0,
1250
+ onSubmit: async (b) => {
1251
+ const password = (b.querySelector("#pw").value || "").trim();
 
 
 
 
1252
  const data = await api(`/api/cards/${encodeURIComponent(c.id)}`, {
1253
  method: "DELETE",
1254
  headers: { "X-Password": password },
 
1259
  renderFilterChips();
1260
  render();
1261
  },
1262
+ onOpen: ({backdrop, refresh}) => {
1263
  const pw = backdrop.querySelector("#pw");
1264
  const c1 = backdrop.querySelector("#c1");
1265
  const c2 = backdrop.querySelector("#c2");
1266
  pw.focus();
1267
+ [pw, c1, c2].forEach(el => el.addEventListener("input", refresh));
1268
+ [c1, c2].forEach(el => el.addEventListener("change", refresh));
1269
  }
1270
  });
1271
  });
1272
 
1273
+ // Edit & Republish (centered, shorter, internal scroll)
1274
  editBtn.addEventListener("click", () => {
1275
  let tagsLocal = tags.slice();
1276
 
1277
+ openDialog({
1278
  title: "Edit & Republish",
1279
  subtitle: "Edits go live only after you enter the password and republish.",
1280
  bodyHtml: `
1281
+ <div style="display:grid; gap:10px;">
1282
  <div>
1283
  <label>Model</label>
1284
  <input id="m" type="text" value="${escapeHtml(c.model || "")}" />
 
1286
 
1287
  <div>
1288
  <label>Tags</label>
1289
+ <div id="tags" style="display:flex; flex-wrap:wrap; gap:8px; align-items:center;"></div>
1290
  <div style="margin-top:8px;">
1291
+ <button class="tag-btn" id="tagBtn" type="button">Tags</button>
1292
  </div>
1293
  </div>
1294
 
1295
  <div>
1296
  <label>How it Works</label>
1297
+ <textarea id="h" style="height:90px;">${escapeHtml(c.how_it_works || "")}</textarea>
1298
  </div>
1299
 
1300
  <div>
1301
  <label>Use Cases</label>
1302
+ <textarea id="u" style="height:90px;">${escapeHtml(c.use_cases || "")}</textarea>
 
 
 
 
 
1303
  </div>
1304
 
1305
+ <div class="footer-row" style="align-items:flex-end;">
1306
+ <div class="link-col">
1307
+ <label>Link</label>
1308
+ <input id="l" type="text" value="${escapeHtml(c.link || "")}" />
1309
+ </div>
1310
+ <div class="by-col" style="padding-bottom:2px;">
1311
+ <span>By:</span>
1312
+ <input id="a" class="by-input" type="text" value="${escapeHtml(c.author || "")}" placeholder="name" />
1313
+ </div>
1314
  </div>
1315
 
1316
  <div>
 
1321
  `,
1322
  primaryText: "Republish",
1323
  primaryClass: "btn-primary",
1324
+ canSubmit: (b) => (b.querySelector("#pw").value || "").trim().length > 0
1325
+ && (b.querySelector("#m").value || "").trim().length > 0
1326
+ && (b.querySelector("#h").value || "").trim().length > 0
1327
+ && (b.querySelector("#u").value || "").trim().length > 0,
1328
+ onSubmit: async (b) => {
1329
+ const password = (b.querySelector("#pw").value || "").trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
1330
  const payload = {
1331
  password,
1332
+ model: b.querySelector("#m").value || "",
1333
  tags: tagsLocal,
1334
+ how_it_works: b.querySelector("#h").value || "",
1335
+ use_cases: b.querySelector("#u").value || "",
1336
+ link: b.querySelector("#l").value || "",
1337
+ author: b.querySelector("#a").value || "",
1338
  };
1339
 
1340
  const data = await api(`/api/cards/${encodeURIComponent(c.id)}`, {
 
1349
  renderFilterChips();
1350
  render();
1351
  },
1352
+ onOpen: ({backdrop, refresh}) => {
1353
+ const tagsWrap = backdrop.querySelector("#tags");
1354
+ const tagBtn = backdrop.querySelector("#tagBtn");
1355
  const pw = backdrop.querySelector("#pw");
1356
 
1357
  function renderEditTags(){
1358
+ tagsWrap.innerHTML = "";
1359
  tagsLocal.forEach(t => {
1360
  const chip = document.createElement("span");
1361
  chip.className = "tag-chip";
 
1365
  tagsLocal = tagsLocal.filter(x => x !== t);
1366
  renderEditTags();
1367
  renderFilterChips();
1368
+ refresh();
1369
  });
1370
+ tagsWrap.appendChild(chip);
1371
  });
1372
  }
1373
 
1374
  tagBtn.addEventListener("click", (e) => {
1375
  e.preventDefault();
1376
+ openTagsDialog({
1377
+ selected: tagsLocal,
1378
+ onApply: (next) => {
1379
+ tagsLocal = next.slice();
1380
+ renderEditTags();
1381
+ renderFilterChips();
1382
+ refresh();
1383
+ }
1384
  });
1385
  });
1386
 
 
1387
  ["m","h","u","l","a","pw"].forEach(id => {
1388
+ backdrop.querySelector("#" + id).addEventListener("input", refresh);
 
1389
  });
1390
 
1391
  pw.focus();
1392
  renderEditTags();
1393
+ refresh();
1394
  }
1395
  });
1396
  });