@decentnetwork/beagle 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.19";
1
+ window.__DK_UI_VERSION="0.1.20";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
@@ -2800,7 +2800,97 @@ function DkEditModal({ T, me, onClose, onSave }) {
2800
2800
  };
2801
2801
  return /* @__PURE__ */ React.createElement("div", { onClick: onClose, style: { position: "fixed", inset: 0, zIndex: 90, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 440, maxWidth: "92vw", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 22, display: "flex", flexDirection: "column", gap: 14 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, T.editProfile), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), /* @__PURE__ */ React.createElement("label", { style: { display: "flex", flexDirection: "column", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "display name"), /* @__PURE__ */ React.createElement("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: onKey, autoFocus: true, maxLength: 48, style: field })), /* @__PURE__ */ React.createElement("label", { style: { display: "flex", flexDirection: "column", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "status message"), /* @__PURE__ */ React.createElement("input", { value: desc, onChange: (e) => setDesc(e.target.value), onKeyDown: onKey, maxLength: 120, placeholder: "optional \u2014 a short bio friends will see", style: field })), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, "Your userid (the unique identity) can't change \u2014 only the display name + status."), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: onClose }, T.cancel || "cancel"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", onClick: save }, T.save || "save"))));
2802
2802
  }
2803
- function DkEnsCard({ T, me }) {
2803
+ const dkPunkCache = /* @__PURE__ */ new Map();
2804
+ function DkPunkAvatar({ id, size, radius, fallbackSeed }) {
2805
+ const [img, setImg] = React.useState(() => (dkPunkCache.get(id) || {}).image || null);
2806
+ React.useEffect(() => {
2807
+ let dead = false;
2808
+ const hit = dkPunkCache.get(id);
2809
+ if (hit) {
2810
+ setImg(hit.image || null);
2811
+ return;
2812
+ }
2813
+ setImg(null);
2814
+ fetch(`/api/punk/${id}`).then((r) => r.json()).then((d) => {
2815
+ if (d.ok && d.punk && d.punk.image) {
2816
+ dkPunkCache.set(id, d.punk);
2817
+ if (!dead)
2818
+ setImg(d.punk.image);
2819
+ }
2820
+ }).catch(() => {
2821
+ });
2822
+ return () => {
2823
+ dead = true;
2824
+ };
2825
+ }, [id]);
2826
+ if (!img)
2827
+ return /* @__PURE__ */ React.createElement(DkIdenticon, { seed: fallbackSeed || String(id), size, radius });
2828
+ return /* @__PURE__ */ React.createElement(
2829
+ "img",
2830
+ {
2831
+ src: img,
2832
+ alt: "",
2833
+ width: size,
2834
+ height: size,
2835
+ style: { width: size, height: size, borderRadius: radius, background: "#638596", imageRendering: "pixelated", flexShrink: 0 }
2836
+ }
2837
+ );
2838
+ }
2839
+ function DkPunkPicker({ T, onPick, onClose }) {
2840
+ const TYPES = ["any", "female", "male", "zombie", "ape", "alien"];
2841
+ const tLabel = (t) => T && T["pt_" + t] || t;
2842
+ const [type, setType] = React.useState("any");
2843
+ const [batch, setBatch] = React.useState(null);
2844
+ const [err, setErr] = React.useState(null);
2845
+ const [gen, setGen] = React.useState(0);
2846
+ React.useEffect(() => {
2847
+ let dead = false;
2848
+ setBatch(null);
2849
+ setErr(null);
2850
+ fetch(`/api/punk-list?type=${type}&limit=24`).then((r) => r.json()).then((d) => {
2851
+ if (dead)
2852
+ return;
2853
+ if (d.ok)
2854
+ setBatch(d.list || []);
2855
+ else
2856
+ setErr(d.error || "load failed");
2857
+ }).catch((e) => {
2858
+ if (!dead)
2859
+ setErr(String(e && e.message || e));
2860
+ });
2861
+ return () => {
2862
+ dead = true;
2863
+ };
2864
+ }, [type, gen]);
2865
+ return /* @__PURE__ */ React.createElement("div", { onClick: onClose, style: { position: "fixed", inset: 0, zIndex: 90, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 480, maxWidth: "94vw", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 20, display: "flex", flexDirection: "column", gap: 14 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, T.ensPickPunk || "choose a punk"), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: () => setGen(gen + 1) }, T.ensShuffle || "shuffle"), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" } }, TYPES.map((t) => /* @__PURE__ */ React.createElement(
2866
+ "button",
2867
+ {
2868
+ key: t,
2869
+ onClick: () => setType(t),
2870
+ style: {
2871
+ padding: "4px 10px",
2872
+ borderRadius: 999,
2873
+ border: "1px solid var(--line)",
2874
+ cursor: "pointer",
2875
+ background: type === t ? "var(--accent)" : "transparent",
2876
+ color: type === t ? "#fff" : "var(--dim)",
2877
+ fontFamily: "var(--ui)",
2878
+ fontSize: 12
2879
+ }
2880
+ },
2881
+ tLabel(t)
2882
+ ))), /* @__PURE__ */ React.createElement("div", { style: { minHeight: 232 } }, err ? /* @__PURE__ */ React.createElement("div", { style: { padding: 16, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, err) : !batch ? /* @__PURE__ */ React.createElement("div", { style: { padding: 16, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : /* @__PURE__ */ React.createElement("div", { style: { display: "grid", gridTemplateColumns: "repeat(6, 1fr)", gap: 8 } }, batch.map((p) => /* @__PURE__ */ React.createElement(
2883
+ "button",
2884
+ {
2885
+ key: p.id,
2886
+ title: `#${p.id} \xB7 ${p.type}`,
2887
+ onClick: () => onPick(Number(p.id)),
2888
+ style: { padding: 0, border: "1px solid var(--line)", borderRadius: 10, cursor: "pointer", background: "#638596", overflow: "hidden", aspectRatio: "1" }
2889
+ },
2890
+ /* @__PURE__ */ React.createElement("img", { src: p.image, alt: `punk #${p.id}`, style: { width: "100%", height: "100%", imageRendering: "pixelated", display: "block" } })
2891
+ ))))));
2892
+ }
2893
+ function DkEnsCard({ T, me, onRecord }) {
2804
2894
  const [st, setSt] = React.useState({ loading: true });
2805
2895
  const [label, setLabel] = React.useState("");
2806
2896
  const [busy, setBusy] = React.useState(false);
@@ -2810,6 +2900,8 @@ function DkEnsCard({ T, me }) {
2810
2900
  setSt({ loading: false, registered: !!d.registered, record: d.record || null, mineOwned: !!d.mineOwned });
2811
2901
  if (d.registered && d.record && d.record.name)
2812
2902
  setLabel(d.record.name.replace(/\.beagles\.eth$/i, ""));
2903
+ if (onRecord)
2904
+ onRecord(d.record || null);
2813
2905
  }).catch((e) => setSt({ loading: false, error: String(e && e.message || e) }));
2814
2906
  };
2815
2907
  React.useEffect(() => {
@@ -2864,10 +2956,16 @@ function DkEnsCard({ T, me }) {
2864
2956
  };
2865
2957
  const field = { flex: 1, minWidth: 0, height: 34, borderRadius: 8, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--mono)", fontSize: 13, padding: "0 10px", outline: "none" };
2866
2958
  const row = { display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", borderBottom: "1px solid var(--line)" };
2959
+ const [picking, setPicking] = React.useState(false);
2867
2960
  const rec = st.record;
2868
2961
  const boundEth = rec && rec.addresses && rec.addresses["60"];
2869
2962
  const boundSol = rec && rec.addresses && rec.addresses["501"];
2870
- const walletRow = (lbl, bound, onBind, last) => /* @__PURE__ */ React.createElement("div", { style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, lbl), bound ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, bound), /* @__PURE__ */ React.createElement(CopyBtn, { value: bound, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy })) : /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotBound || "not bound"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: onBind }, bound ? T.ensRebind || "rebind" : T.ensBind || "bind"));
2963
+ const punkId = rec && rec.nft === "CryptoPunks" && rec.nftid > 0 ? rec.nftid : null;
2964
+ const pickPunk = (id) => {
2965
+ setPicking(false);
2966
+ post("/api/ens-avatar", { nftid: id });
2967
+ };
2968
+ const walletRow = (lbl, chain, bound, onBind, last) => /* @__PURE__ */ React.createElement("div", { style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, lbl), bound ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, bound), /* @__PURE__ */ React.createElement(CopyBtn, { value: bound, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy }), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => bind(chain, "") }, T.ensUnbind || "unbind")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotBound || "not bound"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: onBind }, T.ensBind || "bind")));
2871
2969
  return /* @__PURE__ */ React.createElement(Card, { label: T.ensCard || "Name \xB7 beagles.eth" }, st.loading ? /* @__PURE__ */ React.createElement("div", { style: { padding: "14px 16px", fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensName || "name"), /* @__PURE__ */ React.createElement(
2872
2970
  "input",
2873
2971
  {
@@ -2881,12 +2979,14 @@ function DkEnsCard({ T, me }) {
2881
2979
  },
2882
2980
  style: field
2883
2981
  }
2884
- ), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--dim)", flexShrink: 0 } }, ".beagles.eth"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", disabled: busy || !label.trim(), onClick: register }, busy ? "\u2026" : st.registered && st.mineOwned ? T.ensUpdate || "update" : T.ensRegister || "register")), st.registered && !st.mineOwned && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", borderBottom: "1px solid var(--line)", fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, (T.ensWalletOwned || "registered via your mobile wallet as") + " ", /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)" } }, rec && rec.name)), walletRow(T.ensEth || "ethereum", boundEth, bindEth, false), walletRow(T.ensSol || "solana", boundSol, bindSol, !msg), msg && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", fontFamily: "var(--ui)", fontSize: 12, color: msg.tone === "ok" ? "var(--online)" : "var(--danger)" } }, msg.text)));
2982
+ ), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--dim)", flexShrink: 0 } }, ".beagles.eth"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", disabled: busy || !label.trim(), onClick: register }, busy ? "\u2026" : st.registered && st.mineOwned ? T.ensUpdate || "update" : T.ensRegister || "register")), st.registered && !st.mineOwned && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", borderBottom: "1px solid var(--line)", fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, (T.ensWalletOwned || "registered via your mobile wallet as") + " ", /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)" } }, rec && rec.name)), st.registered && /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensAvatar || "avatar"), punkId != null ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: punkId, size: 34, radius: 9, fallbackSeed: me.userId }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, `CryptoPunk #${punkId}`), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotSet || "not set"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => setPicking(true) }, T.ensChoose || "choose")), walletRow(T.ensEth || "ethereum", "eth", boundEth, bindEth, false), walletRow(T.ensSol || "solana", "sol", boundSol, bindSol, !msg), msg && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", fontFamily: "var(--ui)", fontSize: 12, color: msg.tone === "ok" ? "var(--online)" : "var(--danger)" } }, msg.text)), picking && /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) }));
2885
2983
  }
