diff --git a/public/css/styles.css b/public/css/styles.css
new file mode 100644
index 000000000..84616cbd1
--- /dev/null
+++ b/public/css/styles.css
@@ -0,0 +1,581 @@
+/**
+ * Design System — Glassmorphism Dark Theme para Chat.
+ *
+ * Baseado no tema do projeto node_auth-app.
+ * Paleta: Indigo (#6366f1) + Dark (#0a0a0f) + Slate (#e2e8f0)
+ */
+
+/* === Google Fonts === */
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+
+/* === Reset & Base === */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+:root {
+ --bg: #0a0a0f;
+ --bg-card: rgba(18, 18, 26, 0.7);
+ --bg-card-solid: #12121a;
+ --bg-input: rgba(255, 255, 255, 0.05);
+ --bg-hover: rgba(255, 255, 255, 0.08);
+ --fg: #e2e8f0;
+ --fg-muted: #64748b;
+ --primary: #6366f1;
+ --primary-hover: #818cf8;
+ --primary-glow: rgba(99, 102, 241, 0.3);
+ --border: rgba(255, 255, 255, 0.08);
+ --border-active: rgba(99, 102, 241, 0.4);
+ --success: #10b981;
+ --danger: #ef4444;
+ --danger-hover: #dc2626;
+ --radius: 12px;
+ --radius-sm: 8px;
+ --radius-lg: 16px;
+ --shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+ --transition: 0.2s ease;
+}
+
+html, body {
+ height: 100%;
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ font-size: 16px;
+ color: var(--fg);
+ background: var(--bg);
+ -webkit-font-smoothing: antialiased;
+}
+
+body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+/* === Background Gradient === */
+body::before {
+ content: '';
+ position: fixed;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background:
+ radial-gradient(ellipse at 20% 50%, rgba(99, 102, 241, 0.08) 0%, transparent 50%),
+ radial-gradient(ellipse at 80% 20%, rgba(139, 92, 246, 0.06) 0%, transparent 50%),
+ radial-gradient(ellipse at 50% 80%, rgba(34, 211, 238, 0.04) 0%, transparent 50%);
+ z-index: -1;
+ animation: gradientShift 20s ease-in-out infinite;
+}
+
+@keyframes gradientShift {
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
+ 33% { transform: translate(2%, -1%) rotate(1deg); }
+ 66% { transform: translate(-1%, 2%) rotate(-1deg); }
+}
+
+/* === Username Screen === */
+.username-screen {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 32px;
+ text-align: center;
+ animation: fadeIn 0.5s ease;
+}
+
+.username-screen h1 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ letter-spacing: -0.03em;
+}
+
+.username-screen h1 span {
+ background: linear-gradient(135deg, var(--primary), #a78bfa);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.username-screen p {
+ color: var(--fg-muted);
+ font-size: 1.05rem;
+ max-width: 400px;
+}
+
+.username-form {
+ display: flex;
+ gap: 12px;
+ width: 100%;
+ max-width: 400px;
+}
+
+.username-error {
+ color: var(--danger);
+ font-size: 0.875rem;
+ min-height: 20px;
+}
+
+/* === Chat Layout === */
+.chat-app {
+ display: none;
+ width: 100vw;
+ height: 100vh;
+}
+
+.chat-app.active {
+ display: grid;
+ grid-template-columns: 280px 1fr;
+ animation: fadeIn 0.3s ease;
+}
+
+/* === Sidebar (Rooms) === */
+.sidebar {
+ background: var(--bg-card);
+ backdrop-filter: blur(20px);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: hidden;
+}
+
+.sidebar-header {
+ padding: 20px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.sidebar-header h2 {
+ font-size: 1.1rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.room-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+}
+
+.room-list::-webkit-scrollbar {
+ width: 4px;
+}
+
+.room-list::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 4px;
+}
+
+.room-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all var(--transition);
+ margin-bottom: 2px;
+ border: 1px solid transparent;
+}
+
+.room-item:hover {
+ background: var(--bg-hover);
+}
+
+.room-item.active {
+ background: rgba(99, 102, 241, 0.12);
+ border-color: var(--border-active);
+}
+
+.room-item .room-name {
+ font-size: 0.9rem;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.room-actions {
+ display: flex;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity var(--transition);
+}
+
+.room-item:hover .room-actions {
+ opacity: 1;
+}
+
+.room-actions button {
+ background: none;
+ border: none;
+ color: var(--fg-muted);
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ transition: all var(--transition);
+}
+
+.room-actions button:hover {
+ color: var(--fg);
+ background: var(--bg-hover);
+}
+
+.room-actions button.delete:hover {
+ color: var(--danger);
+}
+
+/* Sidebar footer (create room) */
+.sidebar-footer {
+ padding: 12px;
+ border-top: 1px solid var(--border);
+}
+
+.create-room-form {
+ display: flex;
+ gap: 8px;
+}
+
+/* === Main Chat Area === */
+.chat-main {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ background: var(--bg);
+}
+
+.chat-header {
+ padding: 16px 24px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: var(--bg-card);
+ backdrop-filter: blur(20px);
+}
+
+.chat-header-info h3 {
+ font-size: 1.05rem;
+ font-weight: 600;
+}
+
+.chat-header-info span {
+ font-size: 0.8rem;
+ color: var(--fg-muted);
+}
+
+.online-users {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.8rem;
+ color: var(--fg-muted);
+}
+
+.online-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--success);
+ box-shadow: 0 0 8px var(--success);
+}
+
+/* === Messages Area === */
+.messages-container {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.messages-container::-webkit-scrollbar {
+ width: 6px;
+}
+
+.messages-container::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 6px;
+}
+
+.message {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ max-width: 70%;
+ animation: slideUp 0.2s ease;
+}
+
+.message.own {
+ align-self: flex-end;
+}
+
+.message-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.75rem;
+}
+
+.message-author {
+ font-weight: 600;
+ color: var(--primary);
+}
+
+.message.own .message-author {
+ color: var(--success);
+}
+
+.message-time {
+ color: var(--fg-muted);
+}
+
+.message-bubble {
+ padding: 10px 16px;
+ border-radius: var(--radius);
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ font-size: 0.9rem;
+ line-height: 1.5;
+ word-break: break-word;
+}
+
+.message.own .message-bubble {
+ background: rgba(99, 102, 241, 0.15);
+ border-color: var(--border-active);
+}
+
+/* System message (join/leave/etc) */
+.message-system {
+ text-align: center;
+ color: var(--fg-muted);
+ font-size: 0.8rem;
+ font-style: italic;
+ padding: 8px 0;
+}
+
+/* Empty state */
+.empty-state {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: var(--fg-muted);
+ gap: 12px;
+}
+
+.empty-state .icon {
+ font-size: 3rem;
+ opacity: 0.3;
+}
+
+/* === Message Input === */
+.message-input-area {
+ padding: 16px 24px;
+ border-top: 1px solid var(--border);
+ background: var(--bg-card);
+ backdrop-filter: blur(20px);
+}
+
+.message-form {
+ display: flex;
+ gap: 12px;
+}
+
+/* === Shared Input Styles === */
+input[type="text"] {
+ flex: 1;
+ padding: 10px 16px;
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 0.9rem;
+ outline: none;
+ transition: all var(--transition);
+}
+
+input[type="text"]::placeholder {
+ color: var(--fg-muted);
+}
+
+input[type="text"]:focus {
+ border-color: var(--border-active);
+ box-shadow: 0 0 0 3px var(--primary-glow);
+ background: rgba(255, 255, 255, 0.08);
+}
+
+/* === Button Styles === */
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: var(--radius-sm);
+ font-family: inherit;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all var(--transition);
+ white-space: nowrap;
+}
+
+.btn-primary {
+ background: var(--primary);
+ color: #fff;
+ box-shadow: 0 4px 14px var(--primary-glow);
+}
+
+.btn-primary:hover {
+ background: var(--primary-hover);
+ transform: translateY(-1px);
+ box-shadow: 0 6px 20px var(--primary-glow);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+}
+
+.btn-sm {
+ padding: 6px 12px;
+ font-size: 0.8rem;
+}
+
+.btn-icon {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ width: 38px;
+ height: 38px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all var(--transition);
+ padding: 0;
+ font-size: 1.1rem;
+}
+
+.btn-icon:hover {
+ background: var(--bg-hover);
+ border-color: var(--border-active);
+}
+
+/* === Animations === */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideUp {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* === Responsive === */
+@media (max-width: 768px) {
+ .chat-app.active {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ position: fixed;
+ top: 0;
+ left: -280px;
+ width: 280px;
+ z-index: 100;
+ transition: left var(--transition);
+ box-shadow: var(--shadow);
+ }
+
+ .sidebar.open {
+ left: 0;
+ }
+
+ .sidebar-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 99;
+ }
+
+ .sidebar-overlay.open {
+ display: block;
+ }
+
+ .mobile-menu-btn {
+ display: flex !important;
+ }
+}
+
+@media (min-width: 769px) {
+ .mobile-menu-btn,
+ .sidebar-overlay {
+ display: none !important;
+ }
+}
+
+/* === Modal (rename) === */
+.modal-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(4px);
+ z-index: 200;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-overlay.active {
+ display: flex;
+}
+
+.modal {
+ background: var(--bg-card-solid);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 24px;
+ width: 90%;
+ max-width: 400px;
+ box-shadow: var(--shadow);
+ animation: fadeIn 0.2s ease;
+}
+
+.modal h3 {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 16px;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 16px;
+ justify-content: flex-end;
+}
+
+.btn-ghost {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--fg);
+}
+
+.btn-ghost:hover {
+ background: var(--bg-hover);
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 000000000..1c9e4ce27
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+ 💬 Chat em Tempo Real
+
+
+
+
+
+
+
+
💬 Chat
+
Digite seu nome para entrar no chat em tempo real
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
💬
+
Nenhuma mensagem ainda. Seja o primeiro!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
✏️ Renomear Sala
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/app.js b/public/js/app.js
new file mode 100644
index 000000000..a79887709
--- /dev/null
+++ b/public/js/app.js
@@ -0,0 +1,89 @@
+/**
+ * app.js — Orquestrador principal do Chat.
+ *
+ * Módulo raiz que importa e inicializa todos os sub-módulos.
+ * Responsabilidades:
+ * - Gerenciar fluxo de username (localStorage + join)
+ * - Reconexão automática via Socket.IO
+ * - Inicializar rooms, messages, modal e mobile
+ *
+ * @module app
+ */
+
+/* eslint-env browser */
+
+import { socket, state } from './state.js';
+import { initRoomHandlers } from './rooms.js';
+import { initMessageHandlers } from './messages.js';
+import { initModalHandlers } from './modal.js';
+import { initMobileHandlers } from './mobile.js';
+
+// === Referências DOM (apenas as do fluxo de username) ===
+const usernameScreen = document.getElementById('usernameScreen');
+const usernameForm = document.getElementById('usernameForm');
+const usernameInput = document.getElementById('usernameInput');
+const usernameError = document.getElementById('usernameError');
+const chatApp = document.getElementById('chatApp');
+const messageInput = document.getElementById('messageInput');
+
+// === Fluxo de Username ===
+
+/**
+ * Envia o username ao servidor e aguarda resposta.
+ * Se sucesso → mostra chat. Se erro → mostra mensagem.
+ * @param {string} userName - Nome de usuário desejado
+ */
+function joinWithUsername(userName) {
+ socket.emit('user:join', { username: userName }, (response) => {
+ if (response && response.success) {
+ state.username = userName;
+ localStorage.setItem('chat_username', userName);
+ usernameScreen.style.display = 'none';
+ chatApp.classList.add('active');
+ messageInput.focus();
+ usernameError.textContent = '';
+ } else {
+ usernameError.textContent = response
+ ? response.error
+ : 'Erro ao conectar';
+ usernameScreen.style.display = '';
+ chatApp.classList.remove('active');
+ }
+ });
+}
+
+// Formulário de username
+usernameForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ const userName = usernameInput.value.trim();
+
+ if (!userName) {
+ return;
+ }
+
+ joinWithUsername(userName);
+});
+
+// === Reconexão automática ===
+socket.on('connect', () => {
+ if (state.username && !usernameScreen.style.display) {
+ joinWithUsername(state.username);
+
+ if (state.currentRoomId) {
+ socket.emit('room:join', { roomId: state.currentRoomId });
+ }
+ }
+});
+
+// === Inicializar todos os módulos ===
+initRoomHandlers();
+initMessageHandlers();
+initModalHandlers();
+initMobileHandlers();
+
+// === Auto-login se já tiver username salvo ===
+if (state.username) {
+ usernameInput.value = state.username;
+ joinWithUsername(state.username);
+}
diff --git a/public/js/messages.js b/public/js/messages.js
new file mode 100644
index 000000000..8a3793ccc
--- /dev/null
+++ b/public/js/messages.js
@@ -0,0 +1,56 @@
+/**
+ * messages.js — Handlers de mensagens (Socket.IO + UI).
+ *
+ * Responsabilidades:
+ * - Receber histórico e novas mensagens do servidor
+ * - Enviar mensagens pelo formulário
+ * - Atualizar contagem de usuários online
+ *
+ * @module messages
+ */
+
+/* eslint-env browser */
+
+import { socket } from './state.js';
+import { renderMessages, appendMessage, scrollToBottom } from './ui.js';
+
+/**
+ * Inicializa todos os handlers de mensagem.
+ * Chamado uma única vez pelo app.js na inicialização.
+ */
+export function initMessageHandlers() {
+ const messageForm = document.getElementById('messageForm');
+ const messageInput = document.getElementById('messageInput');
+ const onlineCount = document.getElementById('onlineCount');
+
+ // room:history — Recebe histórico ao entrar na sala
+ socket.on('room:history', (messages) => {
+ renderMessages(messages);
+ });
+
+ // message:new — Nova mensagem em tempo real
+ socket.on('message:new', (message) => {
+ appendMessage(message);
+ scrollToBottom();
+ });
+
+ // user:list — Atualiza contagem de online
+ socket.on('user:list', (users) => {
+ onlineCount.textContent = users.length + ' online';
+ });
+
+ // Enviar mensagem via formulário
+ messageForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ const text = messageInput.value.trim();
+
+ if (!text) {
+ return;
+ }
+
+ socket.emit('message:send', { text });
+ messageInput.value = '';
+ messageInput.focus();
+ });
+}
diff --git a/public/js/mobile.js b/public/js/mobile.js
new file mode 100644
index 000000000..eac328a26
--- /dev/null
+++ b/public/js/mobile.js
@@ -0,0 +1,37 @@
+/**
+ * mobile.js — Menu responsivo da sidebar.
+ *
+ * Responsabilidades:
+ * - Abrir/fechar sidebar em telas pequenas
+ * - Controlar overlay de fundo
+ *
+ * @module mobile
+ */
+
+/* eslint-env browser */
+
+const sidebar = document.getElementById('sidebar');
+const sidebarOverlay = document.getElementById('sidebarOverlay');
+
+/**
+ * Fecha a sidebar mobile e o overlay.
+ */
+export function closeMobileSidebar() {
+ sidebar.classList.remove('open');
+ sidebarOverlay.classList.remove('open');
+}
+
+/**
+ * Inicializa os event listeners do menu mobile.
+ * Chamado uma única vez pelo app.js na inicialização.
+ */
+export function initMobileHandlers() {
+ const mobileMenuBtn = document.getElementById('mobileMenuBtn');
+
+ mobileMenuBtn.addEventListener('click', () => {
+ sidebar.classList.toggle('open');
+ sidebarOverlay.classList.toggle('open');
+ });
+
+ sidebarOverlay.addEventListener('click', closeMobileSidebar);
+}
diff --git a/public/js/modal.js b/public/js/modal.js
new file mode 100644
index 000000000..81203fb80
--- /dev/null
+++ b/public/js/modal.js
@@ -0,0 +1,85 @@
+/**
+ * modal.js — Modal de renomear sala.
+ *
+ * Responsabilidades:
+ * - Abrir/fechar o modal de rename
+ * - Enviar evento room:rename ao confirmar
+ * - Atalhos de teclado (Enter para confirmar, Escape para fechar)
+ *
+ * @module modal
+ */
+
+/* eslint-env browser */
+
+import { socket, state } from './state.js';
+
+const renameModal = document.getElementById('renameModal');
+const renameInput = document.getElementById('renameInput');
+const renameCancelBtn = document.getElementById('renameCancelBtn');
+const renameConfirmBtn = document.getElementById('renameConfirmBtn');
+
+/**
+ * Abre o modal de renomear preenchido com o nome atual.
+ * @param {string} roomId - ID da sala a renomear
+ * @param {string} currentName - Nome atual da sala
+ */
+export function openRenameModal(roomId, currentName) {
+ state.renameTargetId = roomId;
+ renameInput.value = currentName;
+ renameModal.classList.add('active');
+ renameInput.focus();
+}
+
+/**
+ * Fecha o modal de renomear e limpa o estado.
+ */
+export function closeRenameModal() {
+ renameModal.classList.remove('active');
+ state.renameTargetId = null;
+}
+
+/**
+ * Inicializa os event listeners do modal.
+ * Chamado uma única vez pelo app.js na inicialização.
+ */
+export function initModalHandlers() {
+ const currentRoomName = document.getElementById('currentRoomName');
+
+ // Botão cancelar
+ renameCancelBtn.addEventListener('click', closeRenameModal);
+
+ // Botão confirmar
+ renameConfirmBtn.addEventListener('click', () => {
+ const newName = renameInput.value.trim();
+
+ if (!newName || !state.renameTargetId) {
+ return;
+ }
+
+ socket.emit('room:rename', {
+ roomId: state.renameTargetId,
+ name: newName,
+ });
+
+ // Se é a sala ativa, atualiza o header
+ if (state.renameTargetId === state.currentRoomId) {
+ currentRoomName.textContent = newName;
+ }
+
+ closeRenameModal();
+ });
+
+ // Enter para confirmar rename
+ renameInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ renameConfirmBtn.click();
+ }
+ });
+
+ // Escape para fechar modal
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && renameModal.classList.contains('active')) {
+ closeRenameModal();
+ }
+ });
+}
diff --git a/public/js/rooms.js b/public/js/rooms.js
new file mode 100644
index 000000000..f4ec74177
--- /dev/null
+++ b/public/js/rooms.js
@@ -0,0 +1,143 @@
+/**
+ * rooms.js — Handlers de salas (Socket.IO + UI).
+ *
+ * Responsabilidades:
+ * - Renderizar a lista de salas na sidebar
+ * - Lidar com eventos room:list, room:default, room:destroyed, room:created
+ * - Criar salas via formulário
+ * - Entrar em salas ao clicar
+ *
+ * @module rooms
+ */
+
+/* eslint-env browser */
+
+import { socket, state } from './state.js';
+import { openRenameModal } from './modal.js';
+import { closeMobileSidebar } from './mobile.js';
+
+/**
+ * Inicializa todos os handlers de sala.
+ * Chamado uma única vez pelo app.js na inicialização.
+ */
+export function initRoomHandlers() {
+ const roomList = document.getElementById('roomList');
+ const currentRoomName = document.getElementById('currentRoomName');
+ const createRoomForm = document.getElementById('createRoomForm');
+ const newRoomInput = document.getElementById('newRoomInput');
+
+ // room:default — Recebe o ID da sala padrão "Geral"
+ socket.on('room:default', ({ roomId }) => {
+ state.defaultRoomId = roomId;
+ });
+
+ // room:list — Renderiza a sidebar com destaque na sala ativa
+ socket.on('room:list', (rooms) => {
+ roomList.innerHTML = '';
+
+ rooms.forEach((room) => {
+ const item = document.createElement('div');
+ const isActive = room.id === state.currentRoomId;
+
+ item.className = 'room-item' + (isActive ? ' active' : '');
+ item.dataset.roomId = room.id;
+
+ // Nome da sala
+ const nameSpan = document.createElement('span');
+
+ nameSpan.className = 'room-name';
+ nameSpan.textContent = room.name;
+
+ // Botões de ação (renomear, excluir)
+ const actions = document.createElement('div');
+
+ actions.className = 'room-actions';
+
+ // Não mostra ações na sala padrão "Geral"
+ if (room.id !== state.defaultRoomId) {
+ const renameBtn = document.createElement('button');
+
+ renameBtn.textContent = '✏️';
+ renameBtn.title = 'Renomear';
+
+ renameBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ openRenameModal(room.id, room.name);
+ });
+
+ const deleteBtn = document.createElement('button');
+
+ deleteBtn.textContent = '🗑️';
+ deleteBtn.title = 'Excluir';
+ deleteBtn.className = 'delete';
+
+ deleteBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ socket.emit('room:delete', { roomId: room.id });
+ });
+
+ actions.appendChild(renameBtn);
+ actions.appendChild(deleteBtn);
+ }
+
+ item.appendChild(nameSpan);
+ item.appendChild(actions);
+
+ // Click para entrar na sala
+ item.addEventListener('click', () => {
+ if (room.id !== state.currentRoomId) {
+ socket.emit('room:join', { roomId: room.id });
+ state.currentRoomId = room.id;
+ currentRoomName.textContent = room.name;
+ closeMobileSidebar();
+
+ // Atualiza destaque visual
+ document.querySelectorAll('.room-item').forEach((el) => {
+ el.classList.remove('active');
+ });
+ item.classList.add('active');
+ }
+ });
+
+ roomList.appendChild(item);
+ });
+
+ // Se ainda não está em nenhuma sala, entra na padrão
+ if (!state.currentRoomId && state.defaultRoomId) {
+ state.currentRoomId = state.defaultRoomId;
+
+ const defaultRoom = rooms.find((r) => r.id === state.defaultRoomId);
+
+ if (defaultRoom) {
+ currentRoomName.textContent = defaultRoom.name;
+ }
+ }
+ });
+
+ // room:destroyed — Sala excluída, redireciona para "Geral"
+ socket.on('room:destroyed', ({ redirectTo }) => {
+ state.currentRoomId = redirectTo;
+ currentRoomName.textContent = 'Geral';
+ });
+
+ // room:created — Sala criada com sucesso, entra automaticamente
+ socket.on('room:created', (room) => {
+ state.currentRoomId = room.id;
+ currentRoomName.textContent = room.name;
+ socket.emit('room:join', { roomId: room.id });
+ });
+
+ // Criar sala via formulário
+ createRoomForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ const roomName = newRoomInput.value.trim();
+
+ if (!roomName) {
+ return;
+ }
+
+ socket.emit('room:create', { name: roomName });
+ newRoomInput.value = '';
+ });
+}
diff --git a/public/js/state.js b/public/js/state.js
new file mode 100644
index 000000000..e25b18088
--- /dev/null
+++ b/public/js/state.js
@@ -0,0 +1,23 @@
+/**
+ * state.js — Estado global compartilhado e conexão Socket.IO.
+ *
+ * Centraliza todas as variáveis de estado do cliente
+ * e a instância do socket para que os demais módulos
+ * importem a mesma referência.
+ *
+ * @module state
+ */
+
+/* eslint-env browser */
+/* global io */
+
+// === Conexão Socket.IO ===
+export const socket = io();
+
+// === Estado mutável do cliente ===
+export const state = {
+ username: localStorage.getItem('chat_username') || '',
+ currentRoomId: null,
+ defaultRoomId: null,
+ renameTargetId: null,
+};
diff --git a/public/js/ui.js b/public/js/ui.js
new file mode 100644
index 000000000..3cf322e58
--- /dev/null
+++ b/public/js/ui.js
@@ -0,0 +1,96 @@
+/**
+ * ui.js — Funções de renderização de mensagens.
+ *
+ * Responsabilidades:
+ * - Renderizar histórico de mensagens
+ * - Adicionar mensagens individuais ao DOM
+ * - Scroll automático para o final
+ * - Prevenção de XSS (escapeHtml)
+ *
+ * @module ui
+ */
+
+/* eslint-env browser */
+
+import { state } from './state.js';
+
+/**
+ * Escapa caracteres HTML para prevenir XSS.
+ * @param {string} text - Texto a ser escapado
+ * @returns {string} Texto seguro para innerHTML
+ */
+export function escapeHtml(text) {
+ const div = document.createElement('div');
+
+ div.textContent = text;
+
+ return div.innerHTML;
+}
+
+/**
+ * Rola o container de mensagens para o final.
+ */
+export function scrollToBottom() {
+ const container = document.getElementById('messagesContainer');
+
+ container.scrollTop = container.scrollHeight;
+}
+
+/**
+ * Adiciona uma mensagem ao container.
+ * @param {{ author: string, text: string, time: string }} msg
+ */
+export function appendMessage(msg) {
+ const container = document.getElementById('messagesContainer');
+
+ // Remove o empty state se existir
+ const empty = container.querySelector('.empty-state');
+
+ if (empty) {
+ empty.remove();
+ }
+
+ const isOwn = msg.author === state.username;
+ const div = document.createElement('div');
+
+ div.className = 'message' + (isOwn ? ' own' : '');
+
+ const time = new Date(msg.time);
+ const timeStr = time.toLocaleTimeString('pt-BR', {
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+
+ div.innerHTML = `
+
+ ${escapeHtml(msg.text)}
+ `;
+
+ container.appendChild(div);
+}
+
+/**
+ * Renderiza todas as mensagens de uma sala (histórico completo).
+ * @param {{ author: string, text: string, time: string }[]} messages
+ */
+export function renderMessages(messages) {
+ const container = document.getElementById('messagesContainer');
+
+ if (messages.length === 0) {
+ container.innerHTML = `
+
+
💬
+
Nenhuma mensagem ainda. Seja o primeiro!
+
+ `;
+
+ return;
+ }
+
+ container.innerHTML = '';
+ messages.forEach((msg) => appendMessage(msg));
+ scrollToBottom();
+}