fuzzy find my records ken.waow.tech
embeddings pds search
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

collapse pack actions into single disclosure menu

the pack-meta line had the state label ("saved") sitting directly next
to a "delete" button and a "share" button, which read as the nonsense
phrase "saved delete" and made it unclear what any of the verbs acted
on. confusing enough that a user asked literally "delete what?".

rework: the state label IS the menu trigger now. one click on
"saved" / "not saved" opens a small popover anchored to the trigger
with:
- a one-sentence description of what the pack is and where it lives
- view pack on PDS (if saved)
- delete saved pack (if saved)
- save pack to my PDS (if not saved)
- share this search (if there's a query to share)

every action closes the menu on click so the user gets immediate
visual confirmation. mobile gets a left-anchored panel so it doesn't
clip off the right edge of the viewport.

shared-view mode swaps the entire disclosure for a static "shared
view" label — there's no auth and nothing to save/delete, so a menu
would be empty.

no backend changes, just frontend. deploy picks it up because assets
are embedded at build time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+143 -31
+12 -4
backend/src/assets/index.html
··· 62 62 63 63 <div id="pack-meta" class="pack-meta hidden"> 64 64 <span id="pack-stats" class="muted"></span> 65 - <span class="sep muted">·</span> 66 - <span id="pack-state" class="muted"></span> 67 - <button id="pack-save-btn" type="button" class="text-btn text-btn-accent hidden">save to PDS</button> 68 - <button id="pack-delete-btn" type="button" class="text-btn text-btn-danger hidden">delete</button> 65 + <span id="pack-meta-sep" class="sep muted">·</span> 66 + <details id="pack-menu" class="pack-menu"> 67 + <summary id="pack-state" class="pack-menu-trigger muted">not saved</summary> 68 + <div class="pack-menu-panel"> 69 + <div id="pack-menu-desc" class="pack-menu-desc muted"></div> 70 + <a id="pack-view-link" class="pack-menu-item text-btn hidden" href="#" target="_blank" rel="noopener">view pack on PDS ↗</a> 71 + <button id="pack-save-btn" type="button" class="pack-menu-item text-btn text-btn-accent hidden">save pack to my PDS</button> 72 + <button id="pack-delete-btn" type="button" class="pack-menu-item text-btn text-btn-danger hidden">delete saved pack</button> 73 + <button id="pack-share-btn" type="button" class="pack-menu-item text-btn text-btn-accent hidden">share this search</button> 74 + </div> 75 + </details> 76 + <span id="pack-shared-label" class="muted hidden">shared view</span> 69 77 </div> 70 78 71 79 <div id="status" class="status hidden"></div>
+66 -26
backend/src/assets/main.js
··· 31 31 const packSaveBtn = $("#pack-save-btn"); 32 32 const packDeleteBtn = $("#pack-delete-btn"); 33 33 const packStateEl = $("#pack-state"); 34 + const packMenuEl = $("#pack-menu"); 35 + const packMenuDescEl = $("#pack-menu-desc"); 36 + const packViewLink = $("#pack-view-link"); 37 + const packShareBtn = $("#pack-share-btn"); 38 + const packSharedLabel = $("#pack-shared-label"); 39 + const packMetaSep = $("#pack-meta-sep"); 34 40 const aboutBtn = $("#about-btn"); 35 41 const aboutModal = $("#about-modal"); 36 42 const aboutOverlay = $("#about-overlay"); ··· 317 323 renderPackActions(j); 318 324 } 319 325 320 - // show the save / delete buttons based on the pack's current persistence 321 - // state. the intent is that writing a record to the user's repo must be a 322 - // deliberate click, not something we do for them. 326 + // drive the single pack-menu disclosure from the /api/status response. 327 + // the state label itself ("saved" / "not saved") is the clickable 328 + // summary — opening it reveals a popover with the actions that are 329 + // relevant to that state (view / delete / save) plus "share this search" 330 + // when there's a query to share. this replaces the older inline 331 + // "saved · delete · share" layout, which had verbs sitting next to state 332 + // words and read like "saved delete" to users. 323 333 function renderPackActions(j) { 324 334 packMetaEl.classList.remove("hidden"); 335 + // leave shared-view mode behind if we were in it 336 + packSharedLabel.classList.add("hidden"); 337 + packMenuEl.classList.remove("hidden"); 338 + packMetaSep.classList.remove("hidden"); 339 + 325 340 if (j.persisted) { 326 - packSaveBtn.classList.add("hidden"); 327 - packDeleteBtn.classList.remove("hidden"); 328 341 packStateEl.textContent = "saved"; 342 + packMenuDescEl.textContent = 343 + "the vector pack for your records is saved on your PDS as a tech.waow.ken.pack record."; 344 + packViewLink.classList.remove("hidden"); 345 + packViewLink.href = `https://pdsls.dev/${j.persisted_uri}`; 346 + packDeleteBtn.classList.remove("hidden"); 347 + packSaveBtn.classList.add("hidden"); 329 348 } else { 330 - packSaveBtn.classList.remove("hidden"); 331 - packDeleteBtn.classList.add("hidden"); 332 349 packStateEl.textContent = "not saved"; 350 + packMenuDescEl.textContent = 351 + "the vector pack lives in ken's memory only. save it to your PDS to make it portable and shareable."; 352 + packViewLink.classList.add("hidden"); 353 + packDeleteBtn.classList.add("hidden"); 354 + packSaveBtn.classList.remove("hidden"); 333 355 } 356 + 357 + // share is about the current search, not the pack — but it's a pack- 358 + // scoped action (shares a query + the pack that answered it), so it 359 + // lives in the same menu. only useful when there's actually a query 360 + // to share; updateShareButtonVisibility handles the gating. 361 + updateShareButtonVisibility(); 362 + 334 363 // point the about-modal's "example pack" link at whatever pack is 335 364 // currently persisted, so people can see a real record on pdsls.dev. 336 365 if (j.persisted_uri && aboutPackLink) { ··· 338 367 } 339 368 } 340 369 370 + // close the popover after any menu action fires, so the user gets 371 + // immediate visual confirmation that their click landed and isn't 372 + // staring at a still-open menu while the underlying state updates. 373 + function closePackMenu() { 374 + packMenuEl.open = false; 375 + } 376 + 341 377 async function pollStatus() { 342 378 const poll = async () => { 343 379 try { ··· 547 583 548 584 packSaveBtn.addEventListener("click", async () => { 549 585 if (!me) return; 586 + closePackMenu(); 550 587 packSaveBtn.disabled = true; 551 588 packSaveBtn.textContent = "saving…"; 552 589 try { ··· 568 605 569 606 packDeleteBtn.addEventListener("click", async () => { 570 607 if (!me) return; 608 + closePackMenu(); 571 609 if (!confirm("delete the saved pack record from your PDS? your in-memory search will still work until you sign out.")) return; 572 610 packDeleteBtn.disabled = true; 573 611 packDeleteBtn.textContent = "deleting…"; ··· 696 734 signedInSection.classList.remove("hidden"); 697 735 signedNav.classList.remove("hidden"); 698 736 sessionHandleEl.textContent = `@${handle}`; 699 - // visual hint: show whose pack we're viewing in the meta line 737 + // visual hint: show whose pack we're viewing in the meta line. the 738 + // disclosure menu is pointless here (no auth, nothing to save/delete) 739 + // so we hide it entirely and just show the shared-view label. 700 740 packMetaEl.classList.remove("hidden"); 701 741 packStatsEl.textContent = `viewing @${handle}`; 702 - packStateEl.textContent = "shared view"; 703 - packSaveBtn.classList.add("hidden"); 704 - packDeleteBtn.classList.add("hidden"); 742 + packMenuEl.classList.add("hidden"); 743 + packMetaSep.classList.add("hidden"); 744 + packSharedLabel.classList.remove("hidden"); 705 745 // hide signout — there's no session to clear 706 746 signoutBtn.classList.add("hidden"); 707 747 ··· 778 818 }); 779 819 } 780 820 781 - // ---------- share button + modal ---------- 782 - 783 - const shareBtn = document.createElement("button"); 784 - shareBtn.type = "button"; 785 - shareBtn.className = "text-btn text-btn-accent"; 786 - shareBtn.textContent = "share"; 787 - shareBtn.id = "share-btn"; 821 + // ---------- share modal ---------- 822 + // 823 + // the share trigger lives inside the pack-menu popover now (#pack-share-btn 824 + // in index.html), alongside save/delete/view-on-pds. the modal below is 825 + // still created dynamically — it's a single piece of markup with no 826 + // dependencies on the trigger's location. 788 827 789 828 const shareOverlay = document.createElement("div"); 790 829 shareOverlay.className = "overlay hidden"; ··· 849 888 setTimeout(() => (shareCopyBtn.textContent = "copy link"), 1500); 850 889 } 851 890 }); 852 - shareBtn.addEventListener("click", () => { 891 + packShareBtn.addEventListener("click", () => { 892 + closePackMenu(); 853 893 // mobile native share if available, otherwise modal 854 894 if (navigator.share && /Mobi|Android|iPhone/i.test(navigator.userAgent)) { 855 895 navigator ··· 863 903 } 864 904 }); 865 905 866 - // inject the share button into the meta line, after the delete button 867 - packMetaEl.appendChild(shareBtn); 868 - 869 - // hide share until there's a query to share 906 + // hide the share action inside the pack-menu popover until there's a 907 + // query to share. the menu trigger itself stays visible — the user can 908 + // still open it for save/delete/view-on-pds — we just don't expose 909 + // "share this search" until a search has been run. 870 910 function updateShareButtonVisibility() { 871 911 if (lastQuery && me && !shareView) { 872 - shareBtn.classList.remove("hidden"); 912 + packShareBtn.classList.remove("hidden"); 873 913 } else { 874 - shareBtn.classList.add("hidden"); 914 + packShareBtn.classList.add("hidden"); 875 915 } 876 916 } 877 - shareBtn.classList.add("hidden"); 917 + packShareBtn.classList.add("hidden"); 878 918 879 919 880 920 // ---------- bootstrap ----------
+65 -1
backend/src/assets/style.css
··· 317 317 } 318 318 .text-btn-danger:hover { color: var(--fg); } 319 319 320 - #share-btn { margin-left: auto; } 320 + /* ------- pack menu (single-disclosure popover) ------- 321 + * 322 + * one clickable trigger (the state label itself — "saved" or "not saved") 323 + * opens a small popover that lists every action you can take on the pack 324 + * (view, save, delete, share). keeps the resting meta line terse and 325 + * avoids the older "saved delete" side-by-side read that looked like a 326 + * nonsense phrase. 327 + */ 328 + .pack-menu { 329 + position: relative; 330 + display: inline-block; 331 + margin-left: auto; 332 + } 333 + .pack-menu summary { 334 + list-style: none; 335 + cursor: pointer; 336 + display: inline-block; 337 + padding: 2px 4px; 338 + border-radius: var(--radius); 339 + } 340 + .pack-menu summary::-webkit-details-marker { display: none; } 341 + .pack-menu summary::after { 342 + content: " ▾"; 343 + font-size: 0.75em; 344 + opacity: 0.6; 345 + margin-left: 2px; 346 + } 347 + .pack-menu[open] summary::after { content: " ▴"; } 348 + .pack-menu summary:hover { color: var(--fg); } 349 + .pack-menu[open] summary { color: var(--fg); } 350 + .pack-menu-panel { 351 + position: absolute; 352 + right: 0; 353 + top: calc(100% + 6px); 354 + min-width: 15rem; 355 + padding: 12px 14px; 356 + background: var(--bg); 357 + border: 1px solid var(--border); 358 + border-radius: var(--radius); 359 + display: flex; 360 + flex-direction: column; 361 + gap: 8px; 362 + z-index: 20; 363 + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); 364 + } 365 + .pack-menu-desc { 366 + font-size: var(--text-small); 367 + line-height: 1.4; 368 + padding-bottom: 8px; 369 + border-bottom: 1px solid var(--border); 370 + } 371 + .pack-menu-item { 372 + text-align: left; 373 + padding: 2px 0; 374 + font-size: var(--text-small); 375 + } 376 + /* on very narrow screens pin the popover to the viewport so it doesn't 377 + * clip off the right edge of the meta line */ 378 + @media (max-width: 480px) { 379 + .pack-menu-panel { 380 + right: auto; 381 + left: 0; 382 + min-width: min(15rem, calc(100vw - 32px)); 383 + } 384 + } 321 385 322 386 #share-modal input[type="text"] { 323 387 width: 100%;