2886
2984
  function ProfileTab({ T, me, onEdit }) {
2887
2985
  const [qr, setQr] = React.useState(null);
2888
2986
  const [editing, setEditing] = React.useState(false);
2889
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 18, padding: "20px 22px", borderRadius: 14, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 68, radius: 16 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 18, height: 18, borderRadius: 999, background: "var(--online)", border: "3px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, letterSpacing: -0.5, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4 } }, me.handle), me.description ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 3 } }, me.description) : null), /* @__PURE__ */ React.createElement(Btn, { icon: "edit", onClick: () => setEditing(true) }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.userId, value: me.userId, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.carrierAddr, value: me.carrier, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.netKey, value: me.netKey, copy: true, last: true })), /* @__PURE__ */ React.createElement(DkEnsCard, { T, me }), typeof me.autoAccept === "boolean" && /* @__PURE__ */ React.createElement(Card, { label: T && T.friendsSettings || "Friends" }, /* @__PURE__ */ React.createElement("label", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px", cursor: "pointer" } }, /* @__PURE__ */ React.createElement(
2987
+ const [ensRec, setEnsRec] = React.useState(null);
2988
+ const punkId = ensRec && ensRec.nft === "CryptoPunks" && ensRec.nftid > 0 ? ensRec.nftid : null;
2989
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 18, padding: "20px 22px", borderRadius: 14, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, punkId != null ? /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: punkId, size: 68, radius: 16, fallbackSeed: me.userId }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 68, radius: 16 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 18, height: 18, borderRadius: 999, background: "var(--online)", border: "3px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, letterSpacing: -0.5, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4 } }, me.handle), me.description ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 3 } }, me.description) : null), /* @__PURE__ */ React.createElement(Btn, { icon: "edit", onClick: () => setEditing(true) }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.userId, value: me.userId, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.carrierAddr, value: me.carrier, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.netKey, value: me.netKey, copy: true, last: true })), /* @__PURE__ */ React.createElement(DkEnsCard, { T, me, onRecord: setEnsRec }), typeof me.autoAccept === "boolean" && /* @__PURE__ */ React.createElement(Card, { label: T && T.friendsSettings || "Friends" }, /* @__PURE__ */ React.createElement("label", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px", cursor: "pointer" } }, /* @__PURE__ */ React.createElement(
2890
2990
  "input",
2891
2991
  {
2892
2992
  type: "checkbox",
@@ -2904,9 +3004,12 @@ function ProfileTab({ T, me, onEdit }) {
2904
3004
  setEditing(false);
2905
3005
  } }));
