diff --git a/index.html b/index.html
new file mode 100644
index 0000000..7d2a5f6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,106 @@
+
+
+
+
+ Snowdrop Messenger
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/proto/rac.js b/proto/rac.js
new file mode 100644
index 0000000..d342d07
--- /dev/null
+++ b/proto/rac.js
@@ -0,0 +1 @@
+// ъуъ
\ No newline at end of file
diff --git a/proto/wrac.js b/proto/wrac.js
new file mode 100644
index 0000000..efa5110
--- /dev/null
+++ b/proto/wrac.js
@@ -0,0 +1,23 @@
+ function wRAC(onOpenCb) {
+ if (!connectedServer) return;
+ if (ws && ws.readyState === 1) return onOpenCb();
+ if (ws) { ws.onclose = null; ws.close(); }
+ ws = new WebSocket(connectedServer);
+ ws.binaryType = "arraybuffer";
+ ws.onopen = function () { if (onOpenCb) onOpenCb(); };
+ ws.onerror = function () { };
+ ws.onclose = function () { };
+ ws.onmessage = function (event) {
+ if (typeof event.data === "string") return;
+ let buf = new Uint8Array(event.data);
+ if (buf.length === 1 && (buf[0] === 0x01 || buf[0] === 0x02)) return;
+ let str = new TextDecoder().decode(buf).trim();
+ if (/^\d+$/.test(str)) {
+ ws.send(new Uint8Array([0x00, 0x01]));
+ } else {
+ let lines = str.split('\n').filter(Boolean);
+ messages = lines;
+ showMessages();
+ }
+ };
+ }
\ No newline at end of file
diff --git a/src/chat.js b/src/chat.js
new file mode 100644
index 0000000..0c34dc4
--- /dev/null
+++ b/src/chat.js
@@ -0,0 +1,111 @@
+ function extractNickColor(str) {
+ let m = str.match(/<([^>]+)>/);
+ if (!m) return { nick: "unauth", colorClass: "nick-unauth" };
+ let beforeNick = str.substring(0, m.index);
+ let nick = m[1];
+ let colorClass = "nick-cyan";
+ for (const { marker, color } of nickMarkers) {
+ if (beforeNick.includes(marker)) {
+ colorClass = color;
+ break;
+ }
+ }
+ return { nick, colorClass };
+ }
+
+ function parseMsg(msg) {
+ let date = "";
+ let nick = "";
+ let text = msg.trim();
+ let colorClass = "";
+
+ let m = text.match(/^\[(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})\]\s*(.*)$/);
+ if (m) {
+ date = m[1];
+ text = m[2];
+ }
+
+ let nickMatch = text.match(/^(.*?<[^>]+>)(\s?)(.*)$/);
+ if (nickMatch) {
+ let prefix = nickMatch[1];
+ let afterNick = nickMatch[3];
+ let ext = extractNickColor(prefix);
+ nick = ext.nick;
+ colorClass = ext.colorClass;
+ text = afterNick;
+ return { nick, text, date, colorClass };
+ }
+
+ if (date && !nick) {
+ return { nick: "unauth", text, date, colorClass: "nick-unauth" };
+ }
+ return { nick: "unauth", text, date: "", colorClass: "nick-unauth" };
+ }
+
+ function getVisibleMessages(messages) {
+ if (connectedServer && connectedServer.startsWith("wss://meex.lol:52667") && messages.length > 3) {
+ return messages.slice(3);
+ }
+ return messages;
+ }
+
+ function showMessages() {
+ let chat = document.getElementById('chat-area');
+ chat.innerHTML = '';
+ let displayMessages = getVisibleMessages([...messages].reverse());
+ for (let msg of displayMessages) {
+ let { nick, text, date, colorClass } = parseMsg(msg);
+ chat.innerHTML += `
+
+ ${nick ? nick : ""}
+ ${(text ?? "").replace(/")}
+ ${date ? "[" + date + "]" : ""}
+
`;
+ }
+ }
+
+ function sendMsg() {
+ const msg = document.getElementById('chat-input').value.trim();
+ if (!msg || !connectedServer) return;
+ const { username, password } = getActiveServerCreds();
+ wRAC(() => {
+ let arr;
+ let format = (settings && settings.messageFormat) ? settings.messageFormat : DEFAULT_SETTINGS.messageFormat;
+ let formatted = format;
+ if (formatted.includes("{name}")) formatted = formatted.replace("{name}", username ?? "");
+ if (formatted.includes("{text}")) formatted = formatted.replace("{text}", msg ?? "");
+ if (username && password) {
+ let enc = new TextEncoder();
+ let uname = enc.encode(username);
+ let pass = enc.encode(password);
+ let text = enc.encode(formatted);
+ let total = new Uint8Array(1 + uname.length + 1 + pass.length + 1 + text.length);
+ total[0] = 0x02;
+ total.set(uname, 1);
+ total[1 + uname.length] = 10;
+ total.set(pass, 1 + uname.length + 1);
+ total[1 + uname.length + 1 + pass.length] = 10;
+ total.set(text, 1 + uname.length + 1 + pass.length + 1);
+ arr = total;
+ } else {
+ arr = [0x01, ...new TextEncoder().encode(formatted)];
+ arr = new Uint8Array(arr);
+ }
+ ws.send(arr);
+ document.getElementById('chat-input').value = "";
+ setTimeout(fetchMessages, 200);
+ });
+ }
+
+ function fetchMessages() {
+ wRAC(() => {
+ ws.send(new Uint8Array([0x00]));
+ });
+ }
+
+ document.getElementById('send-btn').onclick = sendMsg;
+ document.getElementById('chat-input').addEventListener('keydown', function (e) {
+ if (e.key === "Enter") sendMsg();
+ });
+
+ setInterval(fetchMessages, 6000);
\ No newline at end of file
diff --git a/src/panel.js b/src/panel.js
new file mode 100644
index 0000000..ccf1978
--- /dev/null
+++ b/src/panel.js
@@ -0,0 +1,348 @@
+ const MESSAGE_FORMAT_PRESETS = [
+ { id: "snowdrop", label: "Snowdrop", format: "ඞ<{name}> {text}", class: "preset-snowdrop" },
+ { id: "brac", label: "bRAC", format: "리㹰<{name}> {text}", class: "preset-brac" },
+ { id: "crab", label: "CRAB", format: "═══<{name}> {text}", class: "preset-crab" },
+ { id: "mefidroniy", label: "Mefidroniy", format: "°ʘ<{name}> {text}", class: "preset-mefidroniy" },
+ { id: "crack", label: "cRACk", format: "⁂<{name}> {text}", class: "preset-crack" },
+ { id: "default", label: "Default (clRAC)", format: "<{name}> {text}", class: "preset-default" }
+ ];
+ const DEFAULT_SETTINGS = {
+ lang: "ru",
+ messageFormat: "ඞ<{name}> {text}",
+ messageFormatPreset: "snowdrop"
+ };
+ function getSettings() {
+ try {
+ let s = JSON.parse(localStorage.getItem("snowdrop_settings") || "{}");
+ return { ...DEFAULT_SETTINGS, ...s };
+ } catch (e) {
+ return { ...DEFAULT_SETTINGS };
+ }
+ }
+ function saveSettings(s) {
+ localStorage.setItem("snowdrop_settings", JSON.stringify(s));
+ }
+ let settings = getSettings();
+
+ function getProtoLabel(proto) {
+ let label = { proto, className: proto };
+ if (proto === "wRACs") label.className = "wRACs";
+ else if (proto === "wRAC") label.className = "wRAC";
+ else if (proto === "RACs") label.className = "RACs";
+ else if (proto === "RAC") label.className = "RAC";
+ else label.className = "";
+ return label;
+ }
+ function buildServerUrl({ proto, address, port }) {
+ if (proto === "wRACs") return `wss://${address}:${port}`;
+ if (proto === "wRAC") return `ws://${address}:${port}`;
+ if (proto === "RACs") return `rac+tls://${address}:${port}`;
+ if (proto === "RAC") return `rac://${address}:${port}`;
+ return `${address}:${port}`;
+ }
+ function parseServerUrl(url) {
+ let m;
+ if (m = url.match(/^wss:\/\/([^:\/]+):(\d+)$/)) return { proto: "wRACs", address: m[1], port: m[2] };
+ if (m = url.match(/^ws:\/\/([^:\/]+):(\d+)$/)) return { proto: "wRAC", address: m[1], port: m[2] };
+ if (m = url.match(/^rac\+tls:\/\/([^:\/]+):(\d+)$/)) return { proto: "RACs", address: m[1], port: m[2] };
+ if (m = url.match(/^rac:\/\/([^:\/]+):(\d+)$/)) return { proto: "RAC", address: m[1], port: m[2] };
+ return { proto: "wRACs", address: url, port: "" };
+ }
+ function getSavedServers() {
+ let arr = [];
+ try { arr = JSON.parse(localStorage.getItem('snowdrop_servers') || "[]") } catch (e) { }
+ if (arr.length && typeof arr[0] === "string") {
+ arr = arr.map(url => {
+ const { proto, address, port } = parseServerUrl(url);
+ return { title: address, proto, address, port };
+ });
+ }
+ return arr;
+ }
+ function saveServers(arr) {
+ localStorage.setItem('snowdrop_servers', JSON.stringify(arr));
+ }
+ let servers = getSavedServers();
+ let ws = null;
+ let messages = [];
+ let connectedServer = servers[0] ? buildServerUrl(servers[0]) : null;
+
+ function renderChannels() {
+ const channels = document.getElementById('channels');
+ channels.innerHTML = '';
+ servers.forEach((srv, idx) => {
+ const label = getProtoLabel(srv.proto);
+ const url = buildServerUrl(srv);
+ const isSelected = connectedServer === url || (!connectedServer && idx === 0);
+ const div = document.createElement('div');
+ div.className = 'channel' + (isSelected ? ' selected' : '');
+ div.setAttribute('data-server-idx', idx);
+ div.innerHTML = `
+
+ ${srv.address}:${srv.port}
+ `;
+ div.querySelector('.channel-header-main').onclick = function (e) {
+ document.querySelectorAll('#channels .channel').forEach(c => c.classList.remove('selected'));
+ div.classList.add('selected');
+ connectedServer = url;
+ fetchMessages();
+ };
+ div.querySelector('.delete-server-btn').onclick = function (e) {
+ e.stopPropagation();
+ if (confirm(t('confirmDelete', { title: srv.title }))) {
+ servers.splice(idx, 1);
+ saveServers(servers);
+ if (servers.length) {
+ connectedServer = buildServerUrl(servers[Math.min(idx, servers.length - 1)]);
+ } else {
+ connectedServer = null;
+ messages = [];
+ showMessages();
+ }
+ renderChannels();
+ fetchMessages();
+ }
+ };
+ div.querySelector('.edit-server-btn').onclick = function (e) {
+ e.stopPropagation();
+ openEditModal(idx);
+ };
+ channels.appendChild(div);
+ });
+ }
+
+ function openEditModal(idx) {
+ const srv = servers[idx];
+ modalTitle.value = srv.title || "";
+ modalAddress.value = srv.address || "";
+ modalPort.value = srv.port || "";
+ modalProto.value = srv.proto || "wRACs";
+ modalUsername.value = srv.username || "";
+ modalPassword.value = srv.password || "";
+ modalError.textContent = "";
+ modalBg.style.display = "flex";
+ setTimeout(() => modalTitle.focus(), 50);
+
+ saveBtn.onclick = function () {
+ const title = modalTitle.value.trim();
+ const address = modalAddress.value.trim();
+ const port = modalPort.value.trim();
+ const proto = modalProto.value;
+ const username = modalUsername.value.trim();
+ const password = modalPassword.value;
+ if (!title || !address || !port || !proto) {
+ modalError.textContent = t('fillAllFields');
+ return;
+ }
+ if (!/^[a-zA-Z0-9а-яА-Я\-\.\s_]+$/.test(title)) {
+ modalError.textContent = t('invalidTitle');
+ return;
+ }
+ if (!/^[a-zA-Z0-9\-\.]+$/.test(address)) {
+ modalError.textContent = t('invalidAddress');
+ return;
+ }
+ if (!/^\d+$/.test(port) || +port < 1 || +port > 65535) {
+ modalError.textContent = t('invalidPort');
+ return;
+ }
+ if (servers.some((srv2, i) => i !== idx && srv2.proto === proto && srv2.address === address && srv2.port === port)) {
+ modalError.textContent = t('duplicateServer');
+ return;
+ }
+ servers[idx] = { title, proto, address, port, username, password };
+ saveServers(servers);
+ connectedServer = buildServerUrl(servers[idx]);
+ closeModal();
+ renderChannels();
+ fetchMessages();
+ };
+ }
+
+ function getActiveServerCreds() {
+ if (!connectedServer) return {};
+ let idx = servers.findIndex(
+ s => buildServerUrl(s) === connectedServer
+ );
+ if (idx === -1) return {};
+ let { username, password } = servers[idx];
+ return { username: username || "", password: password || "" };
+ }
+
+ function getCurrentFormatPresetId(fmt) {
+ for (const preset of MESSAGE_FORMAT_PRESETS) {
+ if (preset.format === fmt) return preset.id;
+ }
+ return null;
+ }
+
+ const modalBg = document.getElementById('server-modal-bg');
+ const modalForm = document.getElementById('server-modal');
+ const modalTitle = document.getElementById('modal-server-title');
+ const modalAddress = document.getElementById('modal-server-address');
+ const modalPort = document.getElementById('modal-server-port');
+ const modalProto = document.getElementById('modal-server-proto');
+ const modalUsername = document.getElementById('modal-server-username');
+ const modalPassword = document.getElementById('modal-server-password');
+ const modalError = document.getElementById('server-modal-error');
+ const saveBtn = document.getElementById('save-server-btn');
+ const cancelBtn = document.getElementById('cancel-server-btn');
+
+ function openModal() {
+ modalTitle.value = "";
+ modalAddress.value = "";
+ modalPort.value = "";
+ modalProto.value = "wRACs";
+ modalUsername.value = "";
+ modalPassword.value = "";
+ modalError.textContent = "";
+ modalBg.style.display = "flex";
+ setTimeout(() => modalTitle.focus(), 50);
+
+ saveBtn.onclick = function () {
+ const title = modalTitle.value.trim();
+ const address = modalAddress.value.trim();
+ const port = modalPort.value.trim();
+ const proto = modalProto.value;
+ const username = modalUsername.value.trim();
+ const password = modalPassword.value;
+ if (!title || !address || !port || !proto) {
+ modalError.textContent = t('fillAllFields');
+ return;
+ }
+ if (!/^[a-zA-Z0-9а-яА-Я\-\.\s_]+$/.test(title)) {
+ modalError.textContent = t('invalidTitle');
+ return;
+ }
+ if (!/^[a-zA-Z0-9\-\.]+$/.test(address)) {
+ modalError.textContent = t('invalidAddress');
+ return;
+ }
+ if (!/^\d+$/.test(port) || +port < 1 || +port > 65535) {
+ modalError.textContent = t('invalidPort');
+ return;
+ }
+ if (servers.some(srv => srv.proto === proto && srv.address === address && srv.port === port)) {
+ modalError.textContent = t('duplicateServer');
+ return;
+ }
+ servers.push({ title, proto, address, port, username, password });
+ saveServers(servers);
+ connectedServer = buildServerUrl(servers[servers.length - 1]);
+ closeModal();
+ renderChannels();
+ fetchMessages();
+ };
+ }
+ function closeModal() {
+ modalBg.style.display = "none";
+ }
+ document.getElementById('add-server-btn').onclick = openModal;
+ cancelBtn.onclick = closeModal;
+
+ // Настройки
+ const settingsBtn = document.getElementById('header-settings-btn');
+ const settingsModalBg = document.getElementById('settings-modal-bg');
+ const settingsModal = document.getElementById('settings-modal');
+ const settingsLangSelect = document.getElementById('settings-lang-select');
+ const settingsFormatInput = document.getElementById('settings-format-input');
+ const settingsModalError = document.getElementById('settings-modal-error');
+ const settingsModalPresets = document.querySelectorAll('.settings-format-preset-btn');
+ const saveSettingsBtn = document.getElementById('save-settings-btn');
+ const cancelSettingsBtn = document.getElementById('cancel-settings-btn');
+
+ function updateSettingsModalFields() {
+ settingsLangSelect.value = settings.lang || "ru";
+ settingsFormatInput.value = settings.messageFormat || DEFAULT_SETTINGS.messageFormat;
+ let id = getCurrentFormatPresetId(settingsFormatInput.value);
+ settingsModalPresets.forEach(btn => {
+ btn.classList.toggle("selected", btn.dataset.id === id);
+ });
+ }
+
+ function openSettingsModal() {
+ updateSettingsModalFields();
+ settingsModalError.textContent = "";
+ settingsModalBg.style.display = "flex";
+ setTimeout(() => settingsLangSelect.focus(), 50);
+ }
+ function closeSettingsModal() { settingsModalBg.style.display = "none"; }
+
+ settingsBtn.onclick = openSettingsModal;
+ cancelSettingsBtn.onclick = closeSettingsModal;
+
+ settingsModalPresets.forEach(btn => {
+ btn.onclick = function () {
+ settingsFormatInput.value = btn.getAttribute("data-format")
+ .replace(/</g, "<").replace(/>/g, ">");
+ settingsModalPresets.forEach(b => b.classList.remove("selected"));
+ btn.classList.add("selected");
+ };
+ });
+
+ saveSettingsBtn.onclick = function () {
+ const lang = settingsLangSelect.value;
+ const format = settingsFormatInput.value.trim();
+ settings = {
+ ...settings,
+ lang,
+ messageFormat: format,
+ messageFormatPreset: getCurrentFormatPresetId(format) || null
+ };
+ saveSettings(settings);
+ closeSettingsModal();
+ updateUIStrings();
+ };
+
+ settingsModalBg.addEventListener("click", function (e) {
+ if (e.target === settingsModalBg) closeSettingsModal();
+ });
+
+ function updateUIStrings() {
+ // Панелька
+ document.getElementById('app-title').textContent = t('appName');
+ document.getElementById('add-server-btn').textContent = t('addServer');
+ document.getElementById('header-settings-btn').title = t('settings');
+ // Добавление серверовй
+ document.getElementById('label-server-title').textContent = t('serverTitle');
+ document.getElementById('label-server-address').textContent = t('address');
+ document.getElementById('label-server-port').textContent = t('port');
+ document.getElementById('label-server-proto').textContent = t('protocol');
+ document.getElementById('label-server-username').textContent = t('username');
+ document.getElementById('label-server-password').textContent = t('password');
+ document.getElementById('save-server-btn').textContent = t('save');
+ document.getElementById('cancel-server-btn').textContent = t('cancel');
+ // Настройки
+ document.getElementById('settings-label-lang').textContent = t('settingsLabelLang');
+ document.getElementById('settings-label-format').textContent = t('settingsLabelFormat');
+ saveSettingsBtn.textContent = t('settingsSave');
+ cancelSettingsBtn.textContent = t('settingsCancel');
+ settingsFormatInput.placeholder = t('messageFormat');
+ // Ввод сообщений
+ document.getElementById('chat-input').placeholder = t('writeMessage');
+ document.getElementById('send-btn').title = t('send');
+ renderChannels();
+ }
+
+ settingsLangSelect.onchange = function () {
+ settings.lang = settingsLangSelect.value;
+ saveSettings(settings);
+ updateUIStrings();
+ };
+
+ updateUIStrings();
+ settingsBtn.addEventListener("click", updateSettingsModalFields);
+ renderChannels();
\ No newline at end of file
diff --git a/src/script.js b/src/script.js
new file mode 100644
index 0000000..69a4021
--- /dev/null
+++ b/src/script.js
@@ -0,0 +1,89 @@
+ const translations = {
+ ru: {
+ appName: "Snowdrop",
+ addServer: "+ Добавить сервер",
+ serverTitle: "Название сервера",
+ address: "IP/домен",
+ port: "Порт",
+ protocol: "Протокол",
+ username: "Имя пользователя (необязательно)",
+ password: "Пароль (необязательно)",
+ save: "Сохранить",
+ cancel: "Отмена",
+ editServer: "Изменить сервер",
+ deleteServer: "Удалить сервер",
+ confirmDelete: 'Удалить сервер "{title}"?',
+ settings: "Настройки",
+ language: "Язык",
+ messageFormat: "Формат сообщения",
+ settingsSave: "Сохранить",
+ settingsCancel: "Отмена",
+ invalidTitle: "Некорректное название",
+ invalidAddress: "Некорректный адрес",
+ invalidPort: "Некорректный порт",
+ duplicateServer: "Такой сервер уже добавлен",
+ fillAllFields: "Заполните все поля",
+ writeMessage: "Написать сообщение...",
+ send: "Отправить",
+ plain: "Простой",
+ wRACs: "wRACs",
+ wRAC: "wRAC (open snowdrop using http://)",
+ RACs: "RACs (WIP)",
+ RAC: "RAC (WIP)",
+ settingsLabelLang: "Язык",
+ settingsLabelFormat: "Формат сообщения"
+ },
+ en: {
+ appName: "Snowdrop",
+ addServer: "+ Add server",
+ serverTitle: "Server title",
+ address: "IP/domain",
+ port: "Port",
+ protocol: "Protocol",
+ username: "Username (optional)",
+ password: "Password (optional)",
+ save: "Save",
+ cancel: "Cancel",
+ editServer: "Edit server",
+ deleteServer: "Delete server",
+ confirmDelete: 'Delete server "{title}"?',
+ settings: "Settings",
+ language: "Language",
+ messageFormat: "Message format",
+ settingsSave: "Save",
+ settingsCancel: "Cancel",
+ invalidTitle: "Invalid title",
+ invalidAddress: "Invalid address",
+ invalidPort: "Invalid port",
+ duplicateServer: "This server is already added",
+ fillAllFields: "Fill all fields",
+ writeMessage: "Write a message...",
+ send: "Send",
+ plain: "Plain",
+ wRACs: "wRACs",
+ wRAC: "wRAC (open snowdrop using http://)",
+ RACs: "RACs (WIP)",
+ RAC: "RAC (WIP)",
+ settingsLabelLang: "Language",
+ settingsLabelFormat: "Message format"
+ }
+ };
+
+ function t(key, vars = {}) {
+ const lang = settings && settings.lang || "ru";
+ let str = translations[lang][key] || translations['ru'][key] || key;
+ Object.keys(vars).forEach(k => {
+ str = str.replace("{" + k + "}", vars[k]);
+ });
+ return str;
+ }
+
+ const nickMarkers = [
+ { marker: "\uB9AC\u3E70", color: "nick-green" }, // бракованный
+ { marker: "\u2550\u2550\u2550", color: "nick-lightred" },// краб
+ { marker: "\u00B0\u0298", color: "nick-lightmagenta" }, // меф
+ { marker: "\u2042", color: "nick-gold" }, // кря
+ { marker: "\u0D9E", color: "nick-amogus" }, // сновдроп
+ ];
+
+ window.onload = () => { fetchMessages(); };
\ No newline at end of file
diff --git a/themes/styles.css b/themes/styles.css
new file mode 100644
index 0000000..6db4e18
--- /dev/null
+++ b/themes/styles.css
@@ -0,0 +1,574 @@
+ body {
+ margin: 0;
+ padding: 0;
+ background: #18191c;
+ color: #dee2e6;
+ font-family: 'gg sans', 'Segoe UI', Arial, sans-serif;
+ height: 100vh;
+ overflow: hidden;
+ }
+
+ #app-container {
+ display: flex;
+ height: 100vh;
+ }
+
+ #channel-list {
+ width: 260px;
+ background: #141517;
+ border-right: 1px solid #101114;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ #channel-list-header {
+ padding: 16px 18px 8px 18px;
+ font-weight: bold;
+ color: #fff;
+ font-size: 1rem;
+ letter-spacing: 1px;
+ border-bottom: 1px solid #101114;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .header-settings-btn {
+ background: none;
+ border: none;
+ margin-left: 8px;
+ padding: 1px 0 0 7px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ opacity: 0.8;
+ transition: opacity 0.18s;
+ }
+
+ .header-settings-btn:hover {
+ opacity: 1;
+ }
+
+ .header-settings-btn svg {
+ width: 23px;
+ height: 23px;
+ fill: #aaa;
+ }
+
+ #channels {
+ flex: 1;
+ padding: 10px 0;
+ overflow-y: auto;
+ }
+
+ .channel {
+ padding: 10px 20px;
+ color: #8a99a8;
+ cursor: pointer;
+ font-size: 1rem;
+ display: flex;
+ flex-direction: column;
+ border-radius: 6px;
+ transition: background 0.2s, color 0.2s;
+ margin-bottom: 2px;
+ position: relative;
+ }
+
+ .channel.selected, .channel:hover {
+ background: #23272a;
+ color: #fff;
+ }
+
+ .channel-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .channel-header-main {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ }
+
+ .proto-label {
+ display: inline-block;
+ min-width: 48px;
+ font-family: monospace;
+ font-size: 1em;
+ font-weight: bold;
+ color: #fff;
+ background: #5b8fbd;
+ border-radius: 4px;
+ padding: 2px 8px;
+ margin-right: 10px;
+ text-align: center;
+ }
+
+ .proto-label.wRAC {
+ background: #555;
+ }
+
+ .proto-label.wRACs {
+ background: #5b8fbd;
+ }
+
+ .proto-label.RAC {
+ background: #aa4000;
+ }
+
+ .proto-label.RACs {
+ background: #007c40;
+ }
+
+ .channel-title {
+ font-size: 1.05em;
+ color: #fff;
+ font-weight: normal;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 120px;
+ }
+
+ .channel-address {
+ font-size: 0.92em;
+ color: #888;
+ margin-left: 5px;
+ margin-top: 1px;
+ word-break: break-all;
+ }
+
+ .edit-server-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ margin-left: 0;
+ align-self: flex-start;
+ opacity: 0.7;
+ transition: opacity 0.15s;
+ margin-right: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .edit-server-btn:hover {
+ opacity: 1;
+ }
+
+ .edit-server-btn svg {
+ width: 19px;
+ height: 19px;
+ fill: #888;
+ display: block;
+ pointer-events: none;
+ }
+
+ .delete-server-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ margin-left: 0;
+ align-self: flex-start;
+ opacity: 0.7;
+ transition: opacity 0.15s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .delete-server-btn:hover {
+ opacity: 1;
+ }
+
+ .delete-server-btn svg {
+ width: 19px;
+ height: 19px;
+ fill: #888;
+ display: block;
+ pointer-events: none;
+ }
+
+ .channel.selected .delete-server-btn svg,
+ .channel:hover .delete-server-btn svg,
+ .channel.selected .edit-server-btn svg,
+ .channel:hover .edit-server-btn svg {
+ fill: #ff9191;
+ }
+
+ #add-server-btn {
+ width: 95%;
+ margin: 10px auto 15px auto;
+ padding: 10px 0;
+ background: #23272a;
+ color: #5b8fbd;
+ border: none;
+ border-radius: 7px;
+ font-size: 1em;
+ font-weight: bold;
+ cursor: pointer;
+ transition: background 0.2s, color 0.2s;
+ display: block;
+ }
+
+ #add-server-btn:hover {
+ background: #222e39;
+ color: #fff;
+ }
+
+ #main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: #18191c;
+ }
+
+ #chat-area {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px 24px 12px 24px;
+ background: #18191c;
+ display: flex;
+ flex-direction: column-reverse;
+ font-family: "Fira Mono", "Consolas", "Menlo", "monospace";
+ font-size: 1.09rem;
+ line-height: 1.7;
+ }
+
+ .message {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ padding: 2px 0 2px 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+ min-height: 32px;
+ }
+
+ .nick {
+ font-weight: bold;
+ margin-right: 10px;
+ min-width: 130px;
+ text-align: left;
+ flex-shrink: 0;
+ font-family: inherit;
+ }
+
+ .nick-green {
+ color: #18b800;
+ }
+
+ .nick-lightred {
+ color: #ff9191;
+ }
+
+ .nick-lightmagenta {
+ color: #e782ff;
+ }
+
+ .nick-gold {
+ color: #ffd700;
+ }
+
+ .nick-cyan {
+ color: #5b8fbd;
+ }
+
+ .nick-unauth {
+ color: #666;
+ font-style: italic;
+ }
+
+ .nick-amogus {
+ color: #aeff00;
+ }
+
+ .msg {
+ color: #fff;
+ flex: 1;
+ text-align: left;
+ font-family: inherit;
+ white-space: pre-wrap;
+ word-break: break-word;
+ padding-right: 16px;
+ }
+
+ .time {
+ color: #8a8a8a;
+ font-size: 0.94em;
+ margin-left: 0;
+ min-width: 145px;
+ text-align: right;
+ flex-shrink: 0;
+ font-family: inherit;
+ }
+
+ #chat-input-area {
+ display: flex;
+ flex-direction: column;
+ padding: 10px 18px 16px 18px;
+ background: #18191c;
+ border-top: 1px solid #101114;
+ }
+
+ #chat-input-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ #chat-input {
+ flex: 1;
+ background: #101114;
+ color: #fff;
+ padding: 10px 16px;
+ font-size: 1rem;
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ margin-right: 6px;
+ transition: background 0.2s;
+ }
+
+ #chat-input:focus {
+ background: #222327;
+ }
+
+ #send-btn {
+ background: #5b8fbd;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: bold;
+ transition: background 0.2s;
+ padding: 0 8px 0 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 40px;
+ width: 48px;
+ min-width: 48px;
+ }
+
+ #send-btn svg {
+ width: 28px;
+ height: 28px;
+ display: block;
+ }
+
+ #send-btn:active {
+ background: #3976a1;
+ }
+
+ #server-modal-bg,
+ #settings-modal-bg {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0,0,0,0.65);
+ display: none;
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+ }
+
+ #server-modal, #settings-modal {
+ background: #15171b;
+ color: #ddd;
+ border-radius: 12px;
+ padding: 26px 28px 18px 28px;
+ width: 340px;
+ box-shadow: 0 10px 36px #000a;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ }
+
+ #settings-modal {
+ width: 400px;
+ min-height: 240px;
+ }
+
+ #server-modal label, #settings-modal label {
+ font-size: 1em;
+ margin-bottom: 2px;
+ }
+
+ #server-modal input, #server-modal select,
+ #settings-modal input, #settings-modal select {
+ background: #23272a;
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ padding: 7px 10px;
+ font-size: 1em;
+ margin-bottom: 8px;
+ width: 100%;
+ box-sizing: border-box;
+ }
+
+ #server-modal .small-row {
+ display: flex;
+ gap: 8px;
+ }
+
+ #server-modal .small-row > div {
+ flex: 1;
+ }
+
+ #server-modal-btns, #settings-modal-btns {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+ }
+
+ #save-server-btn, #cancel-server-btn,
+ #save-settings-btn, #cancel-settings-btn {
+ font-size: 1em;
+ border-radius: 7px;
+ border: none;
+ padding: 8px 18px;
+ cursor: pointer;
+ font-weight: bold;
+ }
+
+ #save-server-btn, #save-settings-btn {
+ background: #5b8fbd;
+ color: #fff;
+ }
+
+ #save-server-btn:hover, #save-settings-btn:hover {
+ background: #3976a1;
+ }
+
+ #cancel-server-btn, #cancel-settings-btn {
+ background: #23272a;
+ color: #aaa;
+ }
+
+ #cancel-server-btn:hover, #cancel-settings-btn:hover {
+ background: #393c42;
+ color: #fff;
+ }
+
+ #server-modal-error, #settings-modal-error {
+ color: #ff9191;
+ font-size: 0.98em;
+ min-height: 18px;
+ }
+
+ .settings-format-presets {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 9px;
+ margin-bottom: 5px;
+ }
+
+ .settings-format-preset-btn {
+ background: #23272a;
+ border: none;
+ border-radius: 6px;
+ color: #fff;
+ padding: 6px 10px;
+ font-size: 1em;
+ cursor: pointer;
+ outline: none;
+ font-family: inherit;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ transition: background 0.16s, color 0.16s;
+ }
+
+ .settings-format-preset-btn.selected,
+ .settings-format-preset-btn:hover {
+ background: #1c2330;
+ }
+
+ .preset-snowdrop {
+ color: #aeff00;
+ }
+
+ .preset-brac {
+ color: #18b800;
+ }
+
+ .preset-crab {
+ color: #ff9191;
+ }
+
+ .preset-mefidroniy {
+ color: #e782ff;
+ }
+
+ .preset-crack {
+ color: #ffd700;
+ }
+
+ .preset-default {
+ color: #5b8fbd;
+ }
+
+ .preset-plain {
+ color: #fff;
+ }
+
+ #settings-format-input {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+
+ #settings-modal-lang-row {
+ display: flex;
+ align-items: center;
+ gap: 17px;
+ margin-bottom: 7px;
+ }
+
+ @media (max-width: 800px) {
+ #app-container {
+ flex-direction: column;
+ }
+
+ #channel-list {
+ width: 100vw;
+ border-right: none;
+ }
+
+ #main {
+ width: 100vw;
+ }
+
+ .nick {
+ min-width: 90px;
+ font-size: 0.99em;
+ }
+
+ .time {
+ min-width: 90px;
+ font-size: 0.89em;
+ }
+
+ #send-btn {
+ width: 40px;
+ min-width: 40px;
+ height: 36px;
+ }
+
+ #send-btn svg {
+ width: 22px;
+ height: 22px;
+ }
+
+ #server-modal, #settings-modal {
+ width: 95vw;
+ min-width: 0;
+ }
+ }
\ No newline at end of file