SOURCE / PINNED RELEASE
Made of little things.
Follow Pack Creator
- Release
- 3ef728c7594a…
- Author-recorded commit
- Not provided
- License
- Check the original source
The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" /><title>Follow Pack Creator</title><meta name="description" content="Create and manage Nostr follow packs with your network and Relatr search."><meta name="napplet-id" content="follow-packs"><meta name="napplet-requires" content="identity outbox"><meta name="napplet-optional" content="common cvm keys link resource storage theme">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" crossorigin>function Te(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function a(e){const n=Te()[e];if(!n)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return n}function Me(){const e=new Map;function t(o,s){if(e.has(o))throw new Error(`NAP domain "${o}" is already registered`);e.set(o,s)}function n(o){const s=o.type.indexOf(".");if(s<=0)return!1;const c=o.type.slice(0,s),d=e.get(c);return d?(d(o),!0):!1}function r(){return Array.from(e.keys())}return{registerNap:t,dispatch:n,getRegisteredDomains:r}}var Ue=Me(),qe=Ue.registerNap,_e="theme";function xe(){var t;const e=window;if(!((t=e.napplet)!=null&&t.theme))throw new Error("window.napplet.theme is unavailable -- runtime did not inject this domain");return e.napplet.theme}function Oe(){return xe().get()}function je(e){return xe().onChanged(e)}qe(_e,e=>{});var $e={getItem(e){return a("storage").getItem(e)},setItem(e,t){return a("storage").setItem(e,t)},removeItem(e){return a("storage").removeItem(e)},keys(){return a("storage").keys()},instance:{getItem(e){return a("storage").instance.getItem(e)},setItem(e,t){return a("storage").instance.setItem(e,t)},removeItem(e){return a("storage").instance.removeItem(e)},keys(){return a("storage").instance.keys()}}},Ke={registerAction(e){return a("keys").registerAction(e)},unregisterAction(e){a("keys").unregisterAction(e)},onAction(e,t){return a("keys").onAction(e,t)},async register(e,t){const n=a("keys"),r=await n.registerAction(e),o=n.onAction(e.id,t);return{...r,close(){o.close(),n.unregisterAction(e.id)}}}},_={getPublicKey(){return a("identity").getPublicKey()},onChanged(e){return a("identity").onChanged(e)},getRelays(){return a("identity").getRelays()},getProfile(){return a("identity").getProfile()},getFollows(){return a("identity").getFollows()},getList(e){return a("identity").getList(e)},getZaps(){return a("identity").getZaps()},getMutes(){return a("identity").getMutes()},getBlocked(){return a("identity").getBlocked()},getBadges(){return a("identity").getBadges()}},Be={info(){return a("resource").info()},bytes(e,t){return a("resource").bytes(e,t)},bytesMany(e,t){return a("resource").bytesMany(e,t)},bytesAsObjectURL(e){return a("resource").bytesAsObjectURL(e)}},Fe={discover(e){return a("cvm").discover(e)},request(e,t,n){return a("cvm").request(e,t,n)},listTools(e,t){return a("cvm").listTools(e,t)},callTool(e,t,n,r){return a("cvm").callTool(e,t,n,r)},listResources(e,t){return a("cvm").listResources(e,t)},readResource(e,t,n){return a("cvm").readResource(e,t,n)},close(e){return a("cvm").close(e)},onEvent(e){return a("cvm").onEvent(e)},registry:{list(e){return a("cvm").registry.list(e)},has(e,t){return a("cvm").registry.has(e,t)},describe(e,t){return a("cvm").registry.describe(e,t)},call(e,t,n,r){return a("cvm").registry.call(e,t,n,r)}}},ae={getEvent(e,t){return a("outbox").getEvent(e,t)},query(e,t){return a("outbox").query(e,t)},subscribe(e,t){return a("outbox").subscribe(e,t)},publish(e,t){return a("outbox").publish(e,t)},resolveRelays(e){return a("outbox").resolveRelays(e)}},We={open(e,t){return a("link").open(e,t)}},Ge={encodeNip19(e){return a("common").encodeNip19(e)},decodeNip19(e){return a("common").decodeNip19(e)},getProfile(e){return a("common").getProfile(e)},follows(){return a("common").follows()},follow(...e){return a("common").follow(...e)},unfollow(...e){return a("common").unfollow(...e)},react(e,t,n){return a("common").react(e,t,n)},report(e,t,n){return a("common").report(e,t,n)}};function Je(e,t){return!!(e!=null&&e[t])}function g(e){return Je(window.napplet,e)}const P=39089,O=e=>typeof e=="string"&&/^[0-9a-f]{64}$/i.test(e),ue=e=>`${e.slice(0,8)}…${e.slice(-5)}`,S=e=>[...new Set(e.filter(O).map(t=>t.toLowerCase()))],F=(e,t)=>{var n;return((n=e.tags.find(r=>r[0]===t))==null?void 0:n[1])??""};function Le(e,t){return e.kind!==P||e.pubkey!==t||!F(e,"d")?null:{d:F(e,"d"),title:F(e,"title"),description:F(e,"description"),image:F(e,"image"),members:S(e.tags.filter(n=>n[0]==="p").map(n=>n[1])),event:e}}function ve(e,t){return e.created_at>t.created_at||e.created_at===t.created_at&&e.id<t.id}function Ye(e,t,n){var c;const r=new Map,o=new Map(Object.entries(n)),s=new Set;for(const d of e)if(!(d.pubkey!==t||d.kind!==5))for(const f of d.tags){if(f[0]==="a"&&((c=f[1])!=null&&c.startsWith(`${P}:${t}:`))){const u=f[1].slice(`${P}:${t}:`.length);o.set(u,Math.max(o.get(u)??0,d.created_at))}f[0]==="e"&&f[1]&&s.add(f[1])}for(const d of e){const f=Le(d,t);if(!f)continue;const u=r.get(f.d);(!(u!=null&&u.event)||ve(d,u.event))&&r.set(f.d,f)}return[...r.values()].filter(d=>d.event.created_at>(o.get(d.d)??-1)&&!s.has(d.event.id)).sort((d,f)=>f.event.created_at-d.event.created_at)}function Ve(e,t=Math.floor(Date.now()/1e3)){var s,c,d,f;const n=new Set(["d","title","description","image","p"]),r=(((s=e.event)==null?void 0:s.tags)??[]).filter(u=>!n.has(u[0])).map(u=>[...u]);r.push(["d",e.d],["title",e.title.trim()]),e.description.trim()&&r.push(["description",e.description.trim()]),e.image.trim()&&r.push(["image",e.image.trim()]);const o=new Map((((c=e.event)==null?void 0:c.tags)??[]).filter(u=>u[0]==="p").map(u=>{var v;return[(v=u[1])==null?void 0:v.toLowerCase(),u]}));for(const u of S(e.members))r.push([...o.get(u)??["p",u]]);return{kind:P,created_at:Math.max(t,(((d=e.event)==null?void 0:d.created_at)??0)+1),tags:r,content:((f=e.event)==null?void 0:f.content)??""}}function Ze(e,t,n=Math.floor(Date.now()/1e3)){var o;const r=[["a",`${P}:${t}:${e.d}`],["k",String(P)]];return e.event&&r.push(["e",e.event.id]),{kind:5,tags:r,content:"Deleted follow pack",created_at:Math.max(n,(((o=e.event)==null?void 0:o.created_at)??0)+1)}}function He(e){var n,r;if(e.isError)throw new Error(((n=e.content.find(o=>o.type==="text"))==null?void 0:n.text)??"Relatr could not complete the search.");let t=e.structuredContent;if(!t){const o=(r=e.content.find(s=>s.type==="text"))==null?void 0:r.text;if(o)try{t=JSON.parse(o)}catch{throw new Error("Relatr returned an unreadable search result.")}}if(!t||typeof t!="object"||!("results"in t)||!Array.isArray(t.results))throw new Error("Relatr returned an unexpected search result.");return S(t.results.map(o=>o&&typeof o=="object"?o.pubkey:null))}function ze(e,t){const n={drafts:[],deleted:{}};if(!e)return n;try{const r=JSON.parse(e);if(!r||!Array.isArray(r.drafts))return n;const o=r.drafts.filter(c=>c&&typeof c.d=="string"&&c.d.length>0&&typeof c.title=="string"&&typeof c.description=="string"&&typeof c.image=="string"&&Array.isArray(c.members)&&(!c.event||c.event.pubkey===t&&c.event.kind===P&&Array.isArray(c.event.tags))).map(c=>({...c,members:S(c.members)})),s=Object.fromEntries(Object.entries(r.deleted??{}).filter(([,c])=>typeof c=="number"&&Number.isFinite(c)));return{drafts:o,deleted:s}}catch{return n}}const i=e=>document.getElementById(e),Qe=i("app"),Y=i("pack-title"),ne=i("pack-description"),V=i("pack-image"),N=i("search"),Xe={pubkey:"750682303c9f0ddad75941b49edc9d46e3ed306b9ee3335338a21a3e404c5fa3",relays:["wss://relay.contextvm.org","wss://relay2.contextvm.org","wss://relay.primal.net"]};let m="",l=0,E="",p={drafts:[],deleted:{}},k=[],A=[],fe=new Set;const Z=new Map,X=new Set;let x=!0,C=!1,G=0,j=!1,W=!1,re=0,oe,T=[],U=!1,H="",se=40,ee,me=Promise.resolve();const ce=[],te=new Map,pe=new Set;let he=new AbortController,M,ge=0;function h(e,t=!1,n){i("status").textContent=e,i("status").title=e,i("status").parentElement.dataset.error=String(t),ee=n,i("retry").hidden=!n}function $(e){return e instanceof Error?e.message:String(e)}function le(){const e=new Map(k.map(t=>[t.d,t]));for(const t of p.drafts)e.set(t.d,t);return[...e.values()]}function b(){return le().find(e=>e.d===E)}function I(){return!!m&&!x&&!C}function K(e){Qe.dataset.view=e;for(const t of document.querySelectorAll(".view-tabs button"))t.setAttribute("aria-current",t.dataset.view===e?"page":"false")}function z(e){if(!I())return;const t={d:crypto.randomUUID(),title:e?`${e.title||"Untitled pack"} (copy)`:"",description:(e==null?void 0:e.description)??"",image:(e==null?void 0:e.image)??"",members:[...(e==null?void 0:e.members)??[]]};p.drafts.unshift(t),E=t.d,i("delete-confirm").hidden=!0,Q(),w(),K("design"),Y.focus()}function Q(){if(!m||!g("storage")||!W)return;const e=m,t=l,n=JSON.stringify(p);G++,ie(),me=me.then(()=>$e.setItem(`workspace:${e}`,n)).then(()=>{t===l&&(j&&h("Draft saved privately."),j=!1)}).catch(r=>{t===l&&(j=!0,h(`Draft remains in this session. Could not save: ${$(r)}`,!0,Q))}).finally(()=>{t===l&&(G=Math.max(0,G-1),ie())})}function de(e){const t=b();if(!I()||!t)return;const n={...t,...e},r=p.drafts.findIndex(o=>o.d===t.d);r<0?p.drafts.unshift(n):p.drafts[r]=n,i("delete-confirm").hidden=!0,Q(),Ae(),ie(),"members"in e&&(ye(),R()),Ne()}function et(e){const t=b();if(!I()||!t)return;const n=t.members.includes(e);de({members:n?t.members.filter(r=>r!==e):[...t.members,e]})}function be(e,t){const n=document.createElement("div");n.className="empty";const r=document.createElement("strong");r.textContent=e;const o=document.createElement("span");return o.textContent=t,n.append(r,o),n}function Ae(){const e=le();i("pack-count").textContent=String(e.length),i("pack-list").replaceChildren(),e.length||i("pack-list").append(be(x?"Finding your packs…":m?"A fresh start.":"Connect your account",m?"Create a pack around a topic, a community, or a few good people.":"Sign in through the runtime to load your network and packs."));for(const t of e){const n=document.createElement("button");n.className="pack-item",n.setAttribute("aria-current",String(t.d===E)),n.disabled=C;const r=document.createElement("span");r.className="pack-thumb",r.textContent="◇",r.setAttribute("aria-hidden","true");const o=document.createElement("span");o.className="pack-item-text";const s=document.createElement("span");s.className="pack-item-title truncate",s.textContent=t.title||"Untitled pack";const c=document.createElement("span");c.className="pack-item-meta";const d=p.drafts.some(f=>f.d===t.d);c.textContent=`${t.members.length} ${t.members.length===1?"person":"people"} · ${t.event?d?"Unpublished edits":"Published":"Draft"}`,o.append(s,c),n.append(r,o),n.addEventListener("click",()=>{E=t.d,i("delete-confirm").hidden=!0,w(),K("design"),we(t.members,l)}),i("pack-list").append(n)}}function ie(){const e=b(),t=I()&&!!e,n=p.drafts.some(r=>r.d===E);for(const r of[Y,ne,V])r.disabled=!t;for(const r of["duplicate","delete-pack","find-people"])i(r).disabled=!t;i("new-pack").disabled=!I(),i("refresh").disabled=x||C||!m,i("confirm-delete").disabled=!t,i("publish").disabled=!t||!(e!=null&&e.title.trim())||!e.members.length||!!e.event&&!n,i("publish").textContent=C?"Working…":e!=null&&e.event?"Publish changes ↗":"Publish pack ↗",i("publish-hint").textContent=m?e!=null&&e.title.trim()?e.members.length?e.event&&!n?"All changes published":"Public pack · ready when you are":"Add at least one person":"Give your pack a name":"Sign in through the runtime",i("save-state").textContent=C?"Waiting for the runtime…":x?"Loading…":m?e!=null&&e.event&&!n?"Published":G?"Saving draft…":j||!g("storage")?"Draft · this session":e!=null&&e.event?"Edits saved privately":"Draft saved privately":"Sign in to get started",i("open-pack").hidden=!(e!=null&&e.event)||!g("link"),i("open-pack").disabled=C}function w(){Ae();const e=b();Y.value=(e==null?void 0:e.title)??"",ne.value=(e==null?void 0:e.description)??"",V.value=(e==null?void 0:e.image)??"",ie(),Ne(),ye(),R()}async function Pe(e){if(!g("resource")||!e||!/^(https:|data:image\/|blossom:|htree:|nostr:)/.test(e))return null;if(!te.has(e)){const t=he.signal;te.set(e,Be.bytes(e,{signal:t}).then(n=>{if(t.aborted||!n.type.startsWith("image/"))return null;const r=URL.createObjectURL(n);return pe.add(r),r}).catch(()=>null))}return te.get(e)}function Se(){he.abort(),he=new AbortController,M==null||M.disconnect();for(const e of pe)URL.revokeObjectURL(e);pe.clear(),te.clear()}function tt(e,t){e.dataset.image=t,M.observe(e)}M=new IntersectionObserver(e=>{for(const t of e){if(!t.isIntersecting)continue;const n=t.target;M.unobserve(n);const r=l;Pe(n.dataset.image??"").then(o=>{if(!o||r!==l||!n.isConnected)return;const s=document.createElement("img");s.alt="",s.src=o,s.addEventListener("error",()=>s.remove()),n.append(s)})}});function Ne(){var r;const e=b(),t=++ge;i("cover-title").textContent=(e==null?void 0:e.title)||"Untitled pack",i("cover-count").textContent=`${(e==null?void 0:e.members.length)??0} ${(e==null?void 0:e.members.length)===1?"person":"people"}`,(r=i("cover").querySelector(".cover-image"))==null||r.remove();const n=e==null?void 0:e.image.trim();i("image-hint").textContent=g("resource")?"Paste an image URL, or keep the gradient.":"Image previews are unavailable in this runtime.",n&&Pe(n).then(o=>{if(t!==ge)return;if(!o){i("image-hint").textContent="Preview unavailable. The image URL is still saved with your pack.";return}const s=document.createElement("img");s.className="cover-image",s.alt="",s.src=o,s.addEventListener("error",()=>{s.remove(),i("image-hint").textContent="Could not display this image."}),i("cover").prepend(s)})}function Ie(e,t){var B;const n=Z.get(e),r=(n==null?void 0:n.name)||ue(e),o=((B=b())==null?void 0:B.members.includes(e))??!1,s=document.createElement("div");s.className="person",s.dataset.pubkey=e;const c=document.createElement("span");c.className="avatar",c.setAttribute("aria-hidden","true"),c.textContent=n!=null&&n.name?n.name.split(/\s+/).slice(0,2).map(L=>[...L][0]).join("").toUpperCase():e.slice(0,2).toUpperCase();const d=document.createElement("div");d.className="person-info";const f=document.createElement("span");f.className="person-name truncate",f.textContent=r;const u=document.createElement("span");u.className="person-detail truncate",u.textContent=(n==null?void 0:n.nip05)||(n==null?void 0:n.about)||(fe.has(e)?"In your network":ue(e)),d.title=`${r}
${(n==null?void 0:n.about)??""}
${e}`,d.append(f,u);const v=document.createElement("button");return v.className="person-button",v.textContent=t?"×":o?"✓":"+",v.setAttribute("aria-label",`${t||o?"Remove":"Add"} ${r}${t||o?" from":" to"} pack`),t||v.setAttribute("aria-pressed",String(o)),v.title=v.getAttribute("aria-label"),v.disabled=!I()||!b(),v.addEventListener("click",()=>et(e)),s.append(c,d,v),n!=null&&n.picture&&g("resource")&&tt(c,n.picture),s}function Re(e){for(const t of e.querySelectorAll(".avatar"))M.unobserve(t)}function ye(){var n;const e=((n=b())==null?void 0:n.members)??[];i("member-count").textContent=String(e.length);const t=i("members");Re(t),t.replaceChildren(),e.length||t.append(be("Good people make a great pack.","Pick people from your network, or search for someone new."));for(const r of e)t.append(Ie(r,!0))}function nt(e){const t=e.toLowerCase();return A.filter(n=>{const r=Z.get(n);return!t||`${n} ${(r==null?void 0:r.name)??""} ${(r==null?void 0:r.nip05)??""} ${(r==null?void 0:r.about)??""}`.toLowerCase().includes(t)})}function R(){const e=N.value.trim(),t=nt(e),n=S([...t,...T]),r=i("people-list");Re(r),r.replaceChildren(),i("clear-search").hidden=!e,i("network-count").textContent=A.length?`${A.length} following`:"",i("search-label").textContent=e?`${n.length} ${n.length===1?"person":"people"} found`:"Your network",i("search-source").textContent=e?g("cvm")?"Network + Relatr":"Your follows":"People you follow",i("search-status").textContent=m?x?"Loading your network…":U?"Searching Relatr…":H:"Sign in through the runtime to see your network.",!n.length&&!x&&!U&&r.append(be(e?"No people found.":"Your circle starts here.",e?"Try another name or paste a public key.":"Search for someone to add to your pack."));for(const o of n.slice(0,se))r.append(Ie(o,!1));i("more-people").hidden=n.length<=se}function rt(e){if(e.kind!==0||!O(e.pubkey))return;const t=Z.get(e.pubkey);if(!(t!=null&&t.event&&!ve(e,t.event)))try{const n=JSON.parse(e.content);if(!n||typeof n!="object")return;const r=o=>typeof o=="string"?o:"";Z.set(e.pubkey,{name:r(n.display_name)||r(n.displayName)||r(n.name),about:r(n.about),nip05:r(n.nip05),picture:r(n.picture),event:e})}catch{}}async function we(e,t){const n=S(e).filter(r=>!X.has(r));n.forEach(r=>X.add(r));for(let r=0;r<n.length;r+=80){if(t!==l)return;const o=n.slice(r,r+80);try{const s=await ae.query([{kinds:[0],authors:o,limit:o.length}],{authors:o,timeoutMs:6e3});if(t!==l)return;for(const c of s.events)o.includes(c.event.pubkey)&&rt(c.event);ye(),R()}catch{t===l&&o.forEach(s=>X.delete(s))}}}function ke(){clearTimeout(oe),re++,T=[],H="",U=!1,se=40,R();const e=N.value.trim();if(!e||!m)return;U=!0,R();const t=re,n=l;oe=setTimeout(()=>{ot(e,t,n)},320)}async function ot(e,t,n){const r=()=>t===re&&n===l;try{let o=e.replace(/^nostr:/,"");if(!O(o)&&/^(npub1|nprofile1)/.test(o)&&g("common")){const s=await Ge.decodeNip19(o);if(!r())return;if(!s.ok||!["npub","nprofile"].includes(s.nip19Type??""))throw new Error("This is not a valid profile public key.");o=s.pubkey||s.hex||""}if(O(o))T=[o.toLowerCase()];else if(g("cvm")){const s=await Fe.callTool(Xe,"search_profiles",{query:e,limit:30,extendToNostr:!0},{timeoutMs:3e4,payment:"deny"});if(!r())return;T=He(s)}else H="Global search unavailable here. Search your follows or paste a hex public key.";if(!r())return;U=!1,R(),g("cvm")&&!O(o)&&h(`Search complete · ${T.length} Relatr results`),we(T,n)}catch(o){if(!r())return;U=!1,H=`Search failed: ${$(o)}`,R(),h("Relatr search failed. Your network is still available.",!0,ke)}}async function J(e,t=!1){var u,v,B;const n=++l;if(++re,clearTimeout(oe),Se(),m=O(e)?e.toLowerCase():"",x=!0,C=!1,G=0,j=!1,t||(p={drafts:[],deleted:{}},k=[],E="",W=!1),A=[],fe.clear(),Z.clear(),X.clear(),N.value="",T=[],U=!1,H="",i("delete-confirm").hidden=!0,i("account").textContent=m?`Connected · ${ue(m)}`:"No account connected",w(),!m){x=!1,w(),h("Sign in through the runtime to create and manage packs.");return}h("Loading your packs and network…");const r=m,o=[];if((!t||!W)&&g("storage"))try{if(await me,n!==l)return;const L=await $e.getItem(`workspace:${r}`);if(n!==l)return;const y=ze(L,r),q=new Map(y.drafts.map(D=>[D.d,D]));for(const D of p.drafts)q.set(D.d,D);p={drafts:[...q.values()],deleted:{...y.deleted,...p.deleted}},W=!0}catch{n===l&&(W=!1,j=!0,o.push("Saved drafts could not be restored; new edits stay in this session"))}if(n!==l)return;const s=await Promise.allSettled([ae.query([{kinds:[P],authors:[r],limit:500},{kinds:[5],authors:[r],limit:500}],{authors:[r],timeoutMs:9e3}),_.getFollows()]);if(n!==l)return;const c=s[0];if(c.status==="fulfilled"){const L=Ye([...c.value.events.map(y=>y.event),...k.flatMap(y=>y.event?[y.event]:[])],r,p.deleted);k=L;for(const y of p.drafts){const q=(u=k.find(D=>D.d===y.d))==null?void 0:u.event;q&&y.event&&ve(q,y.event)&&(y.event=q)}L.length>=500&&o.push("Showing up to 500 published packs")}else o.push(`Packs could not be loaded: ${$(c.reason)}`);const d=s[1];d.status==="fulfilled"?A=S(d.value):o.push(`Network could not be loaded: ${$(d.reason)}`),fe=new Set(A),x=!1;const f=le();f.some(L=>L.d===E)||(E=((v=f[0])==null?void 0:v.d)??""),E||z(),w(),o.length?h(o.join(". "),!0,()=>{J(m,!0)}):h(`${A.length} people in your network · ${k.length} published ${k.length===1?"pack":"packs"}`),we(S([...((B=b())==null?void 0:B.members)??[],...A]),n)}async function Ee(){const e=b();if(!I()||!e||!e.title.trim()||!e.members.length)return;if(e.image.trim()&&!/^https:\/\//i.test(e.image.trim())){h("Use an https:// URL for your public cover image.",!0),V.focus();return}const t=l,n=m;C=!0,w(),h("Approve publishing in the runtime…");try{if(await _.getPublicKey()!==n||l!==t)throw new Error("Account changed. Try again with the current account.");const r=await ae.publish(Ve(e));if(t!==l)return;if(!r.ok||!r.event)throw new Error(r.error||"No relay acknowledged this pack.");const o=Le(r.event,n);if(!o)throw new Error("The runtime returned a pack for a different account.");k=[o,...k.filter(s=>s.d!==o.d)],p.drafts=p.drafts.filter(s=>s.d!==o.d),Q(),h(`“${o.title}” published.`)}catch(r){t===l&&h(`Publish failed: ${$(r)}`,!0,()=>{Ee()})}finally{t===l&&(C=!1,w())}}async function De(){var r;const e=b();if(!I()||!e)return;const t=l,n=m;C=!0,w();try{if(e.event){if(h("Approve deletion in the runtime…"),await _.getPublicKey()!==n||l!==t)throw new Error("Account changed.");const o=Ze(e,n),s=await ae.publish(o);if(t!==l)return;if(!s.ok||!s.event||s.event.pubkey!==n)throw new Error(s.error||"No relay acknowledged the deletion.");p.deleted[e.d]=o.created_at}p.drafts=p.drafts.filter(o=>o.d!==e.d),k=k.filter(o=>o.d!==e.d),E=((r=le()[0])==null?void 0:r.d)??"",Q(),i("delete-confirm").hidden=!0,h(e.event?"Deletion requested. Other clients and relays may retain copies.":"Draft deleted.")}catch(o){t===l&&h(`Delete failed: ${$(o)}`,!0,()=>{De()})}finally{t===l&&(C=!1,E||z(),w())}}async function st(){const e=b(),t=l;if(!(!(e!=null&&e.event)||!g("link")))try{const n=await We.open(`https://following.space/d/${encodeURIComponent(e.d)}?p=${m}`);if(t!==l)return;if(n.status!=="opened")throw new Error("The runtime declined to open the link.")}catch(n){t===l&&h($(n),!0)}}function Ce(e){var s,c;const t=document.documentElement;t.style.setProperty("--bg",e.colors.background),t.style.setProperty("--fg",e.colors.text),t.style.setProperty("--primary",e.colors.primary);const n=(s=e.colors.background.match(/^#([\da-f]{6})$/i))==null?void 0:s[1],r=n?parseInt(n.slice(0,2),16)*.299+parseInt(n.slice(2,4),16)*.587+parseInt(n.slice(4,6),16)*.114:255;t.style.colorScheme=r<128?"dark":"light";const o=(c=e.colors.primary.match(/^#([\da-f]{6})$/i))==null?void 0:c[1];if(o){const d=parseInt(o.slice(0,2),16)*.299+parseInt(o.slice(2,4),16)*.587+parseInt(o.slice(4,6),16)*.114;t.style.setProperty("--on-primary",d>155?"#14241f":"#ffffff")}}for(const e of document.querySelectorAll(".view-tabs button"))e.addEventListener("click",()=>K(e.dataset.view));i("new-pack").addEventListener("click",()=>z());i("duplicate").addEventListener("click",()=>{const e=b();e&&z(e)});i("refresh").addEventListener("click",()=>{J(m,!0)});i("find-people").addEventListener("click",()=>{K("people"),N.focus()});Y.addEventListener("input",()=>de({title:Y.value}));ne.addEventListener("input",()=>de({description:ne.value}));V.addEventListener("input",()=>de({image:V.value}));N.addEventListener("input",ke);i("clear-search").addEventListener("click",()=>{N.value="",ke(),N.focus()});i("more-people").addEventListener("click",()=>{se+=40,R()});i("publish").addEventListener("click",()=>{Ee()});i("open-pack").addEventListener("click",()=>{st()});i("delete-pack").addEventListener("click",()=>{const e=b();e&&(i("delete-question").textContent=e.event?"Delete this public pack?":"Delete this private draft?",i("delete-confirm").hidden=!1)});i("cancel-delete").addEventListener("click",()=>{i("delete-confirm").hidden=!0});i("confirm-delete").addEventListener("click",()=>{De()});i("retry").addEventListener("click",()=>ee==null?void 0:ee());K("design");w();if(g("theme")){Oe().then(Ce).catch(()=>{});const e=je(Ce);ce.push(()=>e.close())}if(g("keys"))for(const[e,t,n,r]of[["pack.publish","Publish follow pack","Ctrl+Enter",()=>{Ee()}],["pack.new","Create follow pack",void 0,()=>z()],["pack.search","Find people","Ctrl+K",()=>{K("people"),N.focus()}]])Ke.register({id:e,label:t,defaultKey:n},r).then(o=>ce.push(()=>o.close())).catch(()=>{});if(g("identity")&&g("outbox")){const e=_.onChanged(n=>{J(n)});ce.push(()=>e.close());const t=l;_.getPublicKey().then(n=>{if(l===t)return J(n)}).catch(n=>{x=!1,w(),h(`Could not connect: ${$(n)}`,!0,()=>{_.getPublicKey().then(r=>J(r)).catch(r=>h($(r),!0))})})}else x=!1,w(),h("Open this napplet in a runtime with account and publishing support.");window.addEventListener("pagehide",()=>{++l,++ge,clearTimeout(oe),Se();for(const e of ce)e()});
</script>
<style>:root{font:13px/1.45 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;--bg: #f6f7f3;--fg: #202d29;--primary: #23775c;--surface: color-mix(in srgb, var(--bg) 96%, var(--fg));--hover: color-mix(in srgb, var(--bg) 92%, var(--fg));--border: color-mix(in srgb, var(--fg) 13%, transparent);--muted: color-mix(in srgb, var(--fg) 65%, var(--bg));--accent-soft: color-mix(in srgb, var(--primary) 10%, var(--bg));--danger: color-mix(in srgb, #de5959 85%, var(--fg));color-scheme:light}*{box-sizing:border-box}html,body,#app{height:100%;margin:0;background:var(--bg);color:var(--fg)}body{overflow:hidden;-webkit-user-select:none;user-select:none}[data-napplet-select=text],input,textarea{-webkit-user-select:text;user-select:text}button,input,textarea{font:inherit}button{cursor:pointer;color:inherit;background:transparent;border:1px solid var(--border);border-radius:6px;padding:4px 10px;min-height:30px;display:inline-flex;align-items:center;justify-content:center;gap:6px;line-height:1.3}button:hover{background:var(--hover)}button:disabled{opacity:.45;cursor:default}button:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible{outline:2px solid var(--primary);outline-offset:2px}.primary{background:var(--primary);border-color:transparent;color:var(--on-primary, #fff);font-weight:600}.primary:hover{background:color-mix(in srgb,var(--primary) 87%,var(--fg))}.icon-button{border-color:transparent;padding:3px;width:30px;flex-shrink:0;font-size:18px}.text-button{border-color:transparent;color:var(--primary);padding:3px 0;min-height:26px}.danger{color:var(--danger);border-color:var(--danger)}.small{font-size:12px;padding:4px 8px}[hidden]{display:none!important}strong{font-weight:650}.muted{color:var(--muted);font-weight:400}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}#app{display:grid;grid-template-rows:38px minmax(0,1fr) auto;grid-template-columns:minmax(0,1fr);container-type:inline-size}.view-tabs{display:flex;gap:4px;padding:4px 8px;border-bottom:1px solid var(--border)}.view-tabs button{flex:1;border:0;min-height:28px;font-size:12px}.view-tabs button[aria-current=page]{color:var(--primary);background:var(--accent-soft);font-weight:650}.pane{min-width:0;min-height:0;display:none;overflow:hidden}#app[data-view=library] .library,#app[data-view=design] .design,#app[data-view=people] .people{display:flex}.library,.design,.people{flex-direction:column}.pane-toolbar{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:8px 12px;min-height:48px;flex-shrink:0;border-bottom:1px solid var(--border)}.pane-toolbar>strong{white-space:nowrap}.actions{display:flex;align-items:center;gap:4px;flex-shrink:0}.count{display:inline-flex;font-size:11px;font-weight:500;justify-content:center;align-items:center;border-radius:4px;padding:0 5px;min-width:19px;margin-left:3px;background:var(--hover);color:var(--muted)}.scroll{overflow:auto;overscroll-behavior:contain;min-height:0;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.library{background:var(--surface)}.pack-list{flex:1;padding:8px}.pack-item{display:flex;align-items:center;justify-content:flex-start;gap:10px;text-align:left;width:100%;padding:10px 8px;border:1px solid transparent;margin-bottom:4px}.pack-item[aria-current=true]{background:var(--bg);border-color:var(--border);box-shadow:0 1px 2px #00000005}.pack-thumb{width:35px;height:39px;border-radius:5px;background:linear-gradient(145deg,color-mix(in srgb,var(--primary) 50%,var(--bg)),color-mix(in srgb,var(--primary) 17%,var(--bg)));display:grid;place-items:center;flex-shrink:0;color:var(--primary);font-size:17px}.pack-item-text{min-width:0;flex:1}.pack-item-title{display:block;font-size:13px;font-weight:600}.pack-item-meta{font-size:11px;color:var(--muted);margin-top:3px;display:flex;align-items:center;gap:5px}.draft-dot{width:5px;height:5px;border-radius:50%;background:var(--primary);display:inline-block}.account{padding:10px 12px;font-size:11px;border-top:1px solid var(--border);color:var(--muted);flex-shrink:0}.save-state{font-size:12px;color:var(--muted);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.save-state:before{content:"";display:inline-block;width:6px;height:6px;background:var(--primary);border-radius:50%;margin-right:7px}.editor-scroll{flex:1}.cover{margin:12px 12px 0;min-height:136px;border-radius:7px;overflow:hidden;position:relative;isolation:isolate;background:linear-gradient(110deg,#143f35,#3a7860);color:#f4f5d9}.cover-image{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;object-fit:cover;z-index:-1}.cover:has(.cover-image):after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,#102e32ce,#102e3230);z-index:-1}.cover-content{position:relative;padding:20px}.cover-caption{text-transform:uppercase;letter-spacing:.12em;font-size:8px;opacity:.75}.cover-title{font-size:clamp(19px,2.3vw,32px);font-weight:570;line-height:1.1;letter-spacing:-.04em;margin:9px 0;overflow-wrap:anywhere;max-width:90%}.cover-count{font-size:10px;opacity:.8}.cover-orbit{position:absolute;border:1px solid #ecf9bc35;border-radius:50%;width:200px;height:200px;z-index:-1;right:-55px;top:-70px}.orbit-two{width:260px;height:260px;top:-30px;right:-145px;border-width:30px;opacity:.4}.fields{padding:16px 12px 12px;display:grid;gap:12px}.field{display:grid;gap:5px;min-width:0;font-size:11px;font-weight:600}input,textarea{width:100%;min-width:0;border:1px solid var(--border);border-radius:5px;background:var(--bg);color:var(--fg);padding:7px 9px;font-size:12px;outline:none}input::placeholder,textarea::placeholder{color:var(--muted);opacity:.8}textarea{resize:vertical;min-height:56px;max-height:200px;line-height:1.5}details{font-size:11px;color:var(--muted)}summary{cursor:pointer;width:fit-content}details .field{margin-top:6px}small{font-size:10px}.section-label{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-top:1px solid var(--border);font-size:12px}.member-list{padding:0 8px 8px}.person{display:flex;align-items:center;gap:10px;padding:9px 8px;border-radius:6px;min-width:0}.person:hover{background:var(--surface)}.avatar{position:relative;width:34px;height:34px;flex-shrink:0;display:grid;place-items:center;border-radius:50%;background:color-mix(in srgb,var(--primary) 12%,var(--bg));color:var(--primary);font-size:11px;font-weight:650;overflow:hidden}.avatar img{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;object-fit:cover}.person-info{flex:1;min-width:0}.person-name{font-weight:600;display:block;font-size:12px}.person-detail{display:block;font-size:10px;color:var(--muted)}.person-button{width:27px;min-height:27px;padding:0;border-radius:50%;flex-shrink:0;font-size:16px}.person-button[aria-pressed=true]{color:var(--primary);background:var(--accent-soft);border-color:transparent}.empty{padding:20px 12px;color:var(--muted);font-size:12px;line-height:1.6}.empty strong{display:block;color:var(--fg);font-size:13px;margin-bottom:4px}.empty .text-button{margin-top:5px}.publish-bar{padding:10px 12px;display:flex;gap:8px;align-items:center;justify-content:space-between;border-top:1px solid var(--border);flex-shrink:0}.publish-bar>span{font-size:10px;line-height:1.4;max-width:50%}.publish-bar .primary{white-space:nowrap}.delete-confirm{padding:8px 12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between;border-top:1px solid var(--border);background:var(--surface);font-size:12px}.search-area{padding:12px 12px 0;flex-shrink:0}.search-box{display:flex;gap:4px;align-items:center;border:1px solid var(--border);border-radius:6px;background:var(--bg);padding:0 7px}.search-box:focus-within{border-color:var(--primary);box-shadow:0 0 0 2px var(--accent-soft)}.search-box>span{font-size:23px;color:var(--muted)}.search-box input{border:0;padding:8px 3px;background:transparent;font-size:12px}.search-box input:focus-visible{outline:none}.search-box input::-webkit-search-cancel-button{display:none}.search-box button{width:20px;min-height:24px}.search-meta{display:flex;flex-wrap:wrap;gap:4px;justify-content:space-between;padding:14px 0 8px;font-size:10px}#search-label{font-weight:650}.people-scroll{padding:0 4px 8px;flex:1}.people-list{padding:0 4px}.inline-state{padding:0 8px;color:var(--muted);font-size:11px}.inline-state:not(:empty){padding:8px}.more{display:flex;width:calc(100% - 16px);margin:8px;color:var(--primary);font-size:11px}.status-bar{grid-column:1 / -1;display:flex;align-items:center;gap:8px;padding:4px 10px;border-top:1px solid var(--border);min-width:0;min-height:26px;font-size:10px;color:var(--muted)}.status-bar output{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.status-bar[data-error=true]{color:var(--danger)}.status-bar button{font-size:10px;min-height:18px;padding:0}@media(min-width:700px){#app{grid-template-columns:clamp(180px,23vw,260px) minmax(0,1fr);grid-template-rows:38px minmax(0,1fr) auto}.view-tabs{grid-column:2}.view-tabs button[data-view=library]{display:none}#app .library{display:flex;grid-column:1;grid-row:1 / 3;border-right:1px solid var(--border)}.design,.people{grid-column:2;grid-row:2}#app[data-view=library] .design{display:flex}}@media(min-width:1100px){#app{grid-template-columns:clamp(205px,18vw,300px) minmax(0,1fr) minmax(0,1.05fr);grid-template-rows:minmax(0,1fr) auto}.view-tabs{display:none}#app .library{grid-row:1}#app .design{display:flex;grid-column:2;grid-row:1;border-right:1px solid var(--border)}#app .people{display:flex;grid-column:3;grid-row:1}.cover{min-height:170px}.cover-content{padding:24px}}@media(min-width:1600px){.people-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-content:start;gap:2px 12px}.person{padding:12px 8px}.cover{min-height:215px}.cover-title{font-size:38px}.fields{grid-template-columns:1fr;gap:16px}}@media(max-width:400px){.publish-bar>span{display:none}.publish-bar .primary{width:100%}.cover{min-height:110px}.cover-content{padding:14px}.cover-title{font-size:23px}}@media(max-width:239px){.view-tabs{padding:3px 4px}.view-tabs button{padding:3px;gap:3px;font-size:10px}.pane-toolbar{padding:4px 8px;min-height:36px}.pane-toolbar strong{font-size:11px}.actions{gap:0}.icon-button{width:25px}.cover{margin:8px 8px 0}.cover-caption{display:none}.fields{padding:10px 8px}.section-label{padding:4px 8px;font-size:11px}.section-label .text-button{font-size:10px}.publish-bar{padding:5px 8px}.search-area{padding:8px 8px 0}.person{padding:7px 4px;gap:6px}.avatar{width:28px;height:28px}#network-count{display:none}}@media(max-height:300px){#app{grid-template-rows:32px minmax(0,1fr) 22px}.view-tabs{padding-top:1px;padding-bottom:1px}.pane-toolbar{min-height:34px;padding-top:2px;padding-bottom:2px}.cover{display:none}.publish-bar{padding-top:4px;padding-bottom:4px}.status-bar{min-height:22px;padding-top:1px;padding-bottom:1px}.search-area{padding-top:5px}.search-meta{padding-top:5px;padding-bottom:4px}.account{display:none}}@media(min-width:1100px)and (max-height:300px){#app{grid-template-rows:minmax(0,1fr) 22px}}@media(prefers-reduced-motion:no-preference){button{transition:background .12s ease,border-color .12s ease}}
</style>
</head>
<body>
<main id="app" data-view="design">
<nav class="view-tabs" aria-label="Workspace">
<button data-view="library" aria-label="My packs">▦ <span>Packs</span></button>
<button data-view="design" aria-label="Design pack">◇ <span>Design</span></button>
<button data-view="people" aria-label="Find people">⌕ <span>People</span></button>
</nav>
<aside class="library pane" aria-label="My packs">
<div class="pane-toolbar"><strong>My packs <span id="pack-count" class="count">0</span></strong><div class="actions"><button id="refresh" class="icon-button" title="Refresh packs and network" aria-label="Refresh packs and network">↻</button><button id="new-pack" class="primary small" title="Create a pack">+ New</button></div></div>
<div id="pack-list" class="pack-list scroll"></div>
<div id="account" class="account truncate">Connecting…</div>
</aside>
<section class="design pane" aria-label="Pack editor">
<div class="pane-toolbar"><span id="save-state" class="save-state">New draft</span><div class="actions"><button id="open-pack" class="icon-button" title="Open on following.space" aria-label="Open on following.space" hidden>↗</button><button id="duplicate" class="icon-button" title="Duplicate pack" aria-label="Duplicate pack">⧉</button><button id="delete-pack" class="icon-button" title="Delete pack" aria-label="Delete pack">⌫</button></div></div>
<div id="editor-scroll" class="editor-scroll scroll">
<div id="cover" class="cover"><div class="cover-orbit orbit-one"></div><div class="cover-orbit orbit-two"></div><div class="cover-content"><span class="cover-caption">Pack preview</span><div id="cover-title" class="cover-title">Untitled pack</div><div id="cover-count" class="cover-count">0 people</div></div></div>
<div class="fields">
<label class="field"><span>Pack name</span><input id="pack-title" maxlength="120" placeholder="e.g. People building a better internet" autocomplete="off" /></label>
<label class="field"><span>Description <span class="muted">· optional</span></span><textarea id="pack-description" maxlength="2000" rows="2" placeholder="What brings these people together?"></textarea></label>
<details id="cover-settings"><summary>Cover image <span class="muted">· optional</span></summary><label class="field"><span class="sr-only">Cover image URL</span><input id="pack-image" type="url" placeholder="https://…" autocomplete="off" /><small id="image-hint" class="muted">Paste an image URL, or keep the gradient.</small></label></details>
</div>
<div class="section-label"><strong>In this pack <span id="member-count" class="count">0</span></strong><button id="find-people" class="text-button">+ Add people</button></div>
<div id="members" class="member-list"></div>
</div>
<div id="delete-confirm" class="delete-confirm" hidden><span id="delete-question">Delete this pack?</span><div class="actions"><button id="cancel-delete">Cancel</button><button id="confirm-delete" class="danger">Delete</button></div></div>
<div class="publish-bar"><span id="publish-hint" class="muted">Public pack · only publishes when you’re ready</span><button id="publish" class="primary">Publish pack <span aria-hidden="true">↗</span></button></div>
</section>
<section class="people pane" aria-label="Find people">
<div class="pane-toolbar"><strong>Find your people</strong><span id="network-count" class="muted"></span></div>
<div class="search-area"><label class="search-box"><span aria-hidden="true">⌕</span><input id="search" type="search" maxlength="100" placeholder="Search names, npubs, or public keys" aria-label="Search people" autocomplete="off" /><button id="clear-search" class="icon-button" title="Clear search" aria-label="Clear search" hidden>×</button></label><div class="search-meta"><span id="search-label">Your network</span><span id="search-source" class="muted">People you follow</span></div></div>
<div class="people-scroll scroll"><div id="search-status" class="inline-state" role="status"></div><div id="people-list" class="people-list"></div><button id="more-people" class="more" hidden>Show more people</button></div>
</section>
<div class="status-bar"><output id="status" aria-live="polite">Connecting to your account…</output><button id="retry" class="text-button" hidden>Retry</button></div>
</main>
</body>
</html>
