diff --git a/404.html b/404.html
index 51283fc..57485d4 100644
--- a/404.html
+++ b/404.html
@@ -10,8 +10,8 @@
/>
禅聊 ZenChat
-
-
+
+
diff --git a/assets/index-BxoYAeQ1.css b/assets/index-BxoYAeQ1.css
new file mode 100644
index 0000000..13112a7
--- /dev/null
+++ b/assets/index-BxoYAeQ1.css
@@ -0,0 +1 @@
+:root,[data-theme=light]{--bg: #faf9f5;--grouped: #f3eee4;--fg: #1a1612;--muted: #6f665c;--hair: #e4d9c8;--accent: #d97757;--accent-deep: #c15f3c;--on-accent: #faf6f0;--danger: #9f1239;--ok: #4f8a6a;--fill: rgba(44, 28, 16, .06);--shadow: 0 18px 48px rgba(48, 28, 12, .1);--glow-1: color-mix(in srgb, var(--accent) 22%, transparent);--glow-2: color-mix(in srgb, #e8b86a 18%, transparent)}[data-theme=dark]{--bg: #161412;--grouped: #211e1a;--fg: #f4f0e8;--muted: #a89f94;--hair: #3a332b;--accent: #e39472;--accent-deep: #d97757;--on-accent: #1a1410;--danger: #fb7185;--ok: #7dba8e;--fill: rgba(255, 236, 214, .07);--shadow: 0 18px 48px rgba(0, 0, 0, .4);--glow-1: color-mix(in srgb, var(--accent) 18%, transparent);--glow-2: color-mix(in srgb, #c96442 12%, transparent)}*{box-sizing:border-box}html,body,#app{height:var(--app-h, 100dvh);margin:0;overflow:hidden}body{font:16px/1.5 "Source Serif 4",Iowan Old Style,Palatino Linotype,Songti SC,"Noto Serif SC",PingFang SC,serif;background:radial-gradient(1100px 460px at 8% -12%,var(--glow-1),transparent 54%),radial-gradient(820px 380px at 108% -4%,var(--glow-2),transparent 50%),var(--bg);color:var(--fg);letter-spacing:.01em;-webkit-font-smoothing:antialiased;overscroll-behavior:none}::selection{background:color-mix(in srgb,var(--accent) 34%,transparent)}input,select,textarea,.tab,.btn,.status,.member,.footer,.hint,.field span{font-family:IBM Plex Sans,"Source Sans 3",-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Noto Sans SC,sans-serif}#app{display:flex}.shell{flex:1;min-height:0;width:min(920px,100%);margin:0 auto;padding:0 16px calc(10px + env(safe-area-inset-bottom,0px));padding-top:env(safe-area-inset-top,0px);display:flex;flex-direction:column}.topbar{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px;height:52px;flex:0 0 auto}.brand{display:flex;align-items:center;gap:9px;text-decoration:none;color:inherit}.enso{width:18px;height:18px;border:2px solid var(--accent);border-radius:50%;border-right-color:transparent;transform:rotate(-18deg);flex:0 0 auto}.brand strong{font-size:1.22rem;font-weight:600;letter-spacing:.04em}.tagline{color:var(--muted);font-size:12px;font-family:IBM Plex Sans,"Source Sans 3",-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,sans-serif;white-space:nowrap}.mode-chat .tagline{display:none}.tabs{display:flex;gap:2px;overflow-x:auto;min-width:0;scrollbar-width:none}.tabs::-webkit-scrollbar{display:none}.tab,.btn{font:inherit;color:inherit;background:transparent;border:0;cursor:pointer;touch-action:manipulation}.tab{flex:0 0 auto;padding:6px 10px;border-radius:999px;color:var(--muted);text-decoration:none;font-size:13px}.tab.on{color:var(--accent-deep);background:color-mix(in srgb,var(--accent) 14%,transparent)}.tab.add{width:32px;padding:6px 0}.toolbar{display:flex;gap:2px}.btn{padding:6px 11px;border-radius:999px;color:var(--muted);font-size:13px}.btn.ghost:hover,.tab:hover{background:var(--fill);color:var(--fg)}.btn.primary,.btn.send{background:var(--accent);color:var(--on-accent);border:1px solid transparent}.btn.primary:hover,.btn.send:hover{background:var(--accent-deep);color:var(--on-accent)}.btn.send{width:32px;height:32px;padding:0;border-radius:50%;font-size:16px;line-height:32px}.btn:focus-visible,.tab:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid color-mix(in srgb,var(--accent) 70%,transparent);outline-offset:2px}.main{flex:1;min-height:0;display:flex;flex-direction:column}.lobby{display:grid;gap:12px;align-content:start;min-height:0;overflow:auto}.lede{margin:4px 0 2px;max-width:36em;color:var(--muted);font-size:15px;line-height:1.65}.panel{background:color-mix(in srgb,var(--grouped) 78%,var(--bg));border:1px solid var(--hair);border-radius:20px;padding:16px;box-shadow:var(--shadow)}.lobby-form{display:grid;gap:10px}.join-row{display:grid;grid-template-columns:1fr 1fr auto;gap:8px;align-items:end}.join-btn{min-height:40px;padding:0 18px}.row{display:grid;grid-template-columns:1fr 1fr;gap:8px}.field{display:grid;gap:4px}.field span,.hint,.muted,.footer,.status,.system{color:var(--muted);font-size:13px}.field input,.field select,.composer textarea,.jump input,.jump select{width:100%;font:inherit;font-size:16px;color:inherit;background:color-mix(in srgb,var(--bg) 80%,white);border:1px solid var(--hair);border-radius:12px;padding:8px 10px}[data-theme=dark] .field input,[data-theme=dark] .field select,[data-theme=dark] .composer textarea,[data-theme=dark] .jump input,[data-theme=dark] .jump select{background:color-mix(in srgb,var(--grouped) 70%,#000)}.more{color:var(--muted);font-size:13px}.more summary{cursor:pointer;padding:4px 0}.actions{display:flex;gap:8px}.chat{position:relative;flex:1;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 200px;gap:12px}.transcript{display:flex;flex-direction:column;min-height:0;min-width:0}.status-row{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:28px}.status{display:flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.log{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:8px;padding:4px 0 8px;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}.bubble{max-width:min(78%,36em);padding:8px 12px;border-radius:16px 16px 16px 4px;background:var(--grouped);border:1px solid color-mix(in srgb,var(--hair) 70%,transparent)}.bubble.self{align-self:flex-end;border-radius:16px 16px 4px;background:var(--accent);color:var(--on-accent);border-color:transparent}.bubble.self header,.bubble.self .muted{color:color-mix(in srgb,var(--on-accent) 74%,transparent)}.bubble header{display:flex;gap:8px;font-size:11px;color:var(--muted)}.bubble p{margin:2px 0 0;white-space:pre-wrap;word-break:break-word}.bubble.stamp p{font-size:22px;line-height:1.3}.system{align-self:center}.composer{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:end;padding:8px 0 4px;background:transparent}.composer textarea{resize:none;min-height:36px;max-height:96px;line-height:1.3;border-radius:18px;padding:8px 14px}.dot{width:6px;height:6px;border-radius:50%;background:var(--muted);display:inline-block;margin-right:6px;flex:0 0 auto}.dot.ok{background:var(--ok)}.side{min-height:0;overflow:auto;padding:8px 4px}.side h2{margin:0 0 8px;font-size:12px;font-weight:600;color:var(--muted);letter-spacing:.06em}.member{display:flex;justify-content:space-between;gap:8px;padding:8px 0;border-bottom:1px solid var(--hair);font-size:14px}.banner{color:var(--danger);font-size:13px;padding:8px 0}.footer{margin:0;padding:8px 0 0}.footer a{color:inherit}.picker{position:absolute;left:12px;right:12px;bottom:56px;background:var(--grouped);border:1px solid var(--hair);border-radius:16px;padding:10px;z-index:3;box-shadow:var(--shadow)}.picker-tabs{display:flex;gap:4px;margin-bottom:8px}.picker-grid{display:flex;flex-wrap:wrap;gap:6px;max-height:140px;overflow:auto}.stamp-btn{font:inherit;color:inherit;background:var(--bg);border:1px solid var(--hair);border-radius:10px;padding:6px 8px;cursor:pointer}.stamp-btn:hover{border-color:var(--accent)}.emoji .stamp-btn{font-size:18px}.hidden{display:none!important}.jump{border:1px solid var(--hair);border-radius:20px;background:var(--grouped);color:inherit;padding:0;width:min(400px,calc(100% - 32px));box-shadow:var(--shadow)}.jump::backdrop{background:#1c120a6b}.jump-form{display:grid;gap:10px;padding:18px}.jump h2{margin:0;font-size:1.15rem;font-family:"Source Serif 4",Iowan Old Style,Palatino Linotype,Songti SC,serif}@media(max-width:720px){.tagline{display:none}.join-row,.row{grid-template-columns:1fr}.join-btn{width:100%;min-height:44px}.chat{grid-template-columns:1fr}.side{display:none;position:absolute;top:36px;right:0;width:min(240px,80vw);max-height:50%;background:var(--grouped);border:1px solid var(--hair);border-radius:16px;padding:10px 12px;z-index:2;box-shadow:var(--shadow)}.chat.show-side .side{display:block}.topbar{grid-template-columns:auto 1fr auto}}
diff --git a/assets/index-CDh36co2.css b/assets/index-CDh36co2.css
deleted file mode 100644
index 17c63c4..0000000
--- a/assets/index-CDh36co2.css
+++ /dev/null
@@ -1 +0,0 @@
-:root,[data-theme=light]{--bg: #fff;--grouped: #f5f5f7;--fg: #1d1d1f;--muted: #86868b;--hair: rgba(0, 0, 0, .08);--accent: #0071e3;--danger: #ff3b30;--fill: rgba(0, 0, 0, .04)}[data-theme=dark]{--bg: #000;--grouped: #1c1c1e;--fg: #f5f5f7;--muted: #98989d;--hair: rgba(255, 255, 255, .1);--accent: #0a84ff;--danger: #ff453a;--fill: rgba(255, 255, 255, .08)}*{box-sizing:border-box}html,body,#app{height:var(--app-h, 100dvh);margin:0;overflow:hidden}body{font:16px/1.45 -apple-system,BlinkMacSystemFont,SF Pro Text,Segoe UI,PingFang SC,Noto Sans SC,sans-serif;background:var(--bg);color:var(--fg);letter-spacing:-.01em;-webkit-font-smoothing:antialiased;overscroll-behavior:none}#app{display:flex}.shell{flex:1;min-height:0;width:min(920px,100%);margin:0 auto;padding:0 16px calc(10px + env(safe-area-inset-bottom,0px));padding-top:env(safe-area-inset-top,0px);display:flex;flex-direction:column}.topbar{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px;height:48px;flex:0 0 auto}.brand{display:flex;align-items:center;gap:8px;text-decoration:none;color:inherit}.enso{width:14px;height:14px;border:1.5px solid var(--fg);border-radius:50%;border-right-color:transparent}.brand strong{font-size:17px;font-weight:600}.tabs{display:flex;gap:2px;overflow-x:auto;min-width:0;scrollbar-width:none}.tabs::-webkit-scrollbar{display:none}.tab,.btn{font:inherit;color:inherit;background:transparent;border:0;cursor:pointer;touch-action:manipulation}.tab{flex:0 0 auto;padding:6px 10px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:14px}.tab.on{color:var(--fg);background:var(--fill)}.tab.add{width:32px;padding:6px 0}.toolbar{display:flex;gap:2px}.btn{padding:6px 11px;border-radius:8px;color:var(--muted);font-size:14px}.btn.ghost:hover,.tab:hover{background:var(--fill);color:var(--fg)}.btn.primary,.btn.send{background:var(--accent);color:#fff}.btn.send{width:32px;height:32px;padding:0;border-radius:50%;font-size:16px;line-height:32px}.main{flex:1;min-height:0;display:flex;flex-direction:column}.panel{background:var(--grouped);border-radius:12px;padding:14px}.lobby-form{display:grid;gap:10px}.join-row{display:grid;grid-template-columns:1fr 1fr auto;gap:8px;align-items:end}.join-btn{min-height:40px;padding:0 16px}.row{display:grid;grid-template-columns:1fr 1fr;gap:8px}.field{display:grid;gap:4px}.field span,.hint,.muted,.footer,.status,.system{color:var(--muted);font-size:13px}.field input,.field select,.composer textarea,.jump input,.jump select{width:100%;font:inherit;font-size:16px;color:inherit;background:var(--bg);border:1px solid var(--hair);border-radius:10px;padding:8px 10px}.more{color:var(--muted);font-size:13px}.more summary{cursor:pointer;padding:4px 0}.actions{display:flex;gap:8px}.chat{position:relative;flex:1;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 200px;gap:12px}.transcript{display:flex;flex-direction:column;min-height:0;min-width:0}.status-row{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:28px}.status{display:flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.log{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:8px;padding:4px 0 8px;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}.bubble{max-width:min(78%,36em);padding:8px 12px;border-radius:16px 16px 16px 4px;background:var(--grouped)}.bubble.self{align-self:flex-end;border-radius:16px 16px 4px;background:var(--accent);color:#fff}.bubble.self header,.bubble.self .muted{color:#ffffffb8}.bubble header{display:flex;gap:8px;font-size:11px;color:var(--muted)}.bubble p{margin:2px 0 0;white-space:pre-wrap;word-break:break-word}.bubble.stamp p{font-size:22px;line-height:1.3}.system{align-self:center}.composer{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:end;padding:8px 0 4px;background:var(--bg)}.composer textarea{resize:none;min-height:36px;max-height:96px;line-height:1.3;border-radius:18px;padding:8px 14px}.dot{width:6px;height:6px;border-radius:50%;background:var(--muted);display:inline-block;margin-right:6px;flex:0 0 auto}.dot.ok{background:#34c759}.side{min-height:0;overflow:auto;padding:8px 4px}.side h2{margin:0 0 8px;font-size:12px;font-weight:600;color:var(--muted)}.member{display:flex;justify-content:space-between;gap:8px;padding:8px 0;border-bottom:1px solid var(--hair);font-size:14px}.banner{color:var(--danger);font-size:13px;padding:8px 0}.footer{margin:0;padding:8px 0 0}.footer a{color:inherit}.picker{position:absolute;left:12px;right:12px;bottom:56px;background:var(--grouped);border:1px solid var(--hair);border-radius:14px;padding:10px;z-index:3}.picker-tabs{display:flex;gap:4px;margin-bottom:8px}.picker-grid{display:flex;flex-wrap:wrap;gap:6px;max-height:140px;overflow:auto}.stamp-btn{font:inherit;color:inherit;background:var(--bg);border:1px solid var(--hair);border-radius:8px;padding:6px 8px;cursor:pointer}.emoji .stamp-btn{font-size:18px}.hidden{display:none!important}.jump{border:0;border-radius:14px;background:var(--grouped);color:inherit;padding:0;width:min(400px,calc(100% - 32px))}.jump::backdrop{background:#0006}.jump-form{display:grid;gap:10px;padding:16px}.jump h2{margin:0;font-size:17px}@media(max-width:720px){.join-row,.row{grid-template-columns:1fr}.join-btn{width:100%;min-height:44px}.chat{grid-template-columns:1fr}.side{display:none;position:absolute;top:36px;right:0;width:min(240px,80vw);max-height:50%;background:var(--grouped);border-radius:12px;padding:10px 12px;z-index:2;box-shadow:0 8px 32px #0000002e}.chat.show-side .side{display:block}.topbar{grid-template-columns:auto 1fr auto}}
diff --git a/assets/index-CE2I8ZcW.js b/assets/index-CE2I8ZcW.js
deleted file mode 100644
index d8f1f6e..0000000
--- a/assets/index-CE2I8ZcW.js
+++ /dev/null
@@ -1,3 +0,0 @@
-const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-BgLl3BAq.js","assets/strategy-C52a_06u.js","assets/index-DSJFV8_4.js"])))=>i.map(i=>d[i]);
-(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const c of r.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&n(c)}).observe(document,{childList:!0,subtree:!0});function s(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function n(i){if(i.ep)return;i.ep=!0;const r=s(i);fetch(i.href,r)}})();const st="zenchat.andyccr.v1",m=1,nt=24,it=64,H=4e3,V=10,v=200,ot="lobby",rt=2500,at=1800,lt=25e3,ct=5e3,ht="https://github.com/Andyccr/ZenChat",k="zenchat.nick",R="zenchat.peerId",j=["闲云","青石","疏影","晚风","折柳","听雨","归舟","松风"];function L(e){const t=new Uint8Array(e);return crypto.getRandomValues(t),[...t].map(s=>s.toString(16).padStart(2,"0")).join("")}function J(e){let t=2166136261;for(let n=0;n{const i=n.codePointAt(0)??0;return i>=32&&i!==127}).join("").slice(0,nt)}function z(e){const t=Number.parseInt(e.slice(0,4),16)%j.length;return j[t]??"闲云"}function U(){const e=sessionStorage.getItem(R),t=e&&/^[0-9a-f]{16,}$/i.test(e)?e:L(12);sessionStorage.setItem(R,t);const s=localStorage.getItem(k),n=y(s??"")||z(t);return localStorage.setItem(k,n),{id:t,nick:n}}function dt(e){const t=y(e)||z(U().id);return localStorage.setItem(k,t),t}function ut(e){return e==="torrent"||e==="nostr"}function p(e){return e.normalize("NFKC").trim().replace(/\s+/g,"-").replace(/[/#?&]+/g,"").slice(0,it)}function mt(e){const t=p(e.name);return e.password?`${t}::${e.password}`:t}function I(e){return`${e.strategy}:${p(e.name)}`}function T(e){return`${e.strategy}:${p(e.name)}:${e.password}`}function F(e,t){return T(e)===T(t)}const G="zenchat.recentRooms";function W(){try{const e=localStorage.getItem(G);if(!e)return[];const t=JSON.parse(e);return Array.isArray(t)?t.filter(s=>typeof s=="object"&&s!==null&&typeof s.name=="string"&&(s.strategy==="torrent"||s.strategy==="nostr")&&typeof s.hasPassword=="boolean"&&typeof s.visitedAt=="number").slice(0,V):[]}catch{return[]}}function pt(e){const t={name:e.name,strategy:e.strategy,hasPassword:!!e.password,visitedAt:Date.now()},s=W().filter(i=>I(i)!==I(e)),n=[t,...s].slice(0,V);return localStorage.setItem(G,JSON.stringify(n)),n}const ft="zenchat.cache.v1.";function q(e){return ft+T(e)}function yt(e){try{const t=sessionStorage.getItem(q(e));if(!t)return[];const s=JSON.parse(t);return Array.isArray(s)?s.filter(bt).slice(-v):[]}catch{return[]}}function gt(e,t){try{sessionStorage.setItem(q(e),JSON.stringify(t.slice(-v)))}catch{}}function M(e){return e.length>v?e.slice(-v):e}function bt(e){if(typeof e!="object"||e===null)return!1;const t=e;return t.kind==="system"?typeof t.id=="string"&&typeof t.text=="string"&&typeof t.ts=="number":t.kind==="chat"?typeof t.id=="string"&&typeof t.fromId=="string"&&typeof t.nick=="string"&&typeof t.text=="string"&&typeof t.ts=="number"&&typeof t.self=="boolean":!1}function wt(e){return typeof e=="object"&&e!==null}function vt(e){return e===m}function Et(e,t,s,n){return{v:m,type:"chat",id:s,ts:n,nick:y(e),text:t.trim().slice(0,H)}}function E(e){return{v:m,type:"hello",nick:y(e)}}function St(e){return{v:m,type:"typing",nick:y(e)}}function kt(e){if(!wt(e)||!vt(e.v)||typeof e.type!="string")return null;const t=typeof e.nick=="string"?y(e.nick):"";if(!t)return null;if(e.type==="hello")return{v:m,type:"hello",nick:t};if(e.type==="typing")return{v:m,type:"typing",nick:t};if(e.type==="chat"){if(typeof e.id!="string"||e.id.length<8||e.id.length>80||typeof e.ts!="number"||!Number.isFinite(e.ts)||typeof e.text!="string")return null;const s=e.text.trim().slice(0,H);return s?{v:m,type:"chat",id:e.id,ts:e.ts,nick:t,text:s}:null}return null}function Lt(e,t){let s=0;return(...n)=>{const i=Date.now();i-s{s=n,!t&&(t=requestAnimationFrame(()=>{t=0;const i=s;s=null,i&&e(...i)}))}}const Ct="modulepreload",Pt=function(e){return"/ZenChat/"+e},A={},$=function(t,s,n){let i=Promise.resolve();if(s&&s.length>0){let P=function(d){return Promise.all(d.map(f=>Promise.resolve(f).then(g=>({status:"fulfilled",value:g}),g=>({status:"rejected",reason:g}))))};var c=P;document.getElementsByTagName("link");const l=document.querySelector("meta[property=csp-nonce]"),h=l?.nonce||l?.getAttribute("nonce");i=P(s.map(d=>{if(d=Pt(d),d in A)return;A[d]=!0;const f=d.endsWith(".css"),g=f?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${d}"]${g}`))return;const u=document.createElement("link");if(u.rel=f?"stylesheet":Ct,f||(u.as="script"),u.crossOrigin="",u.href=d,h&&u.setAttribute("nonce",h),document.head.appendChild(u),f)return new Promise((tt,et)=>{u.addEventListener("load",tt),u.addEventListener("error",()=>et(new Error(`Unable to preload CSS for ${d}`)))})}))}function r(l){const h=new Event("vite:preloadError",{cancelable:!0});if(h.payload=l,window.dispatchEvent(h),!h.defaultPrevented)throw l}return i.then(l=>{for(const h of l||[])h.status==="rejected"&&r(h.reason);return t().catch(r)})},Rt=[{urls:"stun:stun.cloudflare.com:3478"},{urls:"stun:stun.l.google.com:19302"}],jt={iceServers:Rt,iceCandidatePoolSize:8,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},It=["wss://tracker.webtorrent.dev","wss://tracker.openwebtorrent.com","wss://open.ftorrent.com:443"],Mt=["wss://relay.damus.io","wss://nos.lol","wss://relay.snort.social","wss://nostr.wine"];function At(e){return Object.entries(e).map(([t,s])=>({url:t,readyState:s?.readyState??WebSocket.CLOSED}))}class N{strategy;room=null;action=null;module=null;constructor(t){this.strategy=t}selfId(){return this.module?.selfId??""}async join(t,s){await this.leave(),this.module=await C(t.strategy);const n=t.strategy==="torrent"?[...It]:[...Mt],i=this.module.joinRoom({appId:t.appId,...t.password?{password:t.password}:{},rtcConfig:jt,trickleIce:!0,relayConfig:{urls:n}},t.roomId,{onJoinError:c=>{s.onJoinError(c.error||"无法与对等节点完成握手")}}),r=i.makeAction("zen");r.onMessage=(c,{peerId:l})=>{s.onPayload(l,c)},i.onPeerJoin=c=>s.onPeerJoin(c),i.onPeerLeave=c=>s.onPeerLeave(c),this.room=i,this.action=r}async leave(){const t=this.room;if(this.room=null,this.action=null,t)try{await t.leave()}catch{}}async send(t,s){if(!this.action)throw new Error("尚未加入房间");await this.action.send(t,s?{target:s}:void 0)}ping(t){return this.room?this.room.ping(t):Promise.reject(new Error("未连接"))}peerIds(){return this.room?Object.keys(this.room.getPeers()):[]}relays(){return this.module?At(this.module.getRelaySockets()):[]}}const _=new Map;async function C(e){const t=_.get(e);if(t)return t;const s=e==="torrent"?$(()=>import("./index-BgLl3BAq.js"),__vite__mapDeps([0,1])).then(n=>({joinRoom:n.joinRoom,getRelaySockets:n.getRelaySockets,selfId:n.selfId})):$(()=>import("./index-DSJFV8_4.js"),__vite__mapDeps([2,1])).then(n=>({joinRoom:n.joinRoom,getRelaySockets:n.getRelaySockets,selfId:n.selfId}));return _.set(e,s),s}function $t(){C("torrent"),C("nostr")}class Nt{transport;identity;members=new Map;lines=[];seen=new Set;listeners;helloTimer=null;relayTimer=null;typingTimers=new Map;paused=!1;joined=!1;status={phase:"idle",detail:"尚未连接",relays:[],peerCount:0};emitMembers=Tt(()=>{this.listeners.onMembers?.([...this.members.values()].sort((t,s)=>t.joinedAt-s.joinedAt))});sendTypingThrottled=Lt(()=>{this.transport.send(St(this.identity.nick))},at);constructor(t,s,n){this.identity=t,this.listeners=s,this.transport=n??new N("torrent")}get selfId(){return this.transport.selfId()||this.identity.id}getLines(){return this.lines}hydrate(t){this.lines=M(t);for(const s of this.lines)s.kind==="chat"&&this.seen.add(s.id);this.listeners.onReset?.(this.lines)}async join(t){this.joined&&await this.leave({silent:!0}),this.transport=new N(t.strategy),this.joined=!0,this.setStatus({phase:"connecting",detail:t.strategy==="torrent"?"正在连接 Tracker…":"正在连接 Nostr…",relays:[],peerCount:0}),await this.transport.join({appId:st,roomId:mt({name:p(t.name),password:t.password}),password:t.password,strategy:t.strategy},{onPeerJoin:s=>{this.upsertMember(s,"访客"),this.pushSystem(`${s.slice(0,6)} 加入了房间`),this.transport.send(E(this.identity.nick),s),this.emitMembers(),this.refreshStatus("connected",`直连 ${this.transport.peerIds().length}`),window.setTimeout(()=>{this.measure(s)},350)},onPeerLeave:s=>{const n=this.members.get(s);this.members.delete(s),this.pushSystem(`${n?.nick??s.slice(0,6)} 离开了房间`),this.emitMembers();const i=this.transport.peerIds().length;this.refreshStatus(i>0?"connected":"connecting",i>0?`直连 ${i}`:"等待同伴")},onPayload:(s,n)=>this.handlePayload(s,n),onJoinError:s=>{this.refreshStatus("connecting",`握手受阻:${s}`)}}),this.bindVisibility(),this.startTimers(),this.refreshStatus("connecting","已宣布,等待对等节点")}async sendChat(t){const s=Et(this.identity.nick,t,L(8),Date.now());s.text&&(this.seen.add(s.id),this.pushLine({kind:"chat",id:s.id,fromId:this.selfId,nick:this.identity.nick,text:s.text,ts:s.ts,self:!0}),this.transport.send(s))}sendTyping(){this.sendTypingThrottled()}setNick(t){this.identity={...this.identity,nick:t},this.transport.send(E(t))}async leave(t={}){this.joined=!1,this.unbindVisibility(),this.stopTimers();for(const s of this.typingTimers.values())window.clearTimeout(s);this.typingTimers.clear(),await this.transport.leave(),this.members.clear(),this.lines=[],this.seen.clear(),t.silent||(this.setStatus({phase:"idle",detail:"已离开房间",relays:[],peerCount:0}),this.emitMembers(),this.listeners.onReset?.([]))}handlePayload(t,s){const n=kt(s);if(n){if(this.upsertMember(t,n.nick),n.type==="hello"){this.emitMembers();return}if(n.type==="typing"){this.setTyping(t),this.emitMembers();return}this.seen.has(n.id)||(this.seen.add(n.id),this.pushLine({kind:"chat",id:n.id,fromId:t,nick:n.nick,text:n.text,ts:n.ts,self:!1}),this.emitMembers())}}upsertMember(t,s){const n=this.members.get(t),i=Date.now();this.members.set(t,{id:t,nick:s,joinedAt:n?.joinedAt??i,lastSeenAt:i,rttMs:n?.rttMs??null,typing:n?.typing??!1})}setTyping(t){const s=this.members.get(t);if(!s)return;s.typing=!0;const n=this.typingTimers.get(t);n&&window.clearTimeout(n),this.typingTimers.set(t,window.setTimeout(()=>{const i=this.members.get(t);i&&(i.typing=!1,this.emitMembers())},rt))}async measure(t){try{const s=await this.transport.ping(t),n=this.members.get(t);n&&(n.rttMs=s,this.emitMembers())}catch{}}pushSystem(t){this.pushLine({kind:"system",id:L(6),text:t,ts:Date.now()})}pushLine(t){this.lines.push(t),this.lines=M(this.lines),this.listeners.onLine?.(t)}refreshStatus(t,s){this.setStatus({phase:t,detail:s,relays:this.transport.relays(),peerCount:this.transport.peerIds().length})}setStatus(t){const s=this.status;s.phase===t.phase&&s.detail===t.detail&&s.peerCount===t.peerCount&&s.relays.length===t.relays.length&&s.relays.every((n,i)=>n.readyState===t.relays[i]?.readyState)||(this.status=t,this.listeners.onStatus?.(t))}onVisibility=()=>{document.hidden?this.stopTimers():this.joined&&this.startTimers()};bindVisibility(){document.addEventListener("visibilitychange",this.onVisibility)}unbindVisibility(){document.removeEventListener("visibilitychange",this.onVisibility)}startTimers(){this.stopTimers(),this.helloTimer=window.setInterval(()=>{this.paused||this.transport.send(E(this.identity.nick))},lt),this.relayTimer=window.setInterval(()=>{this.refreshStatus(this.status.phase,this.status.detail)},ct)}stopTimers(){this.helloTimer!==null&&(window.clearInterval(this.helloTimer),this.helloTimer=null),this.relayTimer!==null&&(window.clearInterval(this.relayTimer),this.relayTimer=null),this.paused=document.hidden}}class _t{identity;listeners;session=null;spec=null;generation=0;constructor(t,s){this.identity=t,this.listeners=s}current(){return this.spec}getSession(){return this.session}setIdentity(t){this.identity=t,this.session?.setNick(t.nick)}async open(t){if(this.spec&&this.session&&F(this.spec,t))return this.session;const s=++this.generation;if(await this.snapshotAndClose(),s!==this.generation)return null;pt(t),this.listeners.onMembers?.([]);const n=new Nt(this.identity,this.listeners),i=yt(t);return i.length>0?n.hydrate(i):this.listeners.onReset?.([]),this.session=n,this.spec=t,await n.join(t),s!==this.generation?(await n.leave({silent:!0}),null):n}async close(){this.generation+=1,await this.snapshotAndClose()}async snapshotAndClose(){this.session&&this.spec&&(gt(this.spec,this.session.getLines()),await this.session.leave({silent:!0})),this.session=null,this.spec=null}}function O(e,t){return e.get(t)??""}function Ot(e){const t=e.startsWith("#")?e.slice(1):e,s=new URL(t||"/","https://zenchat.local"),n=s.pathname.split("/").filter(Boolean);if(n[0]==="r"&&n[1]){const i=p(decodeURIComponent(n[1]));if(!i)return{name:"lobby"};const r=O(s.searchParams,"s"),c=ut(r)?r:"torrent";return{name:"room",spec:{name:i,password:O(s.searchParams,"k"),strategy:c}}}return{name:"lobby"}}function b(e,t=!1){if(e.name==="lobby")return"#/";const s=new URLSearchParams;s.set("s",e.spec.strategy),t&&e.spec.password&&s.set("k",e.spec.password);const n=s.toString();return`#/r/${encodeURIComponent(e.spec.name)}${n?`?${n}`:""}`}const a={title:"禅聊",source:"源码",nick:"昵称",room:"房间",password:"口令",passwordHint:"口令只隔离房间,不会上传。",strategy:"信令",torrent:"Tracker",nostr:"Nostr",join:"加入",more:"更多选项",recent:"最近",share:"分享",shareWithKey:"含口令",leave:"大厅",send:"发送",placeholder:"发消息",members:"在线",you:"你",copied:"已复制",webrtcMissing:"需要支持 WebRTC 的浏览器,并用 HTTPS 打开。",insecure:"需要 HTTPS 或 localhost。",switch:"切换房间",addRoom:"加入房间",stamps:"表情",kaomoji:"颜文字",emoji:"表情",people:"成员"};function o(e,t={},s=[]){const n=document.createElement(e);for(const[i,r]of Object.entries(t))if(!(r===void 0||r===!1)){if(i==="class"){n.className=String(r);continue}if(typeof r=="boolean"){n.toggleAttribute(i,r);continue}if(i==="style"){n.setAttribute("style",String(r));continue}if(i in n&&i!=="list"){n[i]=r;continue}n.setAttribute(i,String(r))}for(const i of s)n.append(typeof i=="string"?document.createTextNode(i):i);return n}function x(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function xt(e){return new Intl.DateTimeFormat("zh-CN",{hour:"2-digit",minute:"2-digit",hour12:!1}).format(e)}function Bt(e){switch(e){case WebSocket.CONNECTING:return"连接中";case WebSocket.OPEN:return"已连接";case WebSocket.CLOSING:return"断开中";default:return"未连接"}}function Kt(e){try{return new URL(e).host}catch{return e}}const Y=["(´・ω・`)","(。•́︿•̀。)","(´▽`ʃ♡ƪ)","(╯°□°)╯︵ ┻━┻","¯\\_(ツ)_/¯","(づ。◕‿‿◕。)づ","(ง'̀-'́)ง","( ̄▽ ̄)","(•̀ᴗ•́)و","ฅ^•ﻌ•^ฅ","(´;ω;`)","(≧∇≦)ノ","(´∀`)♡","┐( ̄ヘ ̄)┌"],X=["😀","😂","🥰","😎","🤔","😭","🙏","✨","🔥","🎉","👍","👋","❤️","🌸","🍵","🌙"],Dt=new Set(Y),Ht=new Set(X);function Vt(e){const t=e.trim();return t?Dt.has(t)||Ht.has(t)?!0:t.length<=8&&/^[\p{Emoji_Presentation}\s]+$/u.test(t):!1}function B(e,t){const s=e.selectionStart,n=e.selectionEnd,i=`${e.value.slice(0,s)}${t}${e.value.slice(n)}`;e.value=i;const r=s+t.length;e.setSelectionRange(r,r),e.focus(),e.dispatchEvent(new Event("input",{bubbles:!0}))}class Jt{el;ids=new Set;constructor(){this.el=o("div",{class:"log",role:"log","aria-live":"polite"})}reset(t){this.el.replaceChildren(),this.ids.clear();for(const s of t)this.append(s,!1);this.stick()}append(t,s=!0){this.ids.has(t.id)||(this.ids.add(t.id),this.el.append(zt(t)),s&&this.stick())}stick(){const t=this.el;requestAnimationFrame(()=>{t.scrollHeight-t.scrollTop-t.clientHeight<96&&(t.scrollTop=t.scrollHeight)})}}function zt(e){if(e.kind==="system")return o("div",{class:"system"},[e.text]);const t=Vt(e.text);return o("article",{class:`bubble${e.self?" self":""}${t?" stamp":""}`},[o("header",{},[o("b",{style:`color:${J(e.fromId)}`},[e.nick]),o("time",{},[xt(e.ts)])]),o("p",{},[e.text])])}class Ut{el;open=!1;constructor(t){const s=o("div",{class:"picker-grid kaomoji"},Y.map(c=>K(c,()=>B(t,c)))),n=o("div",{class:"picker-grid emoji hidden"},X.map(c=>K(c,()=>B(t,c)))),i=o("button",{class:"tab on",type:"button"},[a.kaomoji]),r=o("button",{class:"tab",type:"button"},[a.emoji]);i.addEventListener("click",()=>{s.classList.remove("hidden"),n.classList.add("hidden"),i.classList.add("on"),r.classList.remove("on")}),r.addEventListener("click",()=>{n.classList.remove("hidden"),s.classList.add("hidden"),r.classList.add("on"),i.classList.remove("on")}),this.el=o("div",{class:"picker hidden",role:"dialog","aria-label":a.stamps},[o("div",{class:"picker-tabs"},[i,r]),s,n])}toggle(){this.open=!this.open,this.el.classList.toggle("hidden",!this.open)}hide(){this.open=!1,this.el.classList.add("hidden")}}function K(e,t){const s=o("button",{class:"stamp-btn",type:"button"},[e]);return s.addEventListener("click",t),s}const Z="zenchat.theme";function Ft(){const e=localStorage.getItem(Z);return e==="light"||e==="dark"||e==="system"?e:"system"}function Gt(e){localStorage.setItem(Z,e)}function Wt(e){return e==="system"?"light":e==="light"?"dark":"system"}function qt(){return window.matchMedia("(prefers-color-scheme: dark)").matches}function Yt(e,t=qt()){return e==="system"?t?"dark":"light":e}function S(e){const t=Yt(e);return document.documentElement.dataset.theme=t,document.documentElement.dataset.themePref=e,document.documentElement.style.colorScheme=t,t}function Xt(){return typeof RTCPeerConnection=="function"}class Zt{root;theme;identity=U();manager;themeBtn;tabsEl;toolsEl;main;jump;jumpRoom;jumpPass;jumpStrategy;footerEl;chat=null;lobbyEl=null;routing=!1;constructor(t){this.root=t,this.theme=Ft(),S(this.theme),this.themeBtn=o("button",{class:"btn ghost",type:"button",title:"主题"},[D(this.theme)]),this.tabsEl=o("nav",{class:"tabs","aria-label":a.recent}),this.toolsEl=o("div",{class:"toolbar"}),this.main=o("main",{class:"main"}),this.jumpRoom=o("input",{placeholder:a.room,maxlength:64,autocomplete:"off"}),this.jumpPass=o("input",{placeholder:a.password,type:"password",autocomplete:"off"}),this.jumpStrategy=o("select",{},[o("option",{value:"torrent"},[a.torrent]),o("option",{value:"nostr"},[a.nostr])]),this.jump=this.buildJump(),this.footerEl=o("p",{class:"footer"},[o("a",{href:ht,target:"_blank",rel:"noreferrer"},[a.source])," · AGPL-3.0"]),this.manager=new _t(this.identity,{onStatus:s=>this.chat?.status(s),onMembers:s=>this.chat?.members(s,this.manager.getSession()?.selfId??this.identity.id),onLine:s=>this.chat?.log.append(s),onReset:s=>this.chat?.log.reset(s)})}start(){this.mount(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>S(this.theme)),window.addEventListener("hashchange",()=>{this.routing||this.route()}),window.addEventListener("pagehide",()=>{this.manager.close()}),window.addEventListener("keydown",t=>{(t.metaKey||t.ctrlKey)&&t.key.toLowerCase()==="k"&&(t.preventDefault(),this.jump.showModal(),this.jumpRoom.focus()),t.key==="Escape"&&this.jump.close()}),this.themeBtn.addEventListener("click",()=>{this.theme=Wt(this.theme),Gt(this.theme),S(this.theme),this.themeBtn.textContent=D(this.theme)}),this.route()}mount(){this.root.append(o("div",{class:"shell"},[o("header",{class:"topbar"},[o("a",{class:"brand",href:"#/"},[o("div",{class:"enso","aria-hidden":"true"}),o("strong",{},[a.title])]),this.tabsEl,this.toolsEl]),this.capabilityBanner(),this.main,this.footerEl]),this.jump)}capabilityBanner(){return window.isSecureContext?Xt()?o("div"):o("div",{class:"banner"},[a.webrtcMissing]):o("div",{class:"banner"},[a.insecure])}buildJump(){const t=o("form",{class:"jump-form",method:"dialog"},[o("h2",{},[a.switch]),this.jumpRoom,this.jumpPass,this.jumpStrategy,o("div",{class:"actions"},[o("button",{class:"btn primary",type:"submit"},[a.join]),o("button",{class:"btn ghost",type:"button",value:"cancel"},["取消"])])]),s=o("dialog",{class:"jump"},[t]);return t.addEventListener("submit",n=>{n.preventDefault();const i={name:this.jumpRoom.value,password:this.jumpPass.value,strategy:this.jumpStrategy.value};s.close(),this.go(i)}),t.querySelector('button[value="cancel"]')?.addEventListener("click",()=>s.close()),s}async route(){const t=Ot(location.hash);if(this.refreshTabs(),this.refreshTools(t.name==="room"?t.spec:null),t.name==="lobby"){await this.manager.close(),this.showLobby();return}await this.showChat(t.spec)}go(t){const s=b({name:"room",spec:t},!!t.password);if(location.hash===s){this.showChat(t);return}this.routing=!0,location.hash=s,this.routing=!1,this.showChat(t)}showLobby(){this.chat?.hide(),this.footerEl.hidden=!1,this.root.classList.remove("mode-chat"),this.lobbyEl?this.lobbyEl.replaceWith(this.lobbyEl=this.buildLobby()):this.lobbyEl=this.buildLobby(),this.main.replaceChildren(this.lobbyEl),this.refreshTabs()}async showChat(t){const s=this.manager.current();if(this.chat||(this.chat=new Qt({selfId:this.identity.id,send:n=>{this.manager.getSession()?.sendChat(n)},typing:()=>this.manager.getSession()?.sendTyping()})),this.lobbyEl=null,this.footerEl.hidden=!0,this.root.classList.add("mode-chat"),this.main.replaceChildren(this.chat.root),this.chat.show(),this.refreshTabs(t),this.refreshTools(t),!(s&&F(s,t)))try{await this.manager.open(t)}catch(n){const i=n instanceof Error?n.message:"连接失败";this.chat.status({phase:"error",detail:`无法启动 P2P:${i}`,relays:[],peerCount:0})}}buildLobby(){const t=o("input",{maxlength:24,value:this.identity.nick,autocomplete:"nickname"}),s=o("input",{maxlength:64,value:ot,autocomplete:"off"}),n=o("input",{type:"password",autocomplete:"off"}),i=o("select",{},[o("option",{value:"torrent"},[a.torrent]),o("option",{value:"nostr"},[a.nostr])]),r=o("form",{class:"panel form lobby-form"},[o("div",{class:"join-row"},[w(a.nick,t),w(a.room,s),o("button",{class:"btn primary join-btn",type:"submit"},[a.join])]),o("details",{class:"more"},[o("summary",{},[a.more]),o("div",{class:"row"},[w(a.password,n),w(a.strategy,i)]),o("span",{class:"hint"},[a.passwordHint])])]);return r.addEventListener("submit",c=>{c.preventDefault(),this.identity={...this.identity,nick:dt(t.value)},this.manager.setIdentity(this.identity),this.go({name:s.value,password:n.value,strategy:i.value})}),r}refreshTabs(t){const s=W();x(this.tabsEl);for(const i of s){const r={name:i.name,password:"",strategy:i.strategy},c=!!(t&&i.name===p(t.name)&&i.strategy===t.strategy),l=o("a",{class:`tab${c?" on":""}`,href:b({name:"room",spec:r})},[i.name]);l.addEventListener("click",h=>{h.preventDefault(),this.go(te(i,t))}),this.tabsEl.append(l)}const n=o("button",{class:"tab add",type:"button",title:a.addRoom},["+"]);n.addEventListener("click",()=>{this.jump.showModal(),this.jumpRoom.focus()}),this.tabsEl.append(n)}refreshTools(t){if(x(this.toolsEl),this.toolsEl.append(this.themeBtn),!t)return;const s=o("button",{class:"btn ghost",type:"button"},[a.share]);if(s.addEventListener("click",async()=>{await navigator.clipboard.writeText(`${location.origin}${location.pathname}${b({name:"room",spec:t})}`),s.textContent=a.copied,window.setTimeout(()=>{s.textContent=a.share},1e3)}),this.toolsEl.append(s),t.password){const i=o("button",{class:"btn ghost",type:"button"},[a.shareWithKey]);i.addEventListener("click",async()=>{await navigator.clipboard.writeText(`${location.origin}${location.pathname}${b({name:"room",spec:t},!0)}`),i.textContent=a.copied}),this.toolsEl.append(i)}const n=o("a",{class:"btn ghost",href:"#/"},[a.leave]);this.toolsEl.append(n)}}class Qt{root;log=new Jt;statusEl;membersEl;peopleBtn;lastStatus="";constructor(t){this.statusEl=o("div",{class:"status"}),this.membersEl=o("div",{class:"members"}),this.peopleBtn=o("button",{class:"btn ghost people",type:"button"},[a.people]),this.peopleBtn.addEventListener("click",l=>{l.stopPropagation(),this.root.classList.toggle("show-side")});const s=o("textarea",{rows:1,placeholder:a.placeholder,maxlength:4e3,enterkeyhint:"send"});s.setAttribute("enterkeyhint","send"),s.setAttribute("autocapitalize","sentences");const n=new Ut(s),i=o("button",{class:"btn ghost",type:"button",title:a.stamps},["顔"]),r=o("button",{class:"btn primary send",type:"button","aria-label":a.send},["↑"]);i.addEventListener("click",l=>{l.stopPropagation(),n.toggle()}),this.peopleBtn.addEventListener("pointerdown",l=>l.stopPropagation()),document.addEventListener("pointerdown",l=>{this.root.contains(l.target)&&(l.target.closest(".picker, .people, .side")||n.hide())}),n.el.addEventListener("click",l=>l.stopPropagation());const c=()=>{const l=s.value;s.value="",s.style.height="auto",n.hide(),t.send(l)};r.addEventListener("click",c),s.addEventListener("keydown",l=>{l.key==="Enter"&&!l.shiftKey&&(l.preventDefault(),c())}),s.addEventListener("input",()=>{s.style.height="auto",s.style.height=`${Math.min(s.scrollHeight,96)}px`,t.typing()}),this.root=o("div",{class:"chat"},[o("section",{class:"transcript"},[o("div",{class:"status-row"},[this.statusEl,this.peopleBtn]),this.log.el,n.el,o("div",{class:"composer"},[i,s,r])]),o("aside",{class:"side"},[o("h2",{},[a.members]),this.membersEl])]),this.members([],t.selfId)}hide(){this.root.hidden=!0,this.root.classList.remove("show-side")}show(){this.root.hidden=!1}status(t){const s=t.relays.filter(i=>i.readyState===WebSocket.OPEN).length,n=`${t.phase}|${t.detail}|${t.peerCount}|${s}`;n!==this.lastStatus&&(this.lastStatus=n,this.statusEl.replaceChildren(o("i",{class:`dot${t.peerCount>0?" ok":""}`}),o("span",{},[` ${t.detail}`])),this.peopleBtn.textContent=`${a.people} ${t.peerCount}`,this.statusEl.title=t.relays.map(i=>`${Kt(i.url)} ${Bt(i.readyState)}`).join(`
-`))}members(t,s){this.membersEl.replaceChildren(o("div",{class:"member"},[o("b",{},[a.you]),o("span",{class:"muted"},[s.slice(0,8)])]),...t.map(n=>o("div",{class:"member"},[o("b",{style:`color:${J(n.id)}`},[n.nick]),o("span",{class:"muted"},[n.typing?"输入中":n.rttMs!==null?`${Math.round(n.rttMs)}ms`:""])])))}}function w(e,t){return o("label",{class:"field"},[o("span",{},[e]),t])}function D(e){return e==="light"?"浅色":e==="dark"?"深色":"自动"}function te(e,t){const s=t&&e.name===p(t.name)&&e.strategy===t.strategy?t.password:"";return{name:e.name,password:s,strategy:e.strategy}}function ee(){const e=window.visualViewport,t=()=>{const s=e?.height??window.innerHeight;document.documentElement.style.setProperty("--app-h",`${Math.round(s)}px`)};t(),e?.addEventListener("resize",t,{passive:!0}),e?.addEventListener("scroll",t,{passive:!0}),window.addEventListener("orientationchange",t,{passive:!0})}const Q=document.querySelector("#app");if(!Q)throw new Error("缺少 #app 挂载点");ee();$t();new Zt(Q).start();
diff --git a/assets/index-Dl0q0iBn.js b/assets/index-Dl0q0iBn.js
new file mode 100644
index 0000000..8ec4fd0
--- /dev/null
+++ b/assets/index-Dl0q0iBn.js
@@ -0,0 +1,3 @@
+const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-BgLl3BAq.js","assets/strategy-C52a_06u.js","assets/index-DSJFV8_4.js"])))=>i.map(i=>d[i]);
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const c of r.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&n(c)}).observe(document,{childList:!0,subtree:!0});function s(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function n(i){if(i.ep)return;i.ep=!0;const r=s(i);fetch(i.href,r)}})();const nt="zenchat.andyccr.v1",m=1,it=24,ot=64,V=4e3,J=10,v=200,rt="lobby",at=2500,lt=1800,ct=25e3,ht=5e3,dt="https://github.com/Andyccr/ZenChat",k="zenchat.nick",P="zenchat.peerId",j=["闲云","青石","疏影","晚风","折柳","听雨","归舟","松风"];function L(e){const t=new Uint8Array(e);return crypto.getRandomValues(t),[...t].map(s=>s.toString(16).padStart(2,"0")).join("")}const I=["#c15f3c","#8b5e3c","#5c6b4a","#6b4f3a","#9a6b3f","#4a6b62","#7a4e3a","#5a5568"];function z(e){let t=2166136261;for(let s=0;s{const i=n.codePointAt(0)??0;return i>=32&&i!==127}).join("").slice(0,it)}function U(e){const t=Number.parseInt(e.slice(0,4),16)%j.length;return j[t]??"闲云"}function F(){const e=sessionStorage.getItem(P),t=e&&/^[0-9a-f]{16,}$/i.test(e)?e:L(12);sessionStorage.setItem(P,t);const s=localStorage.getItem(k),n=y(s??"")||U(t);return localStorage.setItem(k,n),{id:t,nick:n}}function ut(e){const t=y(e)||U(F().id);return localStorage.setItem(k,t),t}function mt(e){return e==="torrent"||e==="nostr"}function p(e){return e.normalize("NFKC").trim().replace(/\s+/g,"-").replace(/[/#?&]+/g,"").slice(0,ot)}function pt(e){const t=p(e.name);return e.password?`${t}::${e.password}`:t}function M(e){return`${e.strategy}:${p(e.name)}`}function T(e){return`${e.strategy}:${p(e.name)}:${e.password}`}function G(e,t){return T(e)===T(t)}const W="zenchat.recentRooms";function q(){try{const e=localStorage.getItem(W);if(!e)return[];const t=JSON.parse(e);return Array.isArray(t)?t.filter(s=>typeof s=="object"&&s!==null&&typeof s.name=="string"&&(s.strategy==="torrent"||s.strategy==="nostr")&&typeof s.hasPassword=="boolean"&&typeof s.visitedAt=="number").slice(0,J):[]}catch{return[]}}function ft(e){const t={name:e.name,strategy:e.strategy,hasPassword:!!e.password,visitedAt:Date.now()},s=q().filter(i=>M(i)!==M(e)),n=[t,...s].slice(0,J);return localStorage.setItem(W,JSON.stringify(n)),n}const yt="zenchat.cache.v1.";function Y(e){return yt+T(e)}function gt(e){try{const t=sessionStorage.getItem(Y(e));if(!t)return[];const s=JSON.parse(t);return Array.isArray(s)?s.filter(wt).slice(-v):[]}catch{return[]}}function bt(e,t){try{sessionStorage.setItem(Y(e),JSON.stringify(t.slice(-v)))}catch{}}function A(e){return e.length>v?e.slice(-v):e}function wt(e){if(typeof e!="object"||e===null)return!1;const t=e;return t.kind==="system"?typeof t.id=="string"&&typeof t.text=="string"&&typeof t.ts=="number":t.kind==="chat"?typeof t.id=="string"&&typeof t.fromId=="string"&&typeof t.nick=="string"&&typeof t.text=="string"&&typeof t.ts=="number"&&typeof t.self=="boolean":!1}function vt(e){return typeof e=="object"&&e!==null}function Et(e){return e===m}function St(e,t,s,n){return{v:m,type:"chat",id:s,ts:n,nick:y(e),text:t.trim().slice(0,V)}}function E(e){return{v:m,type:"hello",nick:y(e)}}function kt(e){return{v:m,type:"typing",nick:y(e)}}function Lt(e){if(!vt(e)||!Et(e.v)||typeof e.type!="string")return null;const t=typeof e.nick=="string"?y(e.nick):"";if(!t)return null;if(e.type==="hello")return{v:m,type:"hello",nick:t};if(e.type==="typing")return{v:m,type:"typing",nick:t};if(e.type==="chat"){if(typeof e.id!="string"||e.id.length<8||e.id.length>80||typeof e.ts!="number"||!Number.isFinite(e.ts)||typeof e.text!="string")return null;const s=e.text.trim().slice(0,V);return s?{v:m,type:"chat",id:e.id,ts:e.ts,nick:t,text:s}:null}return null}function Tt(e,t){let s=0;return(...n)=>{const i=Date.now();i-s{s=n,!t&&(t=requestAnimationFrame(()=>{t=0;const i=s;s=null,i&&e(...i)}))}}const Ct="modulepreload",Pt=function(e){return"/ZenChat/"+e},$={},O=function(t,s,n){let i=Promise.resolve();if(s&&s.length>0){let C=function(d){return Promise.all(d.map(f=>Promise.resolve(f).then(g=>({status:"fulfilled",value:g}),g=>({status:"rejected",reason:g}))))};var c=C;document.getElementsByTagName("link");const l=document.querySelector("meta[property=csp-nonce]"),h=l?.nonce||l?.getAttribute("nonce");i=C(s.map(d=>{if(d=Pt(d),d in $)return;$[d]=!0;const f=d.endsWith(".css"),g=f?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${d}"]${g}`))return;const u=document.createElement("link");if(u.rel=f?"stylesheet":Ct,f||(u.as="script"),u.crossOrigin="",u.href=d,h&&u.setAttribute("nonce",h),document.head.appendChild(u),f)return new Promise((et,st)=>{u.addEventListener("load",et),u.addEventListener("error",()=>st(new Error(`Unable to preload CSS for ${d}`)))})}))}function r(l){const h=new Event("vite:preloadError",{cancelable:!0});if(h.payload=l,window.dispatchEvent(h),!h.defaultPrevented)throw l}return i.then(l=>{for(const h of l||[])h.status==="rejected"&&r(h.reason);return t().catch(r)})},jt=[{urls:"stun:stun.cloudflare.com:3478"},{urls:"stun:stun.l.google.com:19302"}],It={iceServers:jt,iceCandidatePoolSize:8,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},Mt=["wss://tracker.webtorrent.dev","wss://tracker.openwebtorrent.com","wss://open.ftorrent.com:443"],At=["wss://relay.damus.io","wss://nos.lol","wss://relay.snort.social","wss://nostr.wine"];function $t(e){return Object.entries(e).map(([t,s])=>({url:t,readyState:s?.readyState??WebSocket.CLOSED}))}class _{strategy;room=null;action=null;module=null;constructor(t){this.strategy=t}selfId(){return this.module?.selfId??""}async join(t,s){await this.leave(),this.module=await R(t.strategy);const n=t.strategy==="torrent"?[...Mt]:[...At],i=this.module.joinRoom({appId:t.appId,...t.password?{password:t.password}:{},rtcConfig:It,trickleIce:!0,relayConfig:{urls:n}},t.roomId,{onJoinError:c=>{s.onJoinError(c.error||"无法与对等节点完成握手")}}),r=i.makeAction("zen");r.onMessage=(c,{peerId:l})=>{s.onPayload(l,c)},i.onPeerJoin=c=>s.onPeerJoin(c),i.onPeerLeave=c=>s.onPeerLeave(c),this.room=i,this.action=r}async leave(){const t=this.room;if(this.room=null,this.action=null,t)try{await t.leave()}catch{}}async send(t,s){if(!this.action)throw new Error("尚未加入房间");await this.action.send(t,s?{target:s}:void 0)}ping(t){return this.room?this.room.ping(t):Promise.reject(new Error("未连接"))}peerIds(){return this.room?Object.keys(this.room.getPeers()):[]}relays(){return this.module?$t(this.module.getRelaySockets()):[]}}const N=new Map;async function R(e){const t=N.get(e);if(t)return t;const s=e==="torrent"?O(()=>import("./index-BgLl3BAq.js"),__vite__mapDeps([0,1])).then(n=>({joinRoom:n.joinRoom,getRelaySockets:n.getRelaySockets,selfId:n.selfId})):O(()=>import("./index-DSJFV8_4.js"),__vite__mapDeps([2,1])).then(n=>({joinRoom:n.joinRoom,getRelaySockets:n.getRelaySockets,selfId:n.selfId}));return N.set(e,s),s}function Ot(){R("torrent"),R("nostr")}class _t{transport;identity;members=new Map;lines=[];seen=new Set;listeners;helloTimer=null;relayTimer=null;typingTimers=new Map;paused=!1;joined=!1;status={phase:"idle",detail:"尚未连接",relays:[],peerCount:0};emitMembers=Rt(()=>{this.listeners.onMembers?.([...this.members.values()].sort((t,s)=>t.joinedAt-s.joinedAt))});sendTypingThrottled=Tt(()=>{this.transport.send(kt(this.identity.nick))},lt);constructor(t,s,n){this.identity=t,this.listeners=s,this.transport=n??new _("torrent")}get selfId(){return this.transport.selfId()||this.identity.id}getLines(){return this.lines}hydrate(t){this.lines=A(t);for(const s of this.lines)s.kind==="chat"&&this.seen.add(s.id);this.listeners.onReset?.(this.lines)}async join(t){this.joined&&await this.leave({silent:!0}),this.transport=new _(t.strategy),this.joined=!0,this.setStatus({phase:"connecting",detail:t.strategy==="torrent"?"正在连接 Tracker…":"正在连接 Nostr…",relays:[],peerCount:0}),await this.transport.join({appId:nt,roomId:pt({name:p(t.name),password:t.password}),password:t.password,strategy:t.strategy},{onPeerJoin:s=>{this.upsertMember(s,"访客"),this.pushSystem(`${s.slice(0,6)} 加入了房间`),this.transport.send(E(this.identity.nick),s),this.emitMembers(),this.refreshStatus("connected",`直连 ${this.transport.peerIds().length}`),window.setTimeout(()=>{this.measure(s)},350)},onPeerLeave:s=>{const n=this.members.get(s);this.members.delete(s),this.pushSystem(`${n?.nick??s.slice(0,6)} 离开了房间`),this.emitMembers();const i=this.transport.peerIds().length;this.refreshStatus(i>0?"connected":"connecting",i>0?`直连 ${i}`:"等待同伴")},onPayload:(s,n)=>this.handlePayload(s,n),onJoinError:s=>{this.refreshStatus("connecting",`握手受阻:${s}`)}}),this.bindVisibility(),this.startTimers(),this.refreshStatus("connecting","已宣布,等待对等节点")}async sendChat(t){const s=St(this.identity.nick,t,L(8),Date.now());s.text&&(this.seen.add(s.id),this.pushLine({kind:"chat",id:s.id,fromId:this.selfId,nick:this.identity.nick,text:s.text,ts:s.ts,self:!0}),this.transport.send(s))}sendTyping(){this.sendTypingThrottled()}setNick(t){this.identity={...this.identity,nick:t},this.transport.send(E(t))}async leave(t={}){this.joined=!1,this.unbindVisibility(),this.stopTimers();for(const s of this.typingTimers.values())window.clearTimeout(s);this.typingTimers.clear(),await this.transport.leave(),this.members.clear(),this.lines=[],this.seen.clear(),t.silent||(this.setStatus({phase:"idle",detail:"已离开房间",relays:[],peerCount:0}),this.emitMembers(),this.listeners.onReset?.([]))}handlePayload(t,s){const n=Lt(s);if(n){if(this.upsertMember(t,n.nick),n.type==="hello"){this.emitMembers();return}if(n.type==="typing"){this.setTyping(t),this.emitMembers();return}this.seen.has(n.id)||(this.seen.add(n.id),this.pushLine({kind:"chat",id:n.id,fromId:t,nick:n.nick,text:n.text,ts:n.ts,self:!1}),this.emitMembers())}}upsertMember(t,s){const n=this.members.get(t),i=Date.now();this.members.set(t,{id:t,nick:s,joinedAt:n?.joinedAt??i,lastSeenAt:i,rttMs:n?.rttMs??null,typing:n?.typing??!1})}setTyping(t){const s=this.members.get(t);if(!s)return;s.typing=!0;const n=this.typingTimers.get(t);n&&window.clearTimeout(n),this.typingTimers.set(t,window.setTimeout(()=>{const i=this.members.get(t);i&&(i.typing=!1,this.emitMembers())},at))}async measure(t){try{const s=await this.transport.ping(t),n=this.members.get(t);n&&(n.rttMs=s,this.emitMembers())}catch{}}pushSystem(t){this.pushLine({kind:"system",id:L(6),text:t,ts:Date.now()})}pushLine(t){this.lines.push(t),this.lines=A(this.lines),this.listeners.onLine?.(t)}refreshStatus(t,s){this.setStatus({phase:t,detail:s,relays:this.transport.relays(),peerCount:this.transport.peerIds().length})}setStatus(t){const s=this.status;s.phase===t.phase&&s.detail===t.detail&&s.peerCount===t.peerCount&&s.relays.length===t.relays.length&&s.relays.every((n,i)=>n.readyState===t.relays[i]?.readyState)||(this.status=t,this.listeners.onStatus?.(t))}onVisibility=()=>{document.hidden?this.stopTimers():this.joined&&this.startTimers()};bindVisibility(){document.addEventListener("visibilitychange",this.onVisibility)}unbindVisibility(){document.removeEventListener("visibilitychange",this.onVisibility)}startTimers(){this.stopTimers(),this.helloTimer=window.setInterval(()=>{this.paused||this.transport.send(E(this.identity.nick))},ct),this.relayTimer=window.setInterval(()=>{this.refreshStatus(this.status.phase,this.status.detail)},ht)}stopTimers(){this.helloTimer!==null&&(window.clearInterval(this.helloTimer),this.helloTimer=null),this.relayTimer!==null&&(window.clearInterval(this.relayTimer),this.relayTimer=null),this.paused=document.hidden}}class Nt{identity;listeners;session=null;spec=null;generation=0;constructor(t,s){this.identity=t,this.listeners=s}current(){return this.spec}getSession(){return this.session}setIdentity(t){this.identity=t,this.session?.setNick(t.nick)}async open(t){if(this.spec&&this.session&&G(this.spec,t))return this.session;const s=++this.generation;if(await this.snapshotAndClose(),s!==this.generation)return null;ft(t),this.listeners.onMembers?.([]);const n=new _t(this.identity,this.listeners),i=gt(t);return i.length>0?n.hydrate(i):this.listeners.onReset?.([]),this.session=n,this.spec=t,await n.join(t),s!==this.generation?(await n.leave({silent:!0}),null):n}async close(){this.generation+=1,await this.snapshotAndClose()}async snapshotAndClose(){this.session&&this.spec&&(bt(this.spec,this.session.getLines()),await this.session.leave({silent:!0})),this.session=null,this.spec=null}}function x(e,t){return e.get(t)??""}function xt(e){const t=e.startsWith("#")?e.slice(1):e,s=new URL(t||"/","https://zenchat.local"),n=s.pathname.split("/").filter(Boolean);if(n[0]==="r"&&n[1]){const i=p(decodeURIComponent(n[1]));if(!i)return{name:"lobby"};const r=x(s.searchParams,"s"),c=mt(r)?r:"torrent";return{name:"room",spec:{name:i,password:x(s.searchParams,"k"),strategy:c}}}return{name:"lobby"}}function b(e,t=!1){if(e.name==="lobby")return"#/";const s=new URLSearchParams;s.set("s",e.spec.strategy),t&&e.spec.password&&s.set("k",e.spec.password);const n=s.toString();return`#/r/${encodeURIComponent(e.spec.name)}${n?`?${n}`:""}`}const a={title:"禅聊",tagline:"无服务器 · 浏览器直连",source:"源码",nick:"昵称",room:"房间",password:"口令",passwordHint:"口令只隔离房间,不会上传。",strategy:"信令",torrent:"Tracker",nostr:"Nostr",join:"加入",more:"更多选项",lobbyHint:"同一房间即可直连,无需账号。",recent:"最近",share:"分享",shareWithKey:"含口令",leave:"大厅",send:"发送",placeholder:"发消息",members:"在线",you:"你",copied:"已复制",webrtcMissing:"需要支持 WebRTC 的浏览器,并用 HTTPS 打开。",insecure:"需要 HTTPS 或 localhost。",switch:"切换房间",addRoom:"加入房间",stamps:"表情",kaomoji:"颜文字",emoji:"表情",people:"成员"};function o(e,t={},s=[]){const n=document.createElement(e);for(const[i,r]of Object.entries(t))if(!(r===void 0||r===!1)){if(i==="class"){n.className=String(r);continue}if(typeof r=="boolean"){n.toggleAttribute(i,r);continue}if(i==="style"){n.setAttribute("style",String(r));continue}if(i in n&&i!=="list"){n[i]=r;continue}n.setAttribute(i,String(r))}for(const i of s)n.append(typeof i=="string"?document.createTextNode(i):i);return n}function B(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function Bt(e){return new Intl.DateTimeFormat("zh-CN",{hour:"2-digit",minute:"2-digit",hour12:!1}).format(e)}function Kt(e){switch(e){case WebSocket.CONNECTING:return"连接中";case WebSocket.OPEN:return"已连接";case WebSocket.CLOSING:return"断开中";default:return"未连接"}}function Ht(e){try{return new URL(e).host}catch{return e}}const X=["(´・ω・`)","(。•́︿•̀。)","(´▽`ʃ♡ƪ)","(╯°□°)╯︵ ┻━┻","¯\\_(ツ)_/¯","(づ。◕‿‿◕。)づ","(ง'̀-'́)ง","( ̄▽ ̄)","(•̀ᴗ•́)و","ฅ^•ﻌ•^ฅ","(´;ω;`)","(≧∇≦)ノ","(´∀`)♡","┐( ̄ヘ ̄)┌"],Z=["😀","😂","🥰","😎","🤔","😭","🙏","✨","🔥","🎉","👍","👋","❤️","🌸","🍵","🌙"],Dt=new Set(X),Vt=new Set(Z);function Jt(e){const t=e.trim();return t?Dt.has(t)||Vt.has(t)?!0:t.length<=8&&/^[\p{Emoji_Presentation}\s]+$/u.test(t):!1}function K(e,t){const s=e.selectionStart,n=e.selectionEnd,i=`${e.value.slice(0,s)}${t}${e.value.slice(n)}`;e.value=i;const r=s+t.length;e.setSelectionRange(r,r),e.focus(),e.dispatchEvent(new Event("input",{bubbles:!0}))}class zt{el;ids=new Set;constructor(){this.el=o("div",{class:"log",role:"log","aria-live":"polite"})}reset(t){this.el.replaceChildren(),this.ids.clear();for(const s of t)this.append(s,!1);this.stick()}append(t,s=!0){this.ids.has(t.id)||(this.ids.add(t.id),this.el.append(Ut(t)),s&&this.stick())}stick(){const t=this.el;requestAnimationFrame(()=>{t.scrollHeight-t.scrollTop-t.clientHeight<96&&(t.scrollTop=t.scrollHeight)})}}function Ut(e){if(e.kind==="system")return o("div",{class:"system"},[e.text]);const t=Jt(e.text);return o("article",{class:`bubble${e.self?" self":""}${t?" stamp":""}`},[o("header",{},[o("b",{style:`color:${z(e.fromId)}`},[e.nick]),o("time",{},[Bt(e.ts)])]),o("p",{},[e.text])])}class Ft{el;open=!1;constructor(t){const s=o("div",{class:"picker-grid kaomoji"},X.map(c=>H(c,()=>K(t,c)))),n=o("div",{class:"picker-grid emoji hidden"},Z.map(c=>H(c,()=>K(t,c)))),i=o("button",{class:"tab on",type:"button"},[a.kaomoji]),r=o("button",{class:"tab",type:"button"},[a.emoji]);i.addEventListener("click",()=>{s.classList.remove("hidden"),n.classList.add("hidden"),i.classList.add("on"),r.classList.remove("on")}),r.addEventListener("click",()=>{n.classList.remove("hidden"),s.classList.add("hidden"),r.classList.add("on"),i.classList.remove("on")}),this.el=o("div",{class:"picker hidden",role:"dialog","aria-label":a.stamps},[o("div",{class:"picker-tabs"},[i,r]),s,n])}toggle(){this.open=!this.open,this.el.classList.toggle("hidden",!this.open)}hide(){this.open=!1,this.el.classList.add("hidden")}}function H(e,t){const s=o("button",{class:"stamp-btn",type:"button"},[e]);return s.addEventListener("click",t),s}const Q="zenchat.theme";function Gt(){const e=localStorage.getItem(Q);return e==="light"||e==="dark"||e==="system"?e:"system"}function Wt(e){localStorage.setItem(Q,e)}function qt(e){return e==="system"?"light":e==="light"?"dark":"system"}function Yt(){return window.matchMedia("(prefers-color-scheme: dark)").matches}function Xt(e,t=Yt()){return e==="system"?t?"dark":"light":e}function S(e){const t=Xt(e);return document.documentElement.dataset.theme=t,document.documentElement.dataset.themePref=e,document.documentElement.style.colorScheme=t,t}function D(e){return e==="light"?"白天":e==="dark"?"黑夜":"跟随系统"}function Zt(){return typeof RTCPeerConnection=="function"}class Qt{root;theme;identity=F();manager;themeBtn;tabsEl;toolsEl;main;jump;jumpRoom;jumpPass;jumpStrategy;footerEl;chat=null;lobbyEl=null;routing=!1;constructor(t){this.root=t,this.theme=Gt(),S(this.theme),this.themeBtn=o("button",{class:"btn ghost",type:"button",title:"主题"},[D(this.theme)]),this.tabsEl=o("nav",{class:"tabs","aria-label":a.recent}),this.toolsEl=o("div",{class:"toolbar"}),this.main=o("main",{class:"main"}),this.jumpRoom=o("input",{placeholder:a.room,maxlength:64,autocomplete:"off"}),this.jumpPass=o("input",{placeholder:a.password,type:"password",autocomplete:"off"}),this.jumpStrategy=o("select",{},[o("option",{value:"torrent"},[a.torrent]),o("option",{value:"nostr"},[a.nostr])]),this.jump=this.buildJump(),this.footerEl=o("p",{class:"footer"},[o("a",{href:dt,target:"_blank",rel:"noreferrer"},[a.source])," · AGPL-3.0"]),this.manager=new Nt(this.identity,{onStatus:s=>this.chat?.status(s),onMembers:s=>this.chat?.members(s,this.manager.getSession()?.selfId??this.identity.id),onLine:s=>this.chat?.log.append(s),onReset:s=>this.chat?.log.reset(s)})}start(){this.mount(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>S(this.theme)),window.addEventListener("hashchange",()=>{this.routing||this.route()}),window.addEventListener("pagehide",()=>{this.manager.close()}),window.addEventListener("keydown",t=>{(t.metaKey||t.ctrlKey)&&t.key.toLowerCase()==="k"&&(t.preventDefault(),this.jump.showModal(),this.jumpRoom.focus()),t.key==="Escape"&&this.jump.close()}),this.themeBtn.addEventListener("click",()=>{this.theme=qt(this.theme),Wt(this.theme),S(this.theme),this.themeBtn.textContent=D(this.theme)}),this.route()}mount(){this.root.append(o("div",{class:"shell"},[o("header",{class:"topbar"},[o("a",{class:"brand",href:"#/"},[o("div",{class:"enso","aria-hidden":"true"}),o("strong",{},[a.title]),o("span",{class:"tagline"},[a.tagline])]),this.tabsEl,this.toolsEl]),this.capabilityBanner(),this.main,this.footerEl]),this.jump)}capabilityBanner(){return window.isSecureContext?Zt()?o("div"):o("div",{class:"banner"},[a.webrtcMissing]):o("div",{class:"banner"},[a.insecure])}buildJump(){const t=o("form",{class:"jump-form",method:"dialog"},[o("h2",{},[a.switch]),this.jumpRoom,this.jumpPass,this.jumpStrategy,o("div",{class:"actions"},[o("button",{class:"btn primary",type:"submit"},[a.join]),o("button",{class:"btn ghost",type:"button",value:"cancel"},["取消"])])]),s=o("dialog",{class:"jump"},[t]);return t.addEventListener("submit",n=>{n.preventDefault();const i={name:this.jumpRoom.value,password:this.jumpPass.value,strategy:this.jumpStrategy.value};s.close(),this.go(i)}),t.querySelector('button[value="cancel"]')?.addEventListener("click",()=>s.close()),s}async route(){const t=xt(location.hash);if(this.refreshTabs(),this.refreshTools(t.name==="room"?t.spec:null),t.name==="lobby"){await this.manager.close(),this.showLobby();return}await this.showChat(t.spec)}go(t){const s=b({name:"room",spec:t},!!t.password);if(location.hash===s){this.showChat(t);return}this.routing=!0,location.hash=s,this.routing=!1,this.showChat(t)}showLobby(){this.chat?.hide(),this.footerEl.hidden=!1,this.root.classList.remove("mode-chat"),this.lobbyEl?this.lobbyEl.replaceWith(this.lobbyEl=this.buildLobby()):this.lobbyEl=this.buildLobby(),this.main.replaceChildren(this.lobbyEl),this.refreshTabs()}async showChat(t){const s=this.manager.current();if(this.chat||(this.chat=new te({selfId:this.identity.id,send:n=>{this.manager.getSession()?.sendChat(n)},typing:()=>this.manager.getSession()?.sendTyping()})),this.lobbyEl=null,this.footerEl.hidden=!0,this.root.classList.add("mode-chat"),this.main.replaceChildren(this.chat.root),this.chat.show(),this.refreshTabs(t),this.refreshTools(t),!(s&&G(s,t)))try{await this.manager.open(t)}catch(n){const i=n instanceof Error?n.message:"连接失败";this.chat.status({phase:"error",detail:`无法启动 P2P:${i}`,relays:[],peerCount:0})}}buildLobby(){const t=o("input",{maxlength:24,value:this.identity.nick,autocomplete:"nickname"}),s=o("input",{maxlength:64,value:rt,autocomplete:"off"}),n=o("input",{type:"password",autocomplete:"off"}),i=o("select",{},[o("option",{value:"torrent"},[a.torrent]),o("option",{value:"nostr"},[a.nostr])]),r=o("form",{class:"panel form lobby-form"},[o("div",{class:"join-row"},[w(a.nick,t),w(a.room,s),o("button",{class:"btn primary join-btn",type:"submit"},[a.join])]),o("details",{class:"more"},[o("summary",{},[a.more]),o("div",{class:"row"},[w(a.password,n),w(a.strategy,i)]),o("span",{class:"hint"},[a.passwordHint])])]);return r.addEventListener("submit",c=>{c.preventDefault(),this.identity={...this.identity,nick:ut(t.value)},this.manager.setIdentity(this.identity),this.go({name:s.value,password:n.value,strategy:i.value})}),o("div",{class:"lobby"},[o("p",{class:"lede"},[a.lobbyHint]),r])}refreshTabs(t){const s=q();B(this.tabsEl);for(const i of s){const r={name:i.name,password:"",strategy:i.strategy},c=!!(t&&i.name===p(t.name)&&i.strategy===t.strategy),l=o("a",{class:`tab${c?" on":""}`,href:b({name:"room",spec:r})},[i.name]);l.addEventListener("click",h=>{h.preventDefault(),this.go(ee(i,t))}),this.tabsEl.append(l)}const n=o("button",{class:"tab add",type:"button",title:a.addRoom},["+"]);n.addEventListener("click",()=>{this.jump.showModal(),this.jumpRoom.focus()}),this.tabsEl.append(n)}refreshTools(t){if(B(this.toolsEl),this.toolsEl.append(this.themeBtn),!t)return;const s=o("button",{class:"btn ghost",type:"button"},[a.share]);if(s.addEventListener("click",async()=>{await navigator.clipboard.writeText(`${location.origin}${location.pathname}${b({name:"room",spec:t})}`),s.textContent=a.copied,window.setTimeout(()=>{s.textContent=a.share},1e3)}),this.toolsEl.append(s),t.password){const i=o("button",{class:"btn ghost",type:"button"},[a.shareWithKey]);i.addEventListener("click",async()=>{await navigator.clipboard.writeText(`${location.origin}${location.pathname}${b({name:"room",spec:t},!0)}`),i.textContent=a.copied}),this.toolsEl.append(i)}const n=o("a",{class:"btn ghost",href:"#/"},[a.leave]);this.toolsEl.append(n)}}class te{root;log=new zt;statusEl;membersEl;peopleBtn;lastStatus="";constructor(t){this.statusEl=o("div",{class:"status"}),this.membersEl=o("div",{class:"members"}),this.peopleBtn=o("button",{class:"btn ghost people",type:"button"},[a.people]),this.peopleBtn.addEventListener("click",l=>{l.stopPropagation(),this.root.classList.toggle("show-side")});const s=o("textarea",{rows:1,placeholder:a.placeholder,maxlength:4e3,enterkeyhint:"send"});s.setAttribute("enterkeyhint","send"),s.setAttribute("autocapitalize","sentences");const n=new Ft(s),i=o("button",{class:"btn ghost",type:"button",title:a.stamps},["顔"]),r=o("button",{class:"btn primary send",type:"button","aria-label":a.send},["↑"]);i.addEventListener("click",l=>{l.stopPropagation(),n.toggle()}),this.peopleBtn.addEventListener("pointerdown",l=>l.stopPropagation()),document.addEventListener("pointerdown",l=>{this.root.contains(l.target)&&(l.target.closest(".picker, .people, .side")||n.hide())}),n.el.addEventListener("click",l=>l.stopPropagation());const c=()=>{const l=s.value;s.value="",s.style.height="auto",n.hide(),t.send(l)};r.addEventListener("click",c),s.addEventListener("keydown",l=>{l.key==="Enter"&&!l.shiftKey&&(l.preventDefault(),c())}),s.addEventListener("input",()=>{s.style.height="auto",s.style.height=`${Math.min(s.scrollHeight,96)}px`,t.typing()}),this.root=o("div",{class:"chat"},[o("section",{class:"transcript"},[o("div",{class:"status-row"},[this.statusEl,this.peopleBtn]),this.log.el,n.el,o("div",{class:"composer"},[i,s,r])]),o("aside",{class:"side"},[o("h2",{},[a.members]),this.membersEl])]),this.members([],t.selfId)}hide(){this.root.hidden=!0,this.root.classList.remove("show-side")}show(){this.root.hidden=!1}status(t){const s=t.relays.filter(i=>i.readyState===WebSocket.OPEN).length,n=`${t.phase}|${t.detail}|${t.peerCount}|${s}`;n!==this.lastStatus&&(this.lastStatus=n,this.statusEl.replaceChildren(o("i",{class:`dot${t.peerCount>0?" ok":""}`}),o("span",{},[` ${t.detail}`])),this.peopleBtn.textContent=`${a.people} ${t.peerCount}`,this.statusEl.title=t.relays.map(i=>`${Ht(i.url)} ${Kt(i.readyState)}`).join(`
+`))}members(t,s){this.membersEl.replaceChildren(o("div",{class:"member"},[o("b",{},[a.you]),o("span",{class:"muted"},[s.slice(0,8)])]),...t.map(n=>o("div",{class:"member"},[o("b",{style:`color:${z(n.id)}`},[n.nick]),o("span",{class:"muted"},[n.typing?"输入中":n.rttMs!==null?`${Math.round(n.rttMs)}ms`:""])])))}}function w(e,t){return o("label",{class:"field"},[o("span",{},[e]),t])}function ee(e,t){const s=t&&e.name===p(t.name)&&e.strategy===t.strategy?t.password:"";return{name:e.name,password:s,strategy:e.strategy}}function se(){const e=window.visualViewport,t=()=>{const s=e?.height??window.innerHeight;document.documentElement.style.setProperty("--app-h",`${Math.round(s)}px`)};t(),e?.addEventListener("resize",t,{passive:!0}),e?.addEventListener("scroll",t,{passive:!0}),window.addEventListener("orientationchange",t,{passive:!0})}const tt=document.querySelector("#app");if(!tt)throw new Error("缺少 #app 挂载点");se();Ot();new Qt(tt).start();
diff --git a/favicon.svg b/favicon.svg
index 27b7ceb..3d7fd1a 100644
--- a/favicon.svg
+++ b/favicon.svg
@@ -1,4 +1,4 @@
diff --git a/index.html b/index.html
index 51283fc..57485d4 100644
--- a/index.html
+++ b/index.html
@@ -10,8 +10,8 @@
/>
禅聊 ZenChat
-
-
+
+
diff --git a/public/favicon.svg b/public/favicon.svg
index 27b7ceb..3d7fd1a 100644
--- a/public/favicon.svg
+++ b/public/favicon.svg
@@ -1,4 +1,4 @@
diff --git a/src/core/identity.test.ts b/src/core/identity.test.ts
index aa15791..ab347a4 100644
--- a/src/core/identity.test.ts
+++ b/src/core/identity.test.ts
@@ -7,9 +7,10 @@ describe('identity helpers', () => {
expect(sanitizeNick('x'.repeat(40)).length).toBe(24)
})
- it('maps ids to a stable fallback nick and color', () => {
+ it('maps ids to a stable fallback nick and earth-tone color', () => {
expect(defaultNick('abcd')).toBe(defaultNick('abcd'))
- expect(colorFromId('peer-a')).toMatch(/^hsl\(\d+ 42% 46%\)$/)
- expect(colorFromId('peer-a')).not.toBe(colorFromId('peer-b'))
+ expect(colorFromId('peer-a')).toMatch(/^#[0-9a-f]{6}$/)
+ expect(colorFromId('peer-a')).toBe(colorFromId('peer-a'))
+ expect(new Set(['peer-a', 'peer-b', 'peer-c', 'peer-d', 'peer-e'].map(colorFromId)).size).toBeGreaterThan(1)
})
})
diff --git a/src/core/identity.ts b/src/core/identity.ts
index 3b131d1..372537a 100644
--- a/src/core/identity.ts
+++ b/src/core/identity.ts
@@ -21,14 +21,15 @@ export function randomHex(bytes: number): string {
return [...buf].map((b) => b.toString(16).padStart(2, '0')).join('')
}
+const EARTH_COLORS = ['#c15f3c', '#8b5e3c', '#5c6b4a', '#6b4f3a', '#9a6b3f', '#4a6b62', '#7a4e3a', '#5a5568']
+
export function colorFromId(id: string): string {
let hash = 2166136261
for (let i = 0; i < id.length; i += 1) {
hash ^= id.charCodeAt(i)
hash = Math.imul(hash, 16777619)
}
- const hue = Math.abs(hash) % 360
- return `hsl(${hue} 42% 46%)`
+ return EARTH_COLORS[Math.abs(hash) % EARTH_COLORS.length] ?? '#c15f3c'
}
export function sanitizeNick(raw: string): string {
diff --git a/src/ui/app.ts b/src/ui/app.ts
index 81a4ee2..fd4dbe2 100644
--- a/src/ui/app.ts
+++ b/src/ui/app.ts
@@ -9,7 +9,7 @@ import { copy } from './copy'
import { el, empty, hostOf, wsLabel } from './dom'
import { LogView } from './log-view'
import { StampPicker } from './picker'
-import { applyTheme, cycleTheme, loadThemePreference, persistThemePreference } from './theme'
+import { applyTheme, cycleTheme, loadThemePreference, persistThemePreference, themeLabel } from './theme'
function webrtcReady(): boolean {
return typeof RTCPeerConnection === 'function'
@@ -37,7 +37,7 @@ export class App {
this.root = root
this.theme = loadThemePreference()
applyTheme(this.theme)
- this.themeBtn = el('button', { class: 'btn ghost', type: 'button', title: '主题' }, [themeIcon(this.theme)])
+ this.themeBtn = el('button', { class: 'btn ghost', type: 'button', title: '主题' }, [themeLabel(this.theme)])
this.tabsEl = el('nav', { class: 'tabs', 'aria-label': copy.recent })
this.toolsEl = el('div', { class: 'toolbar' })
this.main = el('main', { class: 'main' })
@@ -81,7 +81,7 @@ export class App {
this.theme = cycleTheme(this.theme)
persistThemePreference(this.theme)
applyTheme(this.theme)
- this.themeBtn.textContent = themeIcon(this.theme)
+ this.themeBtn.textContent = themeLabel(this.theme)
})
void this.route()
}
@@ -93,6 +93,7 @@ export class App {
el('a', { class: 'brand', href: '#/' }, [
el('div', { class: 'enso', 'aria-hidden': 'true' }),
el('strong', {}, [copy.title]),
+ el('span', { class: 'tagline' }, [copy.tagline]),
]),
this.tabsEl,
this.toolsEl,
@@ -234,7 +235,7 @@ export class App {
strategy: strategy.value as SignalStrategy,
})
})
- return form
+ return el('div', { class: 'lobby' }, [el('p', { class: 'lede' }, [copy.lobbyHint]), form])
}
private refreshTabs(active?: RoomSpec): void {
@@ -399,12 +400,6 @@ function field(label: string, control: HTMLElement): HTMLElement {
return el('label', { class: 'field' }, [el('span', {}, [label]), control])
}
-function themeIcon(pref: ThemePreference): string {
- if (pref === 'light') return '浅色'
- if (pref === 'dark') return '深色'
- return '自动'
-}
-
function specFromRecent(item: RecentRoom, active?: RoomSpec): RoomSpec {
const password = active && item.name === normalizeRoomName(active.name) && item.strategy === active.strategy ? active.password : ''
return { name: item.name, password, strategy: item.strategy }
diff --git a/src/ui/styles.css b/src/ui/styles.css
index db9a786..0615ab1 100644
--- a/src/ui/styles.css
+++ b/src/ui/styles.css
@@ -1,24 +1,36 @@
:root,
[data-theme='light'] {
- --bg: #fff;
- --grouped: #f5f5f7;
- --fg: #1d1d1f;
- --muted: #86868b;
- --hair: rgba(0, 0, 0, 0.08);
- --accent: #0071e3;
- --danger: #ff3b30;
- --fill: rgba(0, 0, 0, 0.04);
+ --bg: #faf9f5;
+ --grouped: #f3eee4;
+ --fg: #1a1612;
+ --muted: #6f665c;
+ --hair: #e4d9c8;
+ --accent: #d97757;
+ --accent-deep: #c15f3c;
+ --on-accent: #faf6f0;
+ --danger: #9f1239;
+ --ok: #4f8a6a;
+ --fill: rgba(44, 28, 16, 0.06);
+ --shadow: 0 18px 48px rgba(48, 28, 12, 0.1);
+ --glow-1: color-mix(in srgb, var(--accent) 22%, transparent);
+ --glow-2: color-mix(in srgb, #e8b86a 18%, transparent);
}
[data-theme='dark'] {
- --bg: #000;
- --grouped: #1c1c1e;
- --fg: #f5f5f7;
- --muted: #98989d;
- --hair: rgba(255, 255, 255, 0.1);
- --accent: #0a84ff;
- --danger: #ff453a;
- --fill: rgba(255, 255, 255, 0.08);
+ --bg: #161412;
+ --grouped: #211e1a;
+ --fg: #f4f0e8;
+ --muted: #a89f94;
+ --hair: #3a332b;
+ --accent: #e39472;
+ --accent-deep: #d97757;
+ --on-accent: #1a1410;
+ --danger: #fb7185;
+ --ok: #7dba8e;
+ --fill: rgba(255, 236, 214, 0.07);
+ --shadow: 0 18px 48px rgba(0, 0, 0, 0.4);
+ --glow-1: color-mix(in srgb, var(--accent) 18%, transparent);
+ --glow-2: color-mix(in srgb, #c96442 12%, transparent);
}
* {
@@ -34,14 +46,34 @@ body,
}
body {
- font: 16px/1.45 -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', 'PingFang SC', 'Noto Sans SC', sans-serif;
- background: var(--bg);
+ font: 16px/1.5 'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', 'Songti SC', 'Noto Serif SC', 'PingFang SC', serif;
+ background:
+ radial-gradient(1100px 460px at 8% -12%, var(--glow-1), transparent 54%),
+ radial-gradient(820px 380px at 108% -4%, var(--glow-2), transparent 50%),
+ var(--bg);
color: var(--fg);
- letter-spacing: -0.01em;
+ letter-spacing: 0.01em;
-webkit-font-smoothing: antialiased;
overscroll-behavior: none;
}
+::selection {
+ background: color-mix(in srgb, var(--accent) 34%, transparent);
+}
+
+input,
+select,
+textarea,
+.tab,
+.btn,
+.status,
+.member,
+.footer,
+.hint,
+.field span {
+ font-family: 'IBM Plex Sans', 'Source Sans 3', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Noto Sans SC', sans-serif;
+}
+
#app {
display: flex;
}
@@ -62,29 +94,43 @@ body {
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
- height: 48px;
+ height: 52px;
flex: 0 0 auto;
}
.brand {
display: flex;
align-items: center;
- gap: 8px;
+ gap: 9px;
text-decoration: none;
color: inherit;
}
.enso {
- width: 14px;
- height: 14px;
- border: 1.5px solid var(--fg);
+ width: 18px;
+ height: 18px;
+ border: 2px solid var(--accent);
border-radius: 50%;
border-right-color: transparent;
+ transform: rotate(-18deg);
+ flex: 0 0 auto;
}
.brand strong {
- font-size: 17px;
+ font-size: 1.22rem;
font-weight: 600;
+ letter-spacing: 0.04em;
+}
+
+.tagline {
+ color: var(--muted);
+ font-size: 12px;
+ font-family: 'IBM Plex Sans', 'Source Sans 3', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
+ white-space: nowrap;
+}
+
+.mode-chat .tagline {
+ display: none;
}
.tabs {
@@ -112,15 +158,15 @@ body {
.tab {
flex: 0 0 auto;
padding: 6px 10px;
- border-radius: 8px;
+ border-radius: 999px;
color: var(--muted);
text-decoration: none;
- font-size: 14px;
+ font-size: 13px;
}
.tab.on {
- color: var(--fg);
- background: var(--fill);
+ color: var(--accent-deep);
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.tab.add {
@@ -135,9 +181,9 @@ body {
.btn {
padding: 6px 11px;
- border-radius: 8px;
+ border-radius: 999px;
color: var(--muted);
- font-size: 14px;
+ font-size: 13px;
}
.btn.ghost:hover,
@@ -149,7 +195,14 @@ body {
.btn.primary,
.btn.send {
background: var(--accent);
- color: #fff;
+ color: var(--on-accent);
+ border: 1px solid transparent;
+}
+
+.btn.primary:hover,
+.btn.send:hover {
+ background: var(--accent-deep);
+ color: var(--on-accent);
}
.btn.send {
@@ -161,6 +214,15 @@ body {
line-height: 32px;
}
+.btn:focus-visible,
+.tab:focus-visible,
+input:focus-visible,
+select:focus-visible,
+textarea:focus-visible {
+ outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent);
+ outline-offset: 2px;
+}
+
.main {
flex: 1;
min-height: 0;
@@ -168,10 +230,28 @@ body {
flex-direction: column;
}
+.lobby {
+ display: grid;
+ gap: 12px;
+ align-content: start;
+ min-height: 0;
+ overflow: auto;
+}
+
+.lede {
+ margin: 4px 0 2px;
+ max-width: 36em;
+ color: var(--muted);
+ font-size: 15px;
+ line-height: 1.65;
+}
+
.panel {
- background: var(--grouped);
- border-radius: 12px;
- padding: 14px;
+ background: color-mix(in srgb, var(--grouped) 78%, var(--bg));
+ border: 1px solid var(--hair);
+ border-radius: 20px;
+ padding: 16px;
+ box-shadow: var(--shadow);
}
.lobby-form {
@@ -188,7 +268,7 @@ body {
.join-btn {
min-height: 40px;
- padding: 0 16px;
+ padding: 0 18px;
}
.row {
@@ -221,12 +301,20 @@ body {
font: inherit;
font-size: 16px;
color: inherit;
- background: var(--bg);
+ background: color-mix(in srgb, var(--bg) 80%, white);
border: 1px solid var(--hair);
- border-radius: 10px;
+ border-radius: 12px;
padding: 8px 10px;
}
+[data-theme='dark'] .field input,
+[data-theme='dark'] .field select,
+[data-theme='dark'] .composer textarea,
+[data-theme='dark'] .jump input,
+[data-theme='dark'] .jump select {
+ background: color-mix(in srgb, var(--grouped) 70%, #000);
+}
+
.more {
color: var(--muted);
font-size: 13px;
@@ -292,18 +380,20 @@ body {
padding: 8px 12px;
border-radius: 16px 16px 16px 4px;
background: var(--grouped);
+ border: 1px solid color-mix(in srgb, var(--hair) 70%, transparent);
}
.bubble.self {
align-self: flex-end;
border-radius: 16px 16px 4px 16px;
background: var(--accent);
- color: #fff;
+ color: var(--on-accent);
+ border-color: transparent;
}
.bubble.self header,
.bubble.self .muted {
- color: rgba(255, 255, 255, 0.72);
+ color: color-mix(in srgb, var(--on-accent) 74%, transparent);
}
.bubble header {
@@ -334,7 +424,7 @@ body {
gap: 8px;
align-items: end;
padding: 8px 0 4px;
- background: var(--bg);
+ background: transparent;
}
.composer textarea {
@@ -357,7 +447,7 @@ body {
}
.dot.ok {
- background: #34c759;
+ background: var(--ok);
}
.side {
@@ -371,6 +461,7 @@ body {
font-size: 12px;
font-weight: 600;
color: var(--muted);
+ letter-spacing: 0.06em;
}
.member {
@@ -404,9 +495,10 @@ body {
bottom: 56px;
background: var(--grouped);
border: 1px solid var(--hair);
- border-radius: 14px;
+ border-radius: 16px;
padding: 10px;
z-index: 3;
+ box-shadow: var(--shadow);
}
.picker-tabs {
@@ -428,11 +520,15 @@ body {
color: inherit;
background: var(--bg);
border: 1px solid var(--hair);
- border-radius: 8px;
+ border-radius: 10px;
padding: 6px 8px;
cursor: pointer;
}
+.stamp-btn:hover {
+ border-color: var(--accent);
+}
+
.emoji .stamp-btn {
font-size: 18px;
}
@@ -442,30 +538,36 @@ body {
}
.jump {
- border: 0;
- border-radius: 14px;
+ border: 1px solid var(--hair);
+ border-radius: 20px;
background: var(--grouped);
color: inherit;
padding: 0;
width: min(400px, calc(100% - 32px));
+ box-shadow: var(--shadow);
}
.jump::backdrop {
- background: rgba(0, 0, 0, 0.4);
+ background: rgba(28, 18, 10, 0.42);
}
.jump-form {
display: grid;
gap: 10px;
- padding: 16px;
+ padding: 18px;
}
.jump h2 {
margin: 0;
- font-size: 17px;
+ font-size: 1.15rem;
+ font-family: 'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', 'Songti SC', serif;
}
@media (max-width: 720px) {
+ .tagline {
+ display: none;
+ }
+
.join-row,
.row {
grid-template-columns: 1fr;
@@ -488,10 +590,11 @@ body {
width: min(240px, 80vw);
max-height: 50%;
background: var(--grouped);
- border-radius: 12px;
+ border: 1px solid var(--hair);
+ border-radius: 16px;
padding: 10px 12px;
z-index: 2;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
+ box-shadow: var(--shadow);
}
.chat.show-side .side {