[READ-ONLY] Mirror of https://github.com/aaronateataco/ermine. A stoat web client erminechat.vercel.app
0

Configure Feed

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

Merge pull request #14 from FascinatingPistachio/codex/develop-complete-branding-system-for-ermine

Ermine branding refresh: UI copy, colors, favicon, and modal motion

authored by

Fascinating Pistachio and committed by
GitHub
(Feb 18, 2026, 4:08 PM UTC) 4b6af690 8b547d46

+78 -57
+3 -3
README.md
··· 14 14 15 15 Ermine is a refined, Discord-inspired web client for **stoat.chat**. 16 16 17 - It prioritizes clarity, hierarchy, and composure while preserving core Stoat/Revolt functionality. 17 + It prioritizes clarity, hierarchy, and a cleaner interaction model while preserving core Stoat/Revolt functionality. 18 18 19 19 This project is **experimental** and under active development. 20 20 ··· 116 116 117 117 ## Direction 118 118 119 - Ermine is the composed interface layer for the stoat.chat ecosystem. 119 + Ermine is the composed, winter-coated interface layer for the stoat.chat ecosystem — familiar in structure, sharper in execution. 120 120 121 - **Minimal shell. Full control.** 121 + **Stoat.chat, distilled.**
+13 -2
index.html
··· 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <title>Ermine</title> 7 - <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 64 64%22%3E%3Ctext y=%2250%22 x=%2212%22 font-size=%2244%22%3E%F0%9F%90%BE%3C/text%3E%3C/svg%3E" /> 7 + <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%230F1115'/%3E%3Cpath d='M18 16h28v8H28v8h14v8H28v8h18v8H18V16z' fill='%23E6EDF3'/%3E%3C/svg%3E" /> 8 8 <link rel="preconnect" href="https://fonts.googleapis.com" /> 9 9 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 10 10 <link ··· 32 32 margin: 0; 33 33 min-height: 100vh; 34 34 font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 35 - background: radial-gradient(circle at top, #20253f 0%, #0f1014 38%, #090a0d 100%); 35 + background: #0f1115; 36 + } 37 + 38 + @keyframes ermineModalIn { 39 + from { 40 + opacity: 0; 41 + transform: translateY(4px); 42 + } 43 + to { 44 + opacity: 1; 45 + transform: translateY(0); 46 + } 36 47 } 37 48 38 49 #root {
+8 -8
privacy-policy.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <title>Ermine Privacy Policy</title> 7 7 <style> 8 - body { background:#1e1f22; color:#f2f3f5; font-family: Inter, system-ui, sans-serif; margin:0; } 8 + body { background:#0F1115; color:#E6EDF3; font-family: Inter, system-ui, sans-serif; margin:0; } 9 9 main { max-width: 760px; margin: 0 auto; padding: 48px 20px; } 10 - h1,h2 { color:#fff; } 11 - section { background:#2b2d31; border:1px solid #202225; border-radius:12px; padding:20px; margin-top:16px; } 12 - a { color:#8ea1ff; } 13 - p,li { color:#c8cbd1; line-height:1.5; } 10 + h1,h2 { color:#E6EDF3; } 11 + section { background:#171A21; border:1px solid #242A35; border-radius:12px; padding:20px; margin-top:16px; } 12 + a { color:#B6D8F6; } 13 + p,li { color:#A6B0C3; line-height:1.5; } 14 14 </style> 15 15 </head> 16 16 <body> 17 17 <main> 18 18 <h1>Ermine Privacy Policy</h1> 19 - <p>Last updated: 2026-02-14</p> 19 + <p>Last updated: 2026-02-18</p> 20 20 21 21 <section> 22 22 <h2>What Ermine stores</h2> ··· 25 25 26 26 <section> 27 27 <h2>What Ermine does not store</h2> 28 - <p>Ermine does not store your personal chat data, profile content, or server data on Ermine repositories or Ermine-operated servers.</p> 28 + <p>Ermine does not store user content outside configured stoat.chat endpoints.</p> 29 29 </section> 30 30 31 31 <section> 32 32 <h2>Service endpoints</h2> 33 - <p>Ermine communicates with official stoat.chat services (API, websocket, and CDN endpoints configured in the app).</p> 33 + <p>Ermine communicates with configured stoat.chat API, WebSocket, and CDN endpoints.</p> 34 34 </section> 35 35 36 36 <section>
+54 -44
src/App.jsx
··· 105 105 const { apiUrl: DEFAULT_API_URL, wsUrl: DEFAULT_WS_URL, cdnUrl: DEFAULT_CDN_URL } = getRuntimeConfig(); 106 106 107 107 const inputBase = 108 - 'w-full rounded-md border border-[#202225] bg-[#111214] px-3 py-2 text-sm text-gray-100 placeholder:text-gray-500 focus:border-[#5865f2] focus:outline-none focus:ring-1 focus:ring-[#5865f2]'; 108 + 'w-full rounded-md border border-[#242A35] bg-[#141821] px-3 py-2 text-sm text-[#E6EDF3] placeholder:text-[#8892A6] focus:border-[#8AB4F8] focus:outline-none focus:ring-1 focus:ring-[#8AB4F8]'; 109 109 110 110 const TOKEN_COOKIE_NAME = 'ermine_session_token'; 111 111 const USER_COOKIE_NAME = 'ermine_user_id'; ··· 211 211 render() { 212 212 if (this.state.hasError) { 213 213 return ( 214 - <div className="grid min-h-screen place-items-center bg-[#1e1f22] p-6 text-gray-100"> 215 - <div className="w-full max-w-lg rounded-2xl border border-[#202225] bg-[#2b2d31] p-8 text-center shadow-2xl"> 216 - <h1 className="mt-3 text-2xl font-bold text-white">Looks like Ermine crashed.</h1> 217 - <p className="mt-2 text-sm text-gray-300">Try reloading the app.</p> 218 - <button className="mt-6 rounded-md bg-[#5865f2] px-4 py-2 text-sm font-semibold text-white hover:bg-[#4956d8]" onClick={() => window.location.reload()} type="button">Reload Ermine</button> 214 + <div className="grid min-h-screen place-items-center bg-[#0F1115] p-6 text-[#E6EDF3]"> 215 + <div className="w-full max-w-lg rounded-2xl border border-[#242A35] bg-[#171A21] p-8 text-center shadow-2xl"> 216 + <h1 className="mt-3 text-2xl font-bold text-white">Connection interrupted.</h1> 217 + <p className="mt-2 text-sm text-[#A6B0C3]">Attempting recovery requires a reload.</p> 218 + <button className="mt-6 rounded-md bg-[#8AB4F8] px-4 py-2 text-sm font-semibold text-[#0F1115] hover:bg-[#6FA6E8]" onClick={() => window.location.reload()} type="button">Reload Ermine</button> 219 219 </div> 220 220 </div> 221 221 ); ··· 226 226 227 227 const Modal = ({ title, onClose, children }) => ( 228 228 <div className="fixed inset-0 z-50 grid place-items-center bg-black/70 p-4 backdrop-blur-sm overflow-y-auto"> 229 - <div className="w-full max-w-lg m-auto rounded-xl border border-[#202225] bg-[#2b2d31] shadow-2xl flex flex-col max-h-[85vh]"> 229 + <div className="w-full max-w-lg m-auto rounded-xl border border-[#242A35] bg-[#171A21] shadow-2xl flex max-h-[85vh] flex-col animate-[ermineModalIn_140ms_ease-out]"> 230 230 <div className="flex shrink-0 items-center justify-between border-b border-[#202225] px-4 py-3"> 231 231 <h3 className="text-sm font-bold text-white">{title}</h3> 232 232 <button className="rounded p-1 text-gray-400 hover:bg-[#3a3d42] hover:text-white" onClick={onClose}><X size={16} /></button> ··· 1068 1068 const deleteServer = async () => { if (!selectedServerId || selectedServerId === '@me') return; if (!confirm(`Are you absolutely sure you want to delete ${selectedServer?.name}? This cannot be undone.`)) return; setIsUpdatingServer(true); try { await fetch(`${config.apiUrl}/servers/${selectedServerId}`, { method: 'DELETE', headers: { 'x-session-token': auth.token } }); setActiveModal(null); } catch {} finally { setIsUpdatingServer(false); } }; 1069 1069 const openServerSettings = () => { if (selectedServer) { setEditServerName(selectedServer.name); setActiveModal('server-settings'); } }; 1070 1070 1071 - if (view === 'loading') return <div className="grid h-screen place-items-center bg-[#1e1f22] text-[#5865f2]"><Hash className="animate-spin" size={42} /></div>; 1071 + if (view === 'loading') return <div className="grid h-screen place-items-center bg-[#0F1115] text-[#8AB4F8]"><Hash className="animate-spin" size={42} /></div>; 1072 1072 1073 1073 if (view === 'login') { 1074 1074 return ( 1075 - <div className="min-h-screen bg-[#1e1f22] px-4 py-10 text-gray-100"> 1076 - <div className="mx-auto max-w-md rounded-2xl border border-[#202225] bg-[#2b2d31] p-8 shadow-2xl"> 1075 + <div className="min-h-screen bg-[#0F1115] px-4 py-10 text-[#E6EDF3]"> 1076 + <div className="relative mx-auto max-w-md rounded-2xl border border-[#242A35] bg-[#171A21] p-8 shadow-2xl"> 1077 + <div className="absolute right-4 top-4"> 1078 + <span className="rounded-full border border-[#242A35] bg-[#141821] px-2 py-1 text-[10px] font-semibold uppercase tracking-wide text-[#B6D8F6]" title="This client is under active development. Some features may be incomplete.">Experimental Build</span> 1079 + </div> 1077 1080 <div className="mb-6 text-center"> 1078 - <div className="mx-auto mb-3 grid h-16 w-16 place-items-center rounded-2xl bg-gradient-to-br from-[#5865f2] to-[#8ea1ff] text-2xl font-extrabold text-white">E</div> 1081 + <div className="mx-auto mb-3 h-16 w-16 overflow-hidden rounded-2xl border border-[#242A35] bg-[#141821]"> 1082 + <img 1083 + alt="Ermine logo" 1084 + className="h-full w-full object-cover" 1085 + src="/assets/android-chrome-192x192.png" 1086 + /> 1087 + </div> 1079 1088 <h1 className="text-2xl font-bold text-white">Ermine</h1> 1080 - <p className="text-sm text-gray-400">Branded stoat.chat client with a Discord-inspired layout.</p> 1089 + <p className="text-sm text-[#A6B0C3]">A refined client for stoat.chat.</p> 1090 + <p className="mt-1 text-xs text-[#8892A6]">Clean layout. Familiar flow. Experimental build.</p> 1081 1091 </div> 1082 - <div className="mb-5 grid grid-cols-2 gap-2 rounded-lg bg-[#1e1f22] p-1 text-xs font-semibold uppercase tracking-wide"> 1083 - <button className={`rounded py-2 ${loginMode === 'credentials' ? 'bg-[#5865f2] text-white' : 'text-gray-400 hover:text-white'}`} onClick={() => setLoginMode('credentials')} type="button">Credentials</button> 1084 - <button className={`rounded py-2 ${loginMode === 'token' ? 'bg-[#5865f2] text-white' : 'text-gray-400 hover:text-white'}`} onClick={() => setLoginMode('token')} type="button">Token</button> 1092 + <div className="mb-5 grid grid-cols-2 gap-2 rounded-lg bg-[#141821] p-1 text-xs font-semibold uppercase tracking-wide"> 1093 + <button className={`rounded py-2 ${loginMode === 'credentials' ? 'bg-[#8AB4F8] text-[#0F1115]' : 'text-gray-400 hover:text-white'}`} onClick={() => setLoginMode('credentials')} type="button">Credentials</button> 1094 + <button className={`rounded py-2 ${loginMode === 'token' ? 'bg-[#8AB4F8] text-[#0F1115]' : 'text-gray-400 hover:text-white'}`} onClick={() => setLoginMode('token')} type="button">Token</button> 1085 1095 </div> 1086 1096 <form className="space-y-4" onSubmit={handleLogin}> 1087 1097 {loginMode === 'credentials' ? ( ··· 1093 1103 ) : ( 1094 1104 <textarea className={`${inputBase} h-24 resize-none`} onChange={(e) => setManualToken(e.target.value)} placeholder="Paste session token" value={manualToken} /> 1095 1105 )} 1096 - <label className="flex items-start gap-2 rounded-md border border-[#2f3237] bg-[#1e1f22] p-2 text-xs text-gray-300"> 1106 + <label className="flex items-start gap-2 rounded-md border border-[#242A35] bg-[#141821] p-2 text-xs text-[#A6B0C3]"> 1097 1107 <input checked={privacyConsent} className="mt-0.5" onChange={(e) => setPrivacyConsent(e.target.checked)} type="checkbox" /> 1098 - <span>I agree to the <a className="text-[#8ea1ff] underline hover:text-[#bdc3ff]" href="/privacy-policy.html" rel="noreferrer" target="_blank">privacy policy</a>. Ermine stores only my session token in a cookie.</span> 1108 + <span>I agree to the <a className="text-[#B6D8F6] underline hover:text-[#E6F0FF]" href="/privacy-policy.html" rel="noreferrer" target="_blank">privacy policy</a>. Ermine does not store user content outside configured stoat.chat endpoints.</span> 1099 1109 </label> 1100 1110 {loginError ? <div className="flex items-start gap-2 rounded-md border border-red-800 bg-red-900/30 p-2 text-sm text-red-200"><AlertCircle className="mt-0.5" size={15} /><span>{loginError}</span></div> : null} 1101 - <button className="w-full rounded-md bg-[#5865f2] py-2.5 text-sm font-semibold text-white hover:bg-[#4956d8]" disabled={isLoggingIn} type="submit">{isLoggingIn ? 'Signing in…' : 'Log in to Ermine'}</button> 1111 + <button className="w-full rounded-md bg-[#8AB4F8] py-2.5 text-sm font-semibold text-[#0F1115] hover:bg-[#6FA6E8]" disabled={isLoggingIn} type="submit">{isLoggingIn ? 'Signing in…' : 'Log in to Ermine'}</button> 1102 1112 </form> 1103 1113 <div className="mt-5 border-t border-[#202225] pt-4"> 1104 - <button className="flex items-center gap-1 text-xs text-gray-400 hover:text-gray-200" onClick={() => setShowAdvanced((prev) => !prev)} type="button"><Settings size={12} /> Advanced connection settings</button> 1114 + <button className="flex items-center gap-1 text-xs text-[#A6B0C3] hover:text-[#E6EDF3]" onClick={() => setShowAdvanced((prev) => !prev)} type="button"><Settings size={12} /> Advanced connection configuration</button> 1105 1115 {showAdvanced ? ( 1106 - <div className="mt-2 space-y-2 rounded-md bg-[#1e1f22] p-2"> 1116 + <div className="mt-2 space-y-2 rounded-md bg-[#141821] p-2"> 1107 1117 <input className={inputBase} onChange={(e) => setConfig((prev) => ({ ...prev, apiUrl: e.target.value }))} placeholder="API URL" value={config.apiUrl} /> 1108 1118 <input className={inputBase} onChange={(e) => setConfig((prev) => ({ ...prev, wsUrl: e.target.value }))} placeholder="WS URL" value={config.wsUrl} /> 1109 1119 <input className={inputBase} onChange={(e) => setConfig((prev) => ({ ...prev, cdnUrl: e.target.value }))} placeholder="CDN URL" value={config.cdnUrl} /> ··· 1116 1126 } 1117 1127 1118 1128 return ( 1119 - <div className="flex h-screen overflow-hidden bg-[#1e1f22] text-gray-100"> 1129 + <div className="flex h-screen overflow-hidden bg-[#0F1115] text-[#E6EDF3]"> 1120 1130 {activeModal === 'create-server' ? ( 1121 - <Modal onClose={() => setActiveModal(null)} title="Create a server"> 1122 - <input className={inputBase} onChange={(e) => setCreateServerName(e.target.value)} placeholder="Server name" value={createServerName} /> 1123 - <button className="w-full rounded-md bg-[#3ba55d] py-2 text-sm font-semibold text-white hover:bg-[#328a4f]" onClick={createServer}>Create server</button> 1131 + <Modal onClose={() => setActiveModal(null)} title="Create a Space"> 1132 + <input className={inputBase} onChange={(e) => setCreateServerName(e.target.value)} placeholder="Space name" value={createServerName} /> 1133 + <button className="w-full rounded-md bg-[#3ba55d] py-2 text-sm font-semibold text-white hover:bg-[#328a4f]" onClick={createServer}>Create Space</button> 1124 1134 </Modal> 1125 1135 ) : null} 1126 1136 1127 1137 {activeModal === 'server-settings' && selectedServer ? ( 1128 - <Modal onClose={() => setActiveModal(null)} title="Server Settings"> 1138 + <Modal onClose={() => setActiveModal(null)} title="Space Settings"> 1129 1139 <div className="space-y-4"> 1130 1140 <div> 1131 - <label className="block text-xs font-semibold uppercase tracking-wide text-gray-400 mb-2">Server Name</label> 1141 + <label className="block text-xs font-semibold uppercase tracking-wide text-gray-400 mb-2">Space Name</label> 1132 1142 <div className="flex gap-2"> 1133 - <input className={inputBase} onChange={(e) => setEditServerName(e.target.value)} value={editServerName} placeholder="Enter server name" /> 1143 + <input className={inputBase} onChange={(e) => setEditServerName(e.target.value)} value={editServerName} placeholder="Enter space name" /> 1134 1144 <button onClick={updateServer} disabled={isUpdatingServer || !editServerName.trim()} className="rounded bg-[#5865f2] px-3 text-white hover:bg-[#4956d8] disabled:opacity-50"><Save size={18} /></button> 1135 1145 </div> 1136 1146 </div> 1137 1147 <div className="border-t border-[#202225] pt-4 mt-4"> 1138 1148 <h4 className="text-xs font-bold text-red-400 uppercase tracking-wide mb-2">Danger Zone</h4> 1139 1149 <div className="flex items-center justify-between rounded border border-red-900/50 p-3 bg-red-900/10"> 1140 - <div><div className="font-semibold text-white">Delete Server</div><div className="text-xs text-gray-400">Permanently remove this server and all its contents.</div></div> 1141 - <button onClick={deleteServer} disabled={isUpdatingServer} className="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm font-semibold transition-colors">Delete Server</button> 1150 + <div><div className="font-semibold text-white">Delete Space</div><div className="text-xs text-gray-400">Permanently remove this space and all its contents.</div></div> 1151 + <button onClick={deleteServer} disabled={isUpdatingServer} className="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm font-semibold transition-colors">Delete Space</button> 1142 1152 </div> 1143 1153 </div> 1144 1154 </div> ··· 1146 1156 ) : null} 1147 1157 1148 1158 {activeModal === 'discovery' ? ( 1149 - <Modal onClose={() => setActiveModal(null)} title="Server Discovery"> 1159 + <Modal onClose={() => setActiveModal(null)} title="Space Discovery"> 1150 1160 <div className="p-4 text-center"> 1151 1161 <Search size={48} className="mx-auto text-green-500 mb-4" /> 1152 1162 <h3 className="text-lg font-bold text-white mb-2">Explore Communities</h3> ··· 1176 1186 ) : null} 1177 1187 1178 1188 {activeModal === 'user-settings' ? ( 1179 - <Modal onClose={() => setActiveModal(null)} title="User Settings"> 1189 + <Modal onClose={() => setActiveModal(null)} title="Preferences"> 1180 1190 <div className="space-y-6"> 1181 1191 <div className="flex flex-col items-center"> 1182 1192 <div className="w-full h-24 rounded-t-lg bg-gray-700 overflow-hidden relative"> ··· 1230 1240 <div className="rounded-md bg-[#1e1f22] p-3 text-sm text-gray-200"> 1231 1241 <p className="text-xs uppercase tracking-wide text-gray-500">Do you trust this link?</p> 1232 1242 <p className="mt-2 break-all text-[#bdc3ff]">{linkPromptUrl}</p> 1233 - <p className="mt-2 text-xs text-gray-400">Ermine says: only open links from people and servers you trust.</p> 1243 + <p className="mt-2 text-xs text-gray-400">Only open links from contacts and spaces you trust.</p> 1234 1244 </div> 1235 1245 <div className="grid grid-cols-2 gap-2"> 1236 1246 <button className="rounded-md bg-[#3a3d42] py-2 text-sm font-semibold text-gray-200 hover:bg-[#4a4d55]" onClick={() => setLinkPromptUrl(null)} type="button">Cancel</button> ··· 1253 1263 </div> 1254 1264 <div className="grid grid-cols-1 gap-2 rounded-md bg-[#1e1f22] p-3 text-xs text-gray-300"> 1255 1265 <p><span className="font-semibold text-gray-100">Joined platform:</span> {toDateLabel(peekPlatformJoined)}</p> 1256 - <p><span className="font-semibold text-gray-100">Joined server:</span> {selectedServerId === '@me' ? 'N/A' : toDateLabel(peekServerJoined)}</p> 1266 + <p><span className="font-semibold text-gray-100">Joined space:</span> {selectedServerId === '@me' ? 'N/A' : toDateLabel(peekServerJoined)}</p> 1257 1267 </div> 1258 1268 <div className="space-y-1 rounded-md bg-[#1e1f22] p-3"> 1259 1269 <p className="text-[11px] font-semibold uppercase tracking-wide text-gray-500">About me</p> ··· 1265 1275 </div> 1266 1276 <div className="space-y-1 rounded-md bg-[#1e1f22] p-3"> 1267 1277 <p className="text-[11px] font-semibold uppercase tracking-wide text-gray-500">Roles</p> 1268 - {peekRoles.length ? <div className="flex flex-wrap gap-1.5">{peekRoles.map((role) => <span className="rounded bg-[#3a3d42] px-2 py-1 text-xs text-gray-200" key={role.name}>{role.name}</span>)}</div> : <p className="text-xs text-gray-400">No server roles</p>} 1278 + {peekRoles.length ? <div className="flex flex-wrap gap-1.5">{peekRoles.map((role) => <span className="rounded bg-[#3a3d42] px-2 py-1 text-xs text-gray-200" key={role.name}>{role.name}</span>)}</div> : <p className="text-xs text-gray-400">No space roles</p>} 1269 1279 </div> 1270 1280 </Modal> 1271 1281 ) : null} ··· 1290 1300 <button className="grid h-12 w-12 place-items-center rounded-full bg-[#313338] text-[#3ba55d] transition hover:rounded-2xl hover:bg-[#3ba55d] hover:text-white" onClick={() => setActiveModal('create-server')}> 1291 1301 <Plus size={20} /> 1292 1302 </button> 1293 - <button className="grid h-12 w-12 place-items-center rounded-full bg-[#313338] text-green-500 transition hover:rounded-2xl hover:bg-green-600 hover:text-white" onClick={() => setActiveModal('discovery')} title="Server Discovery"> 1303 + <button className="grid h-12 w-12 place-items-center rounded-full bg-[#313338] text-green-500 transition hover:rounded-2xl hover:bg-green-600 hover:text-white" onClick={() => setActiveModal('discovery')} title="Space Discovery"> 1294 1304 <Search size={20} /> 1295 1305 </button> 1296 1306 </aside> 1297 1307 1298 1308 <aside className="hidden w-60 flex-col bg-[#2b2d31] md:flex"> 1299 1309 <div className="border-b border-[#202225] px-4 py-3 flex justify-between items-center"> 1300 - <div className="truncate text-sm font-bold text-white">{selectedServerId === '@me' ? 'Ermine Home' : servers[selectedServerId]?.name || 'Server'}</div> 1310 + <div className="truncate text-sm font-bold text-white">{selectedServerId === '@me' ? 'Live' : servers[selectedServerId]?.name || 'Space'}</div> 1301 1311 {isServerOwner && selectedServerId !== '@me' && ( 1302 1312 <button 1303 1313 onClick={openServerSettings} 1304 1314 className="text-gray-400 hover:text-white transition-colors" 1305 - title="Server Settings" 1315 + title="Space Settings" 1306 1316 > 1307 1317 <Settings size={16} /> 1308 1318 </button> ··· 1312 1322 {selectedServerId === '@me' ? ( 1313 1323 <> 1314 1324 <button className={`mb-1 flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm ${selectedChannelId === 'friends' ? 'bg-[#404249] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-white'}`} onClick={() => handleChannelSelect('friends')}> 1315 - <Users size={16} /> Friends 1325 + <Users size={16} /> <span>Direct</span> 1316 1326 </button> 1317 1327 {directMessageChannels.map((channel) => { 1318 1328 const recipientId = (channel.recipients || []).find((id) => id !== auth.userId); ··· 1354 1364 </div> 1355 1365 </button> 1356 1366 <div className="flex items-center gap-1"> 1357 - <button className="rounded p-1 text-gray-400 hover:bg-[#35373c] hover:text-white" onClick={() => setActiveModal('user-settings')} title="User settings" type="button"> 1367 + <button className="rounded p-1 text-gray-400 hover:bg-[#35373c] hover:text-white" onClick={() => setActiveModal('user-settings')} title="Preferences" type="button"> 1358 1368 <Settings size={14} /> 1359 1369 </button> 1360 1370 <button className="rounded p-1 text-gray-400 hover:bg-[#35373c] hover:text-white" onClick={logout} title="Logout" type="button"> ··· 1371 1381 {channels[selectedChannelId]?.channel_type === 'DirectMessage' ? <MessageSquare size={17} className="text-gray-400" /> : <Hash size={17} className="text-gray-400" />} 1372 1382 <span>{currentChannelName}</span> 1373 1383 </div> 1374 - <div className="text-xs text-gray-400">Ermine for stoat.chat</div> 1384 + <div className="text-xs text-gray-400">Stoat.chat, distilled.</div> 1375 1385 </header> 1376 1386 1377 1387 {voiceNotice ? ( ··· 1383 1393 <section className="flex-1 overflow-y-auto py-2" ref={messagesContainerRef}> 1384 1394 {selectedChannelId === 'friends' ? ( 1385 1395 <div className="space-y-2 p-4"> 1386 - <h2 className="text-xs font-bold uppercase tracking-wide text-gray-400">Friends ({friends.length})</h2> 1387 - {friends.length === 0 ? <p className="text-sm text-gray-400">No friends available yet.</p> : null} 1396 + <h2 className="text-xs font-bold uppercase tracking-wide text-gray-400">Direct ({friends.length})</h2> 1397 + {friends.length === 0 ? <p className="text-sm text-gray-400">No direct contacts available yet.</p> : null} 1388 1398 {friends.map((friend) => ( 1389 1399 <button className="flex w-full items-center gap-3 rounded bg-[#2b2d31] p-3 text-left hover:bg-[#35373c]" key={friend._id} onClick={() => openDmWithUser(friend._id)}> 1390 1400 <Avatar animateOnHover cdnUrl={config.cdnUrl} user={friend} /> 1391 1401 <div className="min-w-0"> 1392 1402 <div className="truncate text-sm font-semibold text-white">{friend.username}</div> 1393 - <div className="text-xs text-gray-400">Direct message</div> 1403 + <div className="text-xs text-gray-400">Direct</div> 1394 1404 </div> 1395 1405 </button> 1396 1406 ))} ··· 1479 1489 <div ref={pickerRef} onMouseLeave={() => setShowEmojiPicker(false)} className="absolute bottom-12 left-0 z-20 w-72 rounded-lg border border-[#4c4f56] bg-[#232428] p-2 shadow-2xl max-h-64 overflow-y-auto"> 1480 1490 <p className="mb-2 px-1 text-[10px] uppercase tracking-wide text-gray-500 font-bold sticky top-0 bg-[#232428] z-10">Standard</p> 1481 1491 <div className="grid grid-cols-8 gap-1 mb-2">{STANDARD_EMOJIS.map((emoji) => <button className="rounded p-1 text-lg hover:bg-[#3a3d42]" key={emoji} onClick={() => addEmojiToComposer(emoji)} type="button">{renderTwemoji(emoji, "w-6 h-6 inline-block")}</button>)}</div> 1482 - {allCustomEmojis.length > 0 ? <><p className="mb-2 px-1 text-[10px] uppercase tracking-wide text-gray-500 font-bold sticky top-0 bg-[#232428] z-10">Server Emojis</p><div className="grid grid-cols-8 gap-1">{allCustomEmojis.map((emoji) => <button className="grid h-8 place-items-center rounded hover:bg-[#3a3d42]" key={emoji.id} onClick={() => addCustomEmojiToComposer(emoji.id)} title={`${emoji.name} from ${emoji.serverName}`} type="button">{renderEmojiVisual(`:${emoji.id}:`, { id: emoji.id, name: emoji.name }, config.cdnUrl)}</button>)}</div></> : null} 1492 + {allCustomEmojis.length > 0 ? <><p className="mb-2 px-1 text-[10px] uppercase tracking-wide text-gray-500 font-bold sticky top-0 bg-[#232428] z-10">Space Emojis</p><div className="grid grid-cols-8 gap-1">{allCustomEmojis.map((emoji) => <button className="grid h-8 place-items-center rounded hover:bg-[#3a3d42]" key={emoji.id} onClick={() => addCustomEmojiToComposer(emoji.id)} title={`${emoji.name} from ${emoji.serverName}`} type="button">{renderEmojiVisual(`:${emoji.id}:`, { id: emoji.id, name: emoji.name }, config.cdnUrl)}</button>)}</div></> : null} 1483 1493 </div> 1484 1494 ) : null} 1485 1495 </div>