2906
3006
  }
2907
- Object.assign(window, { ProfileTab });
3007
+ Object.assign(window, { ProfileTab, DkPunkAvatar });
2908
3008
  function DkDirAvatar({ p, size }) {
2909
3009
  const [broken, setBroken] = React.useState(false);
3010
+ if (p.punkId != null) {
3011
+ return /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: p.punkId, size, radius: Math.round(size / 4), fallbackSeed: p.userid || p.name });
3012
+ }
2910
3013
  if (p.avatar && !broken) {
2911
3014
  return /* @__PURE__ */ React.createElement(
2912
3015
  "img",
@@ -3924,8 +4027,20 @@ const STR = {
3924
4027
  ensEth: "ethereum",
3925
4028
  ensSol: "solana",
3926
4029
  ensBind: "bind",
3927
- ensRebind: "rebind",
4030
+ ensUnbind: "unbind",
3928
4031
  ensNotBound: "not bound",
4032
+ ensAvatar: "avatar",
4033
+ ensChoose: "choose",
4034
+ ensClear: "clear",
4035
+ ensNotSet: "not set",
4036
+ ensPickPunk: "choose a punk",
4037
+ ensShuffle: "shuffle",
4038
+ pt_any: "any",
4039
+ pt_female: "female",
4040
+ pt_male: "male",
4041
+ pt_zombie: "zombie",
4042
+ pt_ape: "ape",
4043
+ pt_alien: "alien",
3929
4044
  ensNoEthWallet: "no Ethereum wallet extension found (MetaMask\u2026)",
3930
4045
  ensNoSolWallet: "no Solana wallet extension found (Phantom\u2026)",
3931
4046
  ensWalletOwned: "registered via your mobile wallet as",
@@ -4049,8 +4164,20 @@ const STR = {
4049
4164
  ensEth: "ethereum",
4050
4165
  ensSol: "solana",
4051
4166
  ensBind: "\u7ED1\u5B9A",
4052
- ensRebind: "\u91CD\u65B0\u7ED1\u5B9A",
4167
+ ensUnbind: "\u89E3\u7ED1",
4053
4168
  ensNotBound: "\u672A\u7ED1\u5B9A",
4169
+ ensAvatar: "\u5934\u50CF",
4170
+ ensChoose: "\u9009\u62E9",
4171
+ ensClear: "\u6E05\u9664",
4172
+ ensNotSet: "\u672A\u8BBE\u7F6E",
4173
+ ensPickPunk: "\u9009\u4E00\u4E2A punk",
4174
+ ensShuffle: "\u6362\u4E00\u6279",
4175
+ pt_any: "\u5168\u90E8",
4176
+ pt_female: "\u5973",
4177
+ pt_male: "\u7537",
4178
+ pt_zombie: "\u50F5\u5C38",
4179
+ pt_ape: "\u733F",
4180
+ pt_alien: "\u5916\u661F\u4EBA",
4054
4181
  ensNoEthWallet: "\u672A\u68C0\u6D4B\u5230\u4EE5\u592A\u574A\u94B1\u5305\u63D2\u4EF6(MetaMask \u7B49)",
4055
4182
  ensNoSolWallet: "\u672A\u68C0\u6D4B\u5230 Solana \u94B1\u5305\u63D2\u4EF6(Phantom \u7B49)",
4056
4183
  ensWalletOwned: "\u5DF2\u901A\u8FC7\u624B\u673A\u94B1\u5305\u6CE8\u518C\u4E3A",
package/dist/server.js CHANGED
@@ -111,6 +111,27 @@ async function ensSignAndSet(call, record) {
111
111
  clearTimeout(timer);
112
112
  }
113
113
  }
114
+ // ── CryptoPunks avatar directory ────────────────────────────────────────────
115
+ // Self-hosted punks API serving all 10k punks as inline SVG data URIs. The
116
+ // record convention (shared with the mobile apps) is nft="CryptoPunks" +
117
+ // nftid=<punk id> on the beagles.eth name. Proxied server-side (browser can't
118
+ // reach the origin's port cross-origin reliably); NOTE the upstream's TLS cert
119
+ // must be valid — renew it when these routes start failing.
120
+ const PUNKS_API_URL = process.env.PUNKS_API_URL || "https://app.beagle.chat:1337";
121
+ const punkCache = new Map(); // punk id → JSON; immutable set
122
+ async function punksFetch(path) {
123
+ const ctl = new AbortController();
124
+ const timer = setTimeout(() => ctl.abort(), 10_000);
125
+ try {
126
+ const r = await fetch(`${PUNKS_API_URL}${path}`, { signal: ctl.signal });
127
+ if (!r.ok)
128
+ throw new Error(`punks upstream ${r.status}`);
129
+ return await r.json();
130
+ }
131
+ finally {
132
+ clearTimeout(timer);
133
+ }
134
+ }
114
135
  /** True only when the request came from the local machine. Used to gate the
115
136
  * "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
116
137
  * identity signing to other hosts. The popup always runs in the local user's
@@ -428,12 +449,77 @@ export function startBeagleServer(opts) {
428
449
  // Register/update this node's *.beagles.eth name and bind wallet
429
450
  // addresses into the record. The write routes trigger identity
430
451
  // signatures, so they're LOCALHOST-ONLY like /connect.
431
- if (url === "/api/ens-register" || url === "/api/ens-bind-wallet") {
452
+ if (url === "/api/ens-register" || url === "/api/ens-bind-wallet" || url === "/api/ens-avatar") {
432
453
  if (!isLocalRequest(req)) {
433
454
  sendJson(res, 403, { ok: false, error: "identity signing is available only on this machine (localhost)" });
434
455
  return;
435
456
  }
436
457
  }
458
+ // Punk directory (proxied): a random batch by type, and one punk by id.
459
+ if (req.method === "GET" && url.startsWith("/api/punk-list")) {
460
+ try {
461
+ const q = new URL(url, "http://localhost");
462
+ const type = (q.searchParams.get("type") || "any").toLowerCase().replace(/[^a-z]/g, "") || "any";
463
+ const limit = Math.min(60, Math.max(1, Number(q.searchParams.get("limit")) || 24));
464
+ const list = await punksFetch(`/api/punks/filter/${type}/any?limit=${limit}`);
465
+ sendJson(res, 200, { ok: true, list });
466
+ }
467
+ catch (err) {
468
+ sendJson(res, 502, { ok: false, error: String(err?.message ?? err) });
469
+ }
470
+ return;
471
+ }
472
+ if (req.method === "GET" && /^\/api\/punk\/\d{1,5}$/.test(url)) {
473
+ const id = url.slice("/api/punk/".length);
474
+ try {
475
+ let punk = punkCache.get(id);
476
+ if (!punk) {
477
+ punk = await punksFetch(`/api/punks/${id}`);
478
+ punkCache.set(id, punk);
479
+ }
480
+ sendJson(res, 200, { ok: true, punk });
481
+ }
482
+ catch (err) {
483
+ sendJson(res, 502, { ok: false, error: String(err?.message ?? err) });
484
+ }
485
+ return;
486
+ }
487
+ // Set (or clear) my punk avatar on the beagles.eth record.
488
+ if (req.method === "POST" && url === "/api/ens-avatar") {
489
+ const body = await readBody(req);
490
+ const clear = body.nftid === null || body.nftid === undefined;
491
+ const nftid = Number(body.nftid);
492
+ if (!clear && (!Number.isInteger(nftid) || nftid < 0 || nftid > 9999)) {
493
+ sendJson(res, 400, { ok: false, error: "invalid punk id" });
494
+ return;
495
+ }
496
+ const diag = await opts.call({ op: "diag" });
497
+ const identity = (diag.ok ? diag.data?.identity : null) ?? {};
498
+ if (!identity.userid) {
499
+ sendJson(res, 502, { ok: false, error: "identity unavailable" });
500
+ return;
501
+ }
502
+ const mine = await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`);
503
+ if (!mine) {
504
+ sendJson(res, 404, { ok: false, error: "register a name first" });
505
+ return;
506
+ }
507
+ if (mine.owner !== identity.userid) {
508
+ sendJson(res, 403, { ok: false, error: "this name was registered by your mobile wallet — pick the avatar there" });
509
+ return;
510
+ }
511
+ const r = await ensSignAndSet(opts.call, {
512
+ name: mine.name,
513
+ owner: identity.userid,
514
+ addresses: mine.addresses ?? {},
515
+ texts: mine.texts ?? {},
516
+ referee: mine.referee ?? "",
517
+ nft: clear ? "" : "CryptoPunks",
518
+ nftid: clear ? 0 : nftid,
519
+ });
520
+ sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, nftid: clear ? null : nftid } : { ok: false, error: r.error });
521
+ return;
522
+ }
437
523
  if (req.method === "GET" && url === "/api/ens-name") {
438
524
  const diag = await opts.call({ op: "diag" });
439
525
  const identity = (diag.ok ? diag.data?.identity : null) ?? {};
@@ -835,7 +921,7 @@ export function startBeagleServer(opts) {
835
921
  }
836
922
  if (req.method === "GET" && url === "/api/discover-registered") {
837
923
  try {
838
- const raw = (await fetchDiscoverJson("https://ens-gateway.beaglechat.workers.dev/names"));
924
+ const raw = (await fetchDiscoverJson(`${ENS_GATEWAY}/names`));
839
925
  const list = Object.entries(raw && typeof raw === "object" ? raw : {})
840
926
  .map(([ens, v]) => {
841
927
  const t = v?.texts ?? {};
@@ -845,6 +931,8 @@ export function startBeagleServer(opts) {
845
931
  name: t.displayName || ens,
846
932
  ens,
847
933
  description: t.description || null,
934
+ // punk avatar convention: nft="CryptoPunks" + nftid>0 (0 = unset)
935
+ punkId: v?.nft === "CryptoPunks" && typeof v?.nftid === "number" && v.nftid > 0 ? v.nftid : null,
848
936
  };
849
937
  })
850
938
  .filter((e) => e.userid && e.address);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",