new frontend
This commit is contained in:
376
src/UI.js
376
src/UI.js
@@ -31,7 +31,6 @@ const uiState = {
|
|||||||
userColors: new Map(),
|
userColors: new Map(),
|
||||||
currentUser: 'IRCDUser',
|
currentUser: 'IRCDUser',
|
||||||
channelUsers: new Map(), // Map<channel, Set<user>>
|
channelUsers: new Map(), // Map<channel, Set<user>>
|
||||||
channelMessages: new Map(), // Map<channel, Array<{type, sender, content, timestamp, html}>>
|
|
||||||
peopleSearchQuery: '', // Current search query for people list
|
peopleSearchQuery: '', // Current search query for people list
|
||||||
virtualScrollState: {
|
virtualScrollState: {
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
@@ -280,8 +279,7 @@ function addChannel(name) {
|
|||||||
if (!uiState.channels.includes(normalizedName)) {
|
if (!uiState.channels.includes(normalizedName)) {
|
||||||
uiState.channels.push(normalizedName);
|
uiState.channels.push(normalizedName);
|
||||||
|
|
||||||
// Add to channel list (async DOM update)
|
// Add to channel list
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const channelItem = document.createElement('div');
|
const channelItem = document.createElement('div');
|
||||||
channelItem.className = 'channel-item';
|
channelItem.className = 'channel-item';
|
||||||
channelItem.innerHTML = `
|
channelItem.innerHTML = `
|
||||||
@@ -295,15 +293,9 @@ function addChannel(name) {
|
|||||||
`;
|
`;
|
||||||
channelItem.addEventListener('click', () => setActiveTab(normalizedName));
|
channelItem.addEventListener('click', () => setActiveTab(normalizedName));
|
||||||
channelList.appendChild(channelItem);
|
channelList.appendChild(channelItem);
|
||||||
});
|
|
||||||
|
|
||||||
// Add tab (async)
|
// Add tab
|
||||||
requestAnimationFrame(() => addTab(normalizedName));
|
addTab(normalizedName);
|
||||||
|
|
||||||
// Initialize message storage for this channel
|
|
||||||
if (!uiState.channelMessages.has(normalizedName)) {
|
|
||||||
uiState.channelMessages.set(normalizedName, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-join the channel when connected
|
// Auto-join the channel when connected
|
||||||
if (uiState.connected) {
|
if (uiState.connected) {
|
||||||
@@ -330,9 +322,6 @@ function removeChannel(name) {
|
|||||||
// Clear users for this channel
|
// Clear users for this channel
|
||||||
clearChannelUsers(name);
|
clearChannelUsers(name);
|
||||||
|
|
||||||
// Clear messages for this channel (optional - you might want to keep them)
|
|
||||||
// uiState.channelMessages.delete(name);
|
|
||||||
|
|
||||||
// Remove tab
|
// Remove tab
|
||||||
removeTab(name);
|
removeTab(name);
|
||||||
|
|
||||||
@@ -398,114 +387,51 @@ function setActiveTab(name) {
|
|||||||
|
|
||||||
uiState.activeTab = normalizedName;
|
uiState.activeTab = normalizedName;
|
||||||
|
|
||||||
// Update tabs (async)
|
// Update tabs
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const tabs = tabBar.querySelectorAll('.tab');
|
const tabs = tabBar.querySelectorAll('.tab');
|
||||||
tabs.forEach(tab => {
|
tabs.forEach(tab => {
|
||||||
// Compare with both normalized and original name
|
// Compare with both normalized and original name
|
||||||
tab.classList.toggle('active', tab.dataset.tab === normalizedName || tab.dataset.tab === name);
|
tab.classList.toggle('active', tab.dataset.tab === normalizedName || tab.dataset.tab === name);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Update channels (async)
|
// Update channels
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const channelItems = channelList.querySelectorAll('.channel-item');
|
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||||
channelItems.forEach(item => {
|
channelItems.forEach(item => {
|
||||||
const channelName = item.querySelector('.channel-name').textContent;
|
const channelName = item.querySelector('.channel-name').textContent;
|
||||||
item.classList.toggle('active', channelName === normalizedName || channelName === name);
|
item.classList.toggle('active', channelName === normalizedName || channelName === name);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Update people list for the active channel (async)
|
// Update people list for the active channel
|
||||||
updatePeopleList(normalizedName);
|
updatePeopleList(normalizedName);
|
||||||
|
|
||||||
// Update messages (async)
|
// Update messages
|
||||||
requestAnimationFrame(() => {
|
|
||||||
updateMessagesForTab(normalizedName);
|
updateMessagesForTab(normalizedName);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMessagesForTab(name) {
|
function updateMessagesForTab(name) {
|
||||||
// Clear messages
|
// Clear messages
|
||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
|
|
||||||
// Ensure channel name format is correct
|
// Add welcome messages
|
||||||
let channelKey = name;
|
|
||||||
if (name !== 'welcome' && !name.startsWith('#')) {
|
|
||||||
channelKey = '#' + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load stored messages from RAM
|
|
||||||
const storedMessages = uiState.channelMessages.get(channelKey);
|
|
||||||
if (storedMessages && storedMessages.length > 0) {
|
|
||||||
// Restore all stored messages (only once, not duplicated)
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
// Use a Set to track displayed messages and prevent duplicates
|
|
||||||
const displayedHashes = new Set();
|
|
||||||
|
|
||||||
storedMessages.forEach(msg => {
|
|
||||||
// Create a hash to detect duplicates
|
|
||||||
const msgHash = `${msg.type}-${msg.timestamp}-${msg.content.substring(0, 50)}`;
|
|
||||||
|
|
||||||
if (displayedHashes.has(msgHash)) {
|
|
||||||
return; // Skip duplicate
|
|
||||||
}
|
|
||||||
displayedHashes.add(msgHash);
|
|
||||||
|
|
||||||
if (msg.html) {
|
|
||||||
// For IRC messages, the HTML is the full formatted content
|
|
||||||
// The HTML from backend is a string of spans, so we wrap it in a div
|
|
||||||
const ircMessage = document.createElement('div');
|
|
||||||
ircMessage.className = 'irc-message';
|
|
||||||
|
|
||||||
// Use the stored HTML (which is the full formatted content from backend)
|
|
||||||
// If raw exists, prefer it, otherwise use html
|
|
||||||
const htmlContent = msg.raw || msg.html;
|
|
||||||
ircMessage.innerHTML = htmlContent;
|
|
||||||
|
|
||||||
messagesContainer.appendChild(ircMessage);
|
|
||||||
} else if (msg.type === 'message') {
|
|
||||||
// Recreate message element if HTML not stored
|
|
||||||
const messageLine = document.createElement('div');
|
|
||||||
messageLine.className = 'message-line';
|
|
||||||
|
|
||||||
const senderElement = document.createElement('div');
|
|
||||||
senderElement.className = `message-sender ${msg.isUser ? 'you' : ''}`;
|
|
||||||
senderElement.textContent = msg.isUser ? `${msg.sender} (You):` : `${msg.sender}:`;
|
|
||||||
senderElement.style.color = msg.isUser ? '' : getUserColor(msg.sender);
|
|
||||||
|
|
||||||
const contentElement = document.createElement('div');
|
|
||||||
contentElement.className = 'message-content';
|
|
||||||
contentElement.textContent = msg.content;
|
|
||||||
|
|
||||||
messageLine.appendChild(senderElement);
|
|
||||||
messageLine.appendChild(contentElement);
|
|
||||||
messagesContainer.appendChild(messageLine);
|
|
||||||
} else if (msg.type === 'system') {
|
|
||||||
const systemMessage = document.createElement('div');
|
|
||||||
systemMessage.className = 'system-message';
|
|
||||||
systemMessage.textContent = msg.content;
|
|
||||||
messagesContainer.appendChild(systemMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Add welcome messages for new channels (only if no stored messages)
|
|
||||||
if (name === 'welcome') {
|
if (name === 'welcome') {
|
||||||
// Don't store welcome messages, just display
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const welcomeMsg = document.createElement('div');
|
const welcomeMsg = document.createElement('div');
|
||||||
welcomeMsg.className = 'system-message';
|
welcomeMsg.className = 'system-message';
|
||||||
welcomeMsg.textContent = 'IRCD - Welcome! SystemTime: ' + new Date().toLocaleString();
|
welcomeMsg.textContent = 'IRCD - Welcome! SystemTime: ' + new Date().toLocaleString();
|
||||||
messagesContainer.appendChild(welcomeMsg);
|
messagesContainer.appendChild(welcomeMsg);
|
||||||
});
|
} else {
|
||||||
} else if (channelKey !== 'welcome') {
|
// For channels, just show a welcome message
|
||||||
// Only add welcome message if channel has no messages
|
const welcomeMsg = document.createElement('div');
|
||||||
addSystemMessage(`Welcome to ${channelKey}!`);
|
welcomeMsg.className = 'system-message';
|
||||||
addSystemMessage('This is the beginning of the channel.');
|
welcomeMsg.textContent = `Welcome to ${name}!`;
|
||||||
}
|
messagesContainer.appendChild(welcomeMsg);
|
||||||
|
|
||||||
|
const infoMsg = document.createElement('div');
|
||||||
|
infoMsg.className = 'system-message';
|
||||||
|
infoMsg.textContent = 'This is the beginning of the channel.';
|
||||||
|
messagesContainer.appendChild(infoMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message handling
|
// Message handling
|
||||||
@@ -545,19 +471,6 @@ function addMessage(sender, content, isUser = false) {
|
|||||||
const channel = uiState.activeTab;
|
const channel = uiState.activeTab;
|
||||||
if (!channel || channel === 'welcome') return;
|
if (!channel || channel === 'welcome') return;
|
||||||
|
|
||||||
// Normalize channel name
|
|
||||||
let channelKey = channel;
|
|
||||||
if (!channelKey.startsWith('#')) {
|
|
||||||
channelKey = '#' + channelKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store message in RAM - ALWAYS store
|
|
||||||
if (!uiState.channelMessages.has(channelKey)) {
|
|
||||||
uiState.channelMessages.set(channelKey, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use requestAnimationFrame for async DOM updates
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const messageLine = document.createElement('div');
|
const messageLine = document.createElement('div');
|
||||||
messageLine.className = 'message-line';
|
messageLine.className = 'message-line';
|
||||||
|
|
||||||
@@ -573,261 +486,29 @@ function addMessage(sender, content, isUser = false) {
|
|||||||
messageLine.appendChild(senderElement);
|
messageLine.appendChild(senderElement);
|
||||||
messageLine.appendChild(contentElement);
|
messageLine.appendChild(contentElement);
|
||||||
|
|
||||||
// Store FULL message data with HTML
|
|
||||||
const messageData = {
|
|
||||||
type: 'message',
|
|
||||||
sender,
|
|
||||||
content,
|
|
||||||
isUser,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
html: messageLine.outerHTML, // Full HTML content
|
|
||||||
raw: messageLine.outerHTML, // Keep raw HTML
|
|
||||||
formatted: true // Mark as formatted
|
|
||||||
};
|
|
||||||
|
|
||||||
uiState.channelMessages.get(channelKey).push(messageData);
|
|
||||||
console.log(`[STORE] Stored FULL user message in ${channelKey} (${content.substring(0, 50)}...)`);
|
|
||||||
|
|
||||||
// Update activeTab to normalized name for consistency
|
|
||||||
if (uiState.activeTab !== channelKey) {
|
|
||||||
uiState.activeTab = channelKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesContainer.appendChild(messageLine);
|
messagesContainer.appendChild(messageLine);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSystemMessage(content) {
|
function addSystemMessage(content) {
|
||||||
const channel = uiState.activeTab;
|
const channel = uiState.activeTab;
|
||||||
if (!channel || channel === 'welcome') return;
|
if (!channel || channel === 'welcome') return;
|
||||||
|
|
||||||
// Normalize channel name
|
|
||||||
let channelKey = channel;
|
|
||||||
if (!channelKey.startsWith('#')) {
|
|
||||||
channelKey = '#' + channelKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store message in RAM - ALWAYS store
|
|
||||||
if (!uiState.channelMessages.has(channelKey)) {
|
|
||||||
uiState.channelMessages.set(channelKey, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use requestAnimationFrame for async DOM updates
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const systemMessage = document.createElement('div');
|
const systemMessage = document.createElement('div');
|
||||||
systemMessage.className = 'system-message';
|
systemMessage.className = 'system-message';
|
||||||
systemMessage.textContent = content;
|
systemMessage.textContent = content;
|
||||||
|
|
||||||
// Store FULL message data with HTML
|
|
||||||
const messageData = {
|
|
||||||
type: 'system',
|
|
||||||
sender: null,
|
|
||||||
content,
|
|
||||||
isUser: false,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
html: systemMessage.outerHTML, // Full HTML content
|
|
||||||
raw: systemMessage.outerHTML, // Keep raw HTML
|
|
||||||
formatted: true // Mark as formatted
|
|
||||||
};
|
|
||||||
|
|
||||||
uiState.channelMessages.get(channelKey).push(messageData);
|
|
||||||
console.log(`[STORE] Stored FULL system message in ${channelKey} (${content.substring(0, 50)}...)`);
|
|
||||||
|
|
||||||
// Update activeTab to normalized name for consistency
|
|
||||||
if (uiState.activeTab !== channelKey) {
|
|
||||||
uiState.activeTab = channelKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesContainer.appendChild(systemMessage);
|
messagesContainer.appendChild(systemMessage);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addIRCMessage(content) {
|
function addIRCMessage(content) {
|
||||||
// Parse the message to determine which channel it belongs to
|
|
||||||
let targetChannel = null;
|
|
||||||
let joinMatch = null;
|
|
||||||
let partMatch = null;
|
|
||||||
|
|
||||||
// Extract plain text for checking
|
|
||||||
const plainText = content.replace(/<[^>]*>/g, '');
|
|
||||||
|
|
||||||
// Check if this is a server notice (starts with server name, no channel)
|
|
||||||
const isServerNotice = /^[a-zA-Z0-9.-]+\s+\[NOTICE\]/.test(plainText) ||
|
|
||||||
/^[a-zA-Z0-9.-]+\s+NOTICE/.test(plainText);
|
|
||||||
|
|
||||||
// Try to extract channel from PRIVMSG
|
|
||||||
// PRIVMSG format: <sender> → <channel>: <message>
|
|
||||||
const privmsgMatch = content.match(/→\s*<span[^>]*>([^<]+)<\/span><span[^>]*>:\s*<\/span>/);
|
|
||||||
if (privmsgMatch) {
|
|
||||||
targetChannel = privmsgMatch[1].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTICE format: [NOTICE] <sender> → <target>: <message>
|
|
||||||
// Only if target is a channel (starts with #)
|
|
||||||
const noticeMatch = content.match(/\[NOTICE\].*?→\s*<span[^>]*>([^<]+)<\/span><span[^>]*>:\s*<\/span>/);
|
|
||||||
if (noticeMatch) {
|
|
||||||
const target = noticeMatch[1].trim();
|
|
||||||
// Only treat as channel message if target starts with #
|
|
||||||
if (target.startsWith('#')) {
|
|
||||||
targetChannel = target;
|
|
||||||
} else if (!isServerNotice) {
|
|
||||||
// If it's a user notice, check if we're in a PM (future feature)
|
|
||||||
// For now, skip storing user notices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JOIN format: <sender> joined <channel>
|
|
||||||
joinMatch = content.match(/joined\s*<span[^>]*>([^<]+)<\/span>/);
|
|
||||||
if (joinMatch) {
|
|
||||||
targetChannel = joinMatch[1].trim();
|
|
||||||
// Extract sender name from the beginning of the message
|
|
||||||
const senderMatch = plainText.match(/^([^\s]+)\s+joined/);
|
|
||||||
if (senderMatch) {
|
|
||||||
const sender = senderMatch[1];
|
|
||||||
console.log(`[JOIN] ${sender} joined ${targetChannel}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PART format: <sender> left <channel>
|
|
||||||
partMatch = content.match(/left\s*<span[^>]*>([^<]+)<\/span>/);
|
|
||||||
if (partMatch) {
|
|
||||||
targetChannel = partMatch[1].trim();
|
|
||||||
// Extract sender name from the beginning of the message
|
|
||||||
const senderMatch = plainText.match(/^([^\s]+)\s+left/);
|
|
||||||
if (senderMatch) {
|
|
||||||
const sender = senderMatch[1];
|
|
||||||
console.log(`[PART] ${sender} left ${targetChannel}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// QUIT format: <sender> quit (reason)
|
|
||||||
const quitMatch = plainText.match(/^([^\s]+)\s+quit/);
|
|
||||||
if (quitMatch) {
|
|
||||||
// QUIT messages don't have a channel, but we should still display them
|
|
||||||
const sender = quitMatch[1];
|
|
||||||
console.log(`[QUIT] ${sender} quit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NICK format: <sender> is now known as <newnick>
|
|
||||||
const nickMatch = plainText.match(/^([^\s]+)\s+is now known as\s+([^\s]+)/);
|
|
||||||
if (nickMatch) {
|
|
||||||
const sender = nickMatch[1];
|
|
||||||
const newNick = nickMatch[2];
|
|
||||||
console.log(`[NICK] ${sender} is now known as ${newNick}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NAMES format: [NAMES] <channel>:
|
|
||||||
const namesMatch = content.match(/\[NAMES\]\s*<\/span>\s*<span[^>]*>([^<]+)<\/span>/);
|
|
||||||
if (namesMatch) {
|
|
||||||
targetChannel = namesMatch[1].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize channel name
|
|
||||||
if (targetChannel && !targetChannel.startsWith('#')) {
|
|
||||||
targetChannel = '#' + targetChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize active channel for comparison
|
|
||||||
let activeChannel = uiState.activeTab;
|
|
||||||
if (activeChannel && activeChannel !== 'welcome' && !activeChannel.startsWith('#')) {
|
|
||||||
activeChannel = '#' + activeChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine where to store the message - SIMPLE AND RELIABLE
|
|
||||||
const isJoinOrPart = joinMatch || partMatch;
|
|
||||||
|
|
||||||
// Always store in active channel (where user is viewing)
|
|
||||||
// This ensures all messages seen by the user are stored
|
|
||||||
let storeChannel = activeChannel || 'welcome';
|
|
||||||
|
|
||||||
// If message has a specific target channel (PRIVMSG, JOIN, PART, etc.), also store there
|
|
||||||
// But primary storage is always the active channel
|
|
||||||
if (targetChannel && targetChannel.startsWith('#')) {
|
|
||||||
// Store in both the target channel AND the active channel
|
|
||||||
// This way messages are available when switching channels
|
|
||||||
const channelsToStore = [storeChannel, targetChannel];
|
|
||||||
|
|
||||||
channelsToStore.forEach(ch => {
|
|
||||||
if (ch && ch !== 'welcome') {
|
|
||||||
if (!uiState.channelMessages.has(ch)) {
|
|
||||||
uiState.channelMessages.set(ch, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingMessages = uiState.channelMessages.get(ch);
|
|
||||||
const now = Date.now();
|
|
||||||
const duplicateWindow = isJoinOrPart ? 5000 : 2000;
|
|
||||||
|
|
||||||
// Check for duplicates
|
|
||||||
const isDuplicate = existingMessages.some(msg =>
|
|
||||||
msg.type === 'irc' &&
|
|
||||||
msg.content === plainText &&
|
|
||||||
Math.abs(msg.timestamp - now) < duplicateWindow
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDuplicate) {
|
|
||||||
const messageData = {
|
|
||||||
type: 'irc',
|
|
||||||
sender: null,
|
|
||||||
content: plainText,
|
|
||||||
isUser: false,
|
|
||||||
timestamp: now,
|
|
||||||
html: content, // Full HTML content
|
|
||||||
raw: content, // Keep raw HTML
|
|
||||||
formatted: true // Mark as formatted
|
|
||||||
};
|
|
||||||
|
|
||||||
existingMessages.push(messageData);
|
|
||||||
console.log(`[STORE] Stored FULL message in ${ch} (${plainText.substring(0, 50)}...)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// No target channel - store only in active channel
|
|
||||||
if (storeChannel && storeChannel !== 'welcome') {
|
|
||||||
if (!uiState.channelMessages.has(storeChannel)) {
|
|
||||||
uiState.channelMessages.set(storeChannel, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingMessages = uiState.channelMessages.get(storeChannel);
|
|
||||||
const now = Date.now();
|
|
||||||
const duplicateWindow = isJoinOrPart ? 5000 : 2000;
|
|
||||||
|
|
||||||
const isDuplicate = existingMessages.some(msg =>
|
|
||||||
msg.type === 'irc' &&
|
|
||||||
msg.content === plainText &&
|
|
||||||
Math.abs(msg.timestamp - now) < duplicateWindow
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDuplicate) {
|
|
||||||
const messageData = {
|
|
||||||
type: 'irc',
|
|
||||||
sender: null,
|
|
||||||
content: plainText,
|
|
||||||
isUser: false,
|
|
||||||
timestamp: now,
|
|
||||||
html: content, // Full HTML content
|
|
||||||
raw: content, // Keep raw HTML
|
|
||||||
formatted: true // Mark as formatted
|
|
||||||
};
|
|
||||||
|
|
||||||
existingMessages.push(messageData);
|
|
||||||
console.log(`[STORE] Stored FULL message in ${storeChannel} (no target channel) (${plainText.substring(0, 50)}...)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display ALL messages in the active channel - no filtering
|
|
||||||
// Always display, regardless of target channel
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const ircMessage = document.createElement('div');
|
const ircMessage = document.createElement('div');
|
||||||
ircMessage.className = 'irc-message';
|
ircMessage.className = 'irc-message';
|
||||||
ircMessage.innerHTML = content; // Use innerHTML to render HTML formatting
|
ircMessage.innerHTML = content;
|
||||||
|
|
||||||
messagesContainer.appendChild(ircMessage);
|
messagesContainer.appendChild(ircMessage);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
@@ -959,7 +640,6 @@ function updatePeopleList(channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort and filter users (all stored in RAM, not DOM)
|
// Sort and filter users (all stored in RAM, not DOM)
|
||||||
setTimeout(() => {
|
|
||||||
let sortedUsers = Array.from(users).sort((a, b) => {
|
let sortedUsers = Array.from(users).sort((a, b) => {
|
||||||
const aIsOp = a.startsWith('@');
|
const aIsOp = a.startsWith('@');
|
||||||
const bIsOp = b.startsWith('@');
|
const bIsOp = b.startsWith('@');
|
||||||
@@ -994,7 +674,6 @@ function updatePeopleList(channel) {
|
|||||||
|
|
||||||
// Update virtual scroll - only renders visible items
|
// Update virtual scroll - only renders visible items
|
||||||
renderVisibleUsers();
|
renderVisibleUsers();
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVisibleUsers() {
|
function renderVisibleUsers() {
|
||||||
@@ -1039,7 +718,6 @@ function renderVisibleUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear and render only visible items
|
// Clear and render only visible items
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!peopleList) return;
|
if (!peopleList) return;
|
||||||
|
|
||||||
peopleList.innerHTML = '';
|
peopleList.innerHTML = '';
|
||||||
@@ -1052,7 +730,6 @@ function renderVisibleUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
peopleList.appendChild(fragment);
|
peopleList.appendChild(fragment);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPeopleItem(user) {
|
function createPeopleItem(user) {
|
||||||
@@ -1098,25 +775,19 @@ function handlePeopleSearch(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePeopleListScroll() {
|
function handlePeopleListScroll() {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
renderVisibleUsers();
|
renderVisibleUsers();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVirtualScrollContainerHeight() {
|
function updateVirtualScrollContainerHeight() {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (peopleListContainer) {
|
if (peopleListContainer) {
|
||||||
uiState.virtualScrollState.containerHeight = peopleListContainer.clientHeight;
|
uiState.virtualScrollState.containerHeight = peopleListContainer.clientHeight;
|
||||||
renderVisibleUsers();
|
renderVisibleUsers();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNamesMessage(channel, usersStr) {
|
function parseNamesMessage(channel, usersStr) {
|
||||||
if (!channel || !usersStr) return;
|
if (!channel || !usersStr) return;
|
||||||
|
|
||||||
// Process parsing asynchronously
|
|
||||||
setTimeout(() => {
|
|
||||||
// Initialize users set for this channel if it doesn't exist
|
// Initialize users set for this channel if it doesn't exist
|
||||||
// Note: We accumulate users from multiple NAMES messages until ENDOFNAMES
|
// Note: We accumulate users from multiple NAMES messages until ENDOFNAMES
|
||||||
// Users are stored in RAM, not DOM
|
// Users are stored in RAM, not DOM
|
||||||
@@ -1135,12 +806,11 @@ function parseNamesMessage(channel, usersStr) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the people list if this is the active channel (async)
|
// Update the people list if this is the active channel
|
||||||
// This will use virtual scrolling to only render visible users
|
// This will use virtual scrolling to only render visible users
|
||||||
if (uiState.activeTab === channel) {
|
if (uiState.activeTab === channel) {
|
||||||
updatePeopleList(channel);
|
updatePeopleList(channel);
|
||||||
}
|
}
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearChannelUsers(channel) {
|
function clearChannelUsers(channel) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link rel="stylesheet" href="./styles/styles.css">
|
<link rel="stylesheet" href="./styles/styles.css">
|
||||||
|
<link rel="stylesheet" href="./styles/mobile.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<aside class="sidebar" id="channels">
|
<aside class="sidebar" id="channels">
|
||||||
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h1>IRCd</h1>
|
<h1>IRCd<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M10 3H20M4 21H14M15 3L9 21" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></h1>
|
||||||
<button class="toggle-sidebar" id="toggleSidebar">
|
<button class="toggle-sidebar" id="toggleSidebar">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
@@ -134,5 +135,6 @@
|
|||||||
|
|
||||||
<script src="tauri.js"></script>
|
<script src="tauri.js"></script>
|
||||||
<script src="UI.js"></script>
|
<script src="UI.js"></script>
|
||||||
|
<script src="mobile.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
613
src/mobile.js
Normal file
613
src/mobile.js
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
// mobile.js - Fixed fullscreen sidebar behavior with textbox preservation
|
||||||
|
|
||||||
|
class MobileManager {
|
||||||
|
constructor() {
|
||||||
|
this.breakpoints = {
|
||||||
|
desktop: 1024,
|
||||||
|
tablet: 768,
|
||||||
|
mobile: 480,
|
||||||
|
narrow: 320,
|
||||||
|
superNarrow: 280
|
||||||
|
};
|
||||||
|
this.currentBreakpoint = this.getCurrentBreakpoint();
|
||||||
|
this.sidebarOpen = false;
|
||||||
|
this.peopleSidebarOpen = false;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentBreakpoint() {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
if (width >= this.breakpoints.desktop) return 'desktop';
|
||||||
|
if (width >= this.breakpoints.tablet) return 'tablet';
|
||||||
|
if (width >= this.breakpoints.mobile) return 'mobile';
|
||||||
|
if (width >= this.breakpoints.narrow) return 'narrow';
|
||||||
|
return 'super-narrow';
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.log('Initializing mobile interface for:', this.currentBreakpoint);
|
||||||
|
this.createMobileOverlay();
|
||||||
|
this.createMobileHeader();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.overrideCollapseButtons();
|
||||||
|
this.applyResponsiveStyles();
|
||||||
|
this.handleResize();
|
||||||
|
this.enhanceTouchInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
createMobileOverlay() {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'mobile-overlay';
|
||||||
|
overlay.id = 'mobileOverlay';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
overlay.addEventListener('click', () => {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createMobileHeader() {
|
||||||
|
const mainContent = document.querySelector('.main-content');
|
||||||
|
if (!mainContent) return;
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'mobile-header';
|
||||||
|
header.innerHTML = `
|
||||||
|
<button class="mobile-menu-button" id="mobileMenuButton" aria-label="Toggle channels">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 12H21M3 6H21M3 18H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h1 class="mobile-title" id="mobileTitle">IRCD</h1>
|
||||||
|
<button class="mobile-menu-button" id="mobilePeopleButton" aria-label="Toggle people list">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 21V19C17 17.9391 16.5786 16.9217 15.8284 16.1716C15.0783 15.4214 14.0609 15 13 15H5C3.93913 15 2.92172 15.4214 2.17157 16.1716C1.42143 16.9217 1 17.9391 1 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
mainContent.insertBefore(header, mainContent.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideCollapseButtons() {
|
||||||
|
// Override the channels sidebar toggle button (left sidebar)
|
||||||
|
const sidebarToggle = document.getElementById('toggleSidebar');
|
||||||
|
if (sidebarToggle) {
|
||||||
|
const newToggle = sidebarToggle.cloneNode(true);
|
||||||
|
sidebarToggle.parentNode.replaceChild(newToggle, sidebarToggle);
|
||||||
|
|
||||||
|
newToggle.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.isMobile()) {
|
||||||
|
// On mobile: close the channels sidebar instead of collapsing
|
||||||
|
this.closeSidebar();
|
||||||
|
} else {
|
||||||
|
// On desktop: use original collapse behavior for channels sidebar
|
||||||
|
if (typeof window.toggleSidebar === 'function') {
|
||||||
|
window.toggleSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the people sidebar toggle button (right sidebar)
|
||||||
|
const peopleToggle = document.getElementById('togglePeopleSidebar');
|
||||||
|
if (peopleToggle) {
|
||||||
|
const newPeopleToggle = peopleToggle.cloneNode(true);
|
||||||
|
peopleToggle.parentNode.replaceChild(newPeopleToggle, peopleToggle);
|
||||||
|
|
||||||
|
newPeopleToggle.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.isMobile()) {
|
||||||
|
// On mobile: close the people sidebar instead of collapsing
|
||||||
|
this.closePeopleSidebar();
|
||||||
|
} else {
|
||||||
|
// On desktop: use original collapse behavior for people sidebar
|
||||||
|
if (typeof window.togglePeopleSidebar === 'function') {
|
||||||
|
window.togglePeopleSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToggleButtonIcons() {
|
||||||
|
const sidebarToggle = document.getElementById('toggleSidebar');
|
||||||
|
const peopleToggle = document.getElementById('togglePeopleSidebar');
|
||||||
|
|
||||||
|
if (sidebarToggle) {
|
||||||
|
const icon = sidebarToggle.querySelector('svg');
|
||||||
|
if (icon) {
|
||||||
|
if (this.isMobile() && this.sidebarOpen) {
|
||||||
|
// Show close icon (X) when channels sidebar is open on mobile
|
||||||
|
icon.innerHTML = '<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
} else {
|
||||||
|
// Show collapse icon (chevron)
|
||||||
|
icon.innerHTML = '<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peopleToggle) {
|
||||||
|
const icon = peopleToggle.querySelector('svg');
|
||||||
|
if (icon) {
|
||||||
|
if (this.isMobile() && this.peopleSidebarOpen) {
|
||||||
|
// Show close icon (X) when people sidebar is open on mobile
|
||||||
|
icon.innerHTML = '<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
} else {
|
||||||
|
// Show collapse icon (chevron)
|
||||||
|
icon.innerHTML = '<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Mobile menu buttons - only control channels and people sidebars
|
||||||
|
document.getElementById('mobileMenuButton')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleSidebar();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('mobilePeopleButton')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.togglePeopleSidebar();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
this.setupTouchEvents();
|
||||||
|
|
||||||
|
// Orientation and resize
|
||||||
|
window.addEventListener('orientationchange', () => {
|
||||||
|
setTimeout(() => this.handleResize(), 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => this.throttle(this.handleResize.bind(this), 250));
|
||||||
|
|
||||||
|
// Fixed: Don't close sidebars when clicking on form elements
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (this.isMobile() &&
|
||||||
|
!e.target.closest('#channels') &&
|
||||||
|
!e.target.closest('#peopleSidebar') &&
|
||||||
|
!e.target.closest('.mobile-menu-button') &&
|
||||||
|
// Don't close sidebars when clicking on form inputs
|
||||||
|
!e.target.matches('input, textarea, select')) {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard handling
|
||||||
|
this.setupKeyboardHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTouchEvents() {
|
||||||
|
let touchStartX = 0;
|
||||||
|
let touchStartY = 0;
|
||||||
|
|
||||||
|
document.addEventListener('touchstart', (e) => {
|
||||||
|
touchStartX = e.touches[0].clientX;
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('touchend', (e) => {
|
||||||
|
if (!this.isMobile()) return;
|
||||||
|
|
||||||
|
const touchEndX = e.changedTouches[0].clientX;
|
||||||
|
const touchEndY = e.changedTouches[0].clientY;
|
||||||
|
const diffX = touchEndX - touchStartX;
|
||||||
|
const diffY = touchEndY - touchStartY;
|
||||||
|
|
||||||
|
// Only handle horizontal swipes with minimal vertical movement
|
||||||
|
if (Math.abs(diffX) > 50 && Math.abs(diffY) < 30) {
|
||||||
|
if (diffX > 0) {
|
||||||
|
// Swipe right - open channels sidebar
|
||||||
|
this.toggleSidebar();
|
||||||
|
} else {
|
||||||
|
// Swipe left - open people sidebar or close open ones
|
||||||
|
if (this.sidebarOpen) {
|
||||||
|
this.closeSidebar();
|
||||||
|
} else {
|
||||||
|
this.togglePeopleSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Better touch feedback
|
||||||
|
document.querySelectorAll('button, .channel-item, .people-item, .tab').forEach(element => {
|
||||||
|
element.addEventListener('touchstart', function() {
|
||||||
|
this.classList.add('touch-active');
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('touchend', function() {
|
||||||
|
this.classList.remove('touch-active');
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('touchcancel', function() {
|
||||||
|
this.classList.remove('touch-active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupKeyboardHandling() {
|
||||||
|
if ('visualViewport' in window) {
|
||||||
|
const visualViewport = window.visualViewport;
|
||||||
|
|
||||||
|
visualViewport.addEventListener('resize', () => {
|
||||||
|
this.handleKeyboardResize(visualViewport);
|
||||||
|
});
|
||||||
|
|
||||||
|
visualViewport.addEventListener('scroll', () => {
|
||||||
|
this.handleKeyboardScroll(visualViewport);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed: Only auto-close sidebars when focusing on main message input
|
||||||
|
document.addEventListener('focusin', (e) => {
|
||||||
|
if (this.isMobile() && e.target.id === 'messageInput') {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
}
|
||||||
|
// Don't close sidebars for other input types (connection form, etc.)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyboardResize(viewport) {
|
||||||
|
// Handle virtual keyboard appearance
|
||||||
|
if (this.isMobile()) {
|
||||||
|
const composer = document.querySelector('.composer');
|
||||||
|
if (composer) {
|
||||||
|
composer.style.marginBottom = '0px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyboardScroll(viewport) {
|
||||||
|
// Adjust scroll when keyboard appears
|
||||||
|
if (this.isMobile()) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const messages = document.querySelector('.messages');
|
||||||
|
if (messages) {
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
const sidebar = document.getElementById('channels');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
|
||||||
|
if (!sidebar || !overlay) return;
|
||||||
|
|
||||||
|
if (this.sidebarOpen) {
|
||||||
|
this.closeSidebar();
|
||||||
|
} else {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
sidebar.classList.add('active');
|
||||||
|
overlay.classList.add('active');
|
||||||
|
this.sidebarOpen = true;
|
||||||
|
this.updateMobileTitle('Channels');
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePeopleSidebar() {
|
||||||
|
const peopleSidebar = document.getElementById('peopleSidebar');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
|
||||||
|
if (!peopleSidebar || !overlay) return;
|
||||||
|
|
||||||
|
if (this.peopleSidebarOpen) {
|
||||||
|
this.closePeopleSidebar();
|
||||||
|
} else {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
peopleSidebar.classList.add('active');
|
||||||
|
overlay.classList.add('active');
|
||||||
|
this.peopleSidebarOpen = true;
|
||||||
|
this.updateMobileTitle('People');
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSidebar() {
|
||||||
|
const sidebar = document.getElementById('channels');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
|
||||||
|
if (sidebar) sidebar.classList.remove('active');
|
||||||
|
if (overlay) overlay.classList.remove('active');
|
||||||
|
this.sidebarOpen = false;
|
||||||
|
this.updateMobileTitle();
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
closePeopleSidebar() {
|
||||||
|
const peopleSidebar = document.getElementById('peopleSidebar');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
|
||||||
|
if (peopleSidebar) peopleSidebar.classList.remove('active');
|
||||||
|
if (overlay) overlay.classList.remove('active');
|
||||||
|
this.peopleSidebarOpen = false;
|
||||||
|
this.updateMobileTitle();
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllSidebars() {
|
||||||
|
this.closeSidebar();
|
||||||
|
this.closePeopleSidebar();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMobileTitle(title = '') {
|
||||||
|
const mobileTitle = document.getElementById('mobileTitle');
|
||||||
|
if (!mobileTitle) return;
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
mobileTitle.textContent = this.truncateText(title, this.getMaxTitleLength());
|
||||||
|
} else {
|
||||||
|
const activeTab = document.querySelector('.tab.active');
|
||||||
|
if (activeTab) {
|
||||||
|
const tabText = activeTab.textContent.replace('×', '').trim();
|
||||||
|
mobileTitle.textContent = this.truncateText(tabText, this.getMaxTitleLength());
|
||||||
|
} else {
|
||||||
|
mobileTitle.textContent = 'IRCD';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
truncateText(text, maxLength) {
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return text.substring(0, maxLength - 3) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxTitleLength() {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
if (width < this.breakpoints.superNarrow) return 12;
|
||||||
|
if (width < this.breakpoints.narrow) return 15;
|
||||||
|
if (width < this.breakpoints.mobile) return 20;
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyResponsiveStyles() {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = this.generateResponsiveCSS();
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
this.updateResponsiveClasses();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResponsiveClasses() {
|
||||||
|
const breakpoint = this.currentBreakpoint;
|
||||||
|
document.body.className = document.body.className.replace(/\b(desktop|tablet|mobile|narrow-screen|super-narrow)\b/g, '');
|
||||||
|
document.body.classList.add(breakpoint === 'narrow' ? 'narrow-screen' : breakpoint);
|
||||||
|
document.body.classList.add('mobile-device');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize() {
|
||||||
|
const previousBreakpoint = this.currentBreakpoint;
|
||||||
|
this.currentBreakpoint = this.getCurrentBreakpoint();
|
||||||
|
|
||||||
|
if (previousBreakpoint !== this.currentBreakpoint) {
|
||||||
|
console.log('Breakpoint changed:', previousBreakpoint, '→', this.currentBreakpoint);
|
||||||
|
this.updateResponsiveClasses();
|
||||||
|
this.updateMobileTitle();
|
||||||
|
this.adjustLayoutForBreakpoint();
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
|
||||||
|
// Close sidebars when switching to mobile layout
|
||||||
|
if (this.isMobile() && !previousBreakpoint.includes('mobile') && !previousBreakpoint.includes('narrow')) {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.adjustComposerForMobile();
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustLayoutForBreakpoint() {
|
||||||
|
const breakpoint = this.currentBreakpoint;
|
||||||
|
|
||||||
|
// Adjust tab bar for narrow screens
|
||||||
|
const tabs = document.querySelector('.tabs');
|
||||||
|
if (tabs) {
|
||||||
|
if (breakpoint === 'super-narrow') {
|
||||||
|
tabs.style.flexWrap = 'nowrap';
|
||||||
|
tabs.style.overflowX = 'auto';
|
||||||
|
} else {
|
||||||
|
tabs.style.flexWrap = '';
|
||||||
|
tabs.style.overflowX = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust message layout
|
||||||
|
const messages = document.querySelector('.messages');
|
||||||
|
if (messages) {
|
||||||
|
if (breakpoint === 'super-narrow') {
|
||||||
|
messages.classList.add('narrow-layout');
|
||||||
|
} else {
|
||||||
|
messages.classList.remove('narrow-layout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only hide collapse buttons for channels/people sidebars, not server section
|
||||||
|
const sidebarToggle = document.querySelector('#channels .toggle-sidebar');
|
||||||
|
const peopleToggle = document.querySelector('#peopleSidebar .toggle-people-sidebar');
|
||||||
|
|
||||||
|
if (this.isMobile()) {
|
||||||
|
if (sidebarToggle) sidebarToggle.style.display = 'none';
|
||||||
|
if (peopleToggle) peopleToggle.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
if (sidebarToggle) sidebarToggle.style.display = '';
|
||||||
|
if (peopleToggle) peopleToggle.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustComposerForMobile() {
|
||||||
|
const composer = document.querySelector('.composer');
|
||||||
|
const messages = document.querySelector('.messages');
|
||||||
|
|
||||||
|
if (!composer || !messages) return;
|
||||||
|
|
||||||
|
if (this.isMobile()) {
|
||||||
|
messages.style.paddingBottom = '90px';
|
||||||
|
} else {
|
||||||
|
messages.style.paddingBottom = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enhanceTouchInteractions() {
|
||||||
|
let lastTap = 0;
|
||||||
|
document.addEventListener('touchend', (e) => {
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const tapLength = currentTime - lastTap;
|
||||||
|
|
||||||
|
if (tapLength < 500 && tapLength > 0) {
|
||||||
|
const messageLine = e.target.closest('.message-line');
|
||||||
|
if (messageLine && !e.target.closest('.message-sender')) {
|
||||||
|
this.handleDoubleTapMessage(messageLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastTap = currentTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDoubleTapMessage(messageLine) {
|
||||||
|
messageLine.style.backgroundColor = '#3A3A3A';
|
||||||
|
setTimeout(() => {
|
||||||
|
messageLine.style.backgroundColor = '';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMobile() {
|
||||||
|
return this.currentBreakpoint !== 'desktop';
|
||||||
|
}
|
||||||
|
|
||||||
|
throttle(func, limit) {
|
||||||
|
let inThrottle;
|
||||||
|
return function() {
|
||||||
|
const args = arguments;
|
||||||
|
const context = this;
|
||||||
|
if (!inThrottle) {
|
||||||
|
func.apply(context, args);
|
||||||
|
inThrottle = true;
|
||||||
|
setTimeout(() => inThrottle = false, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public methods for integration
|
||||||
|
handleNewMessage() {
|
||||||
|
if (this.isMobile() && (this.sidebarOpen || this.peopleSidebarOpen)) {
|
||||||
|
setTimeout(() => this.closeAllSidebars(), 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChannelSwitch(channelName) {
|
||||||
|
if (this.isMobile()) {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
this.updateMobileTitle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMessageInput() {
|
||||||
|
const messageInput = document.getElementById('messageInput');
|
||||||
|
if (!messageInput) return;
|
||||||
|
|
||||||
|
messageInput.addEventListener('focus', () => {
|
||||||
|
if (this.isMobile()) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const messages = document.querySelector('.messages');
|
||||||
|
if (messages) {
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
messageInput.addEventListener('keypress', (e) => {
|
||||||
|
if (this.isMobile() && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
setTimeout(() => {
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
if (sendBtn) sendBtn.click();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize mobile manager
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.mobileManager = new MobileManager();
|
||||||
|
integrateWithExistingApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Integration with existing app
|
||||||
|
function integrateWithExistingApp() {
|
||||||
|
// Override setActiveTab
|
||||||
|
const originalSetActiveTab = window.setActiveTab;
|
||||||
|
if (originalSetActiveTab) {
|
||||||
|
window.setActiveTab = function(name) {
|
||||||
|
originalSetActiveTab(name);
|
||||||
|
if (window.mobileManager) {
|
||||||
|
window.mobileManager.handleChannelSwitch(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override addMessage
|
||||||
|
const originalAddMessage = window.addMessage;
|
||||||
|
if (originalAddMessage) {
|
||||||
|
window.addMessage = function(sender, content, isUser = false) {
|
||||||
|
originalAddMessage(sender, content, isUser);
|
||||||
|
if (window.mobileManager) {
|
||||||
|
window.mobileManager.handleNewMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override addSystemMessage
|
||||||
|
const originalAddSystemMessage = window.addSystemMessage;
|
||||||
|
if (originalAddSystemMessage) {
|
||||||
|
window.addSystemMessage = function(content) {
|
||||||
|
originalAddSystemMessage(content);
|
||||||
|
if (window.mobileManager) {
|
||||||
|
window.mobileManager.handleNewMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the original toggle functions
|
||||||
|
if (window.toggleSidebar) {
|
||||||
|
const originalToggleSidebar = window.toggleSidebar;
|
||||||
|
window.toggleSidebar = function() {
|
||||||
|
if (window.mobileManager && window.mobileManager.isMobile()) {
|
||||||
|
window.mobileManager.toggleSidebar();
|
||||||
|
} else {
|
||||||
|
originalToggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.togglePeopleSidebar) {
|
||||||
|
const originalTogglePeopleSidebar = window.togglePeopleSidebar;
|
||||||
|
window.togglePeopleSidebar = function() {
|
||||||
|
if (window.mobileManager && window.mobileManager.isMobile()) {
|
||||||
|
window.mobileManager.togglePeopleSidebar();
|
||||||
|
} else {
|
||||||
|
originalTogglePeopleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup mobile message input
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.mobileManager) {
|
||||||
|
window.mobileManager.setupMessageInput();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = MobileManager;
|
||||||
|
}
|
||||||
223
src/styles/mobile.css
Normal file
223
src/styles/mobile.css
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#channels .connection-section {
|
||||||
|
position: relative !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only apply fullscreen to the sidebar containers, not their internal sections */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-device #channels.active {
|
||||||
|
width: 85vw !important;
|
||||||
|
max-width: 400px !important;
|
||||||
|
min-width: 280px !important;
|
||||||
|
transform: translateX(0) !important;
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device #peopleSidebar.active {
|
||||||
|
width: 85vw !important;
|
||||||
|
max-width: 400px !important;
|
||||||
|
min-width: 280px !important;
|
||||||
|
transform: translateX(0) !important;
|
||||||
|
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure server connection form stays properly sized */
|
||||||
|
.mobile-device #channels.active .connection-section {
|
||||||
|
width: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device #channels.active .form-group input {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Super narrow screens (280px and below) */
|
||||||
|
@media (max-width: 280px) {
|
||||||
|
.super-narrow .mobile-header {
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .mobile-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .mobile-menu-button {
|
||||||
|
padding: 6px;
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .tab {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
max-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .message-sender {
|
||||||
|
min-width: 50px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .message-content {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .composer {
|
||||||
|
padding: 8px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .composer-input input {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow button {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .form-group input {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust sidebar width for super narrow */
|
||||||
|
.super-narrow #channels.active,
|
||||||
|
.super-narrow #peopleSidebar.active {
|
||||||
|
width: 95vw !important;
|
||||||
|
min-width: 250px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Narrow screens (320px and below) */
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
.narrow-screen .mobile-header {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .tab {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .messages {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .message-sender {
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .composer {
|
||||||
|
padding: 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .composer-input input {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen #channels.active,
|
||||||
|
.narrow-screen #peopleSidebar.active {
|
||||||
|
width: 90vw !important;
|
||||||
|
min-width: 260px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .channel-item,
|
||||||
|
.narrow-screen .people-item {
|
||||||
|
padding: 10px 8px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .form-group {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .button-group {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile optimizations */
|
||||||
|
.mobile-device {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device .messages,
|
||||||
|
.mobile-device .channel-list,
|
||||||
|
.mobile-device .people-list-container {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-active {
|
||||||
|
background-color: #4A4A4A !important;
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improved tap targets */
|
||||||
|
.mobile-device button,
|
||||||
|
.mobile-device .channel-item,
|
||||||
|
.mobile-device .people-item,
|
||||||
|
.mobile-device .tab {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide resize handles on mobile */
|
||||||
|
.mobile-device .sidebar-resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact forms for narrow screens */
|
||||||
|
.narrow-screen .connection-section,
|
||||||
|
.narrow-screen .channel-section,
|
||||||
|
.narrow-screen .people-section {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .section-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack buttons vertically on very narrow screens */
|
||||||
|
.super-narrow .button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .button-group button {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust message layout for narrow screens */
|
||||||
|
.super-narrow .message-line {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .message-sender {
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 0;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact composer for narrow screens */
|
||||||
|
.super-narrow .composer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .composer-input {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
@@ -121,20 +121,26 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: "Inter", "Segoe UI", sans-serif;
|
font-family: "Inter", "Segoe UI", sans-serif;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
background: #1a1c21;
|
background: #2D2D2D;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
--sidebar-width: 280px;
|
--sidebar-width: 280px;
|
||||||
--sidebar-collapsed-width: 60px;
|
--sidebar-collapsed-width: 60px;
|
||||||
--primary-color: #4b82ff;
|
--primary-color: #4A4A4A;
|
||||||
--success-color: #7edba5;
|
--success-color: #7edba5;
|
||||||
--danger-color: #d36d6d;
|
--danger-color: #d36d6d;
|
||||||
--bg-dark: #111217;
|
--bg-dark: #2D2D2D;
|
||||||
--bg-panel: #181a1f;
|
--bg-panel: #252525;
|
||||||
--bg-content: #1a1c21;
|
--bg-content: #2D2D2D;
|
||||||
--border-color: #2a2d35;
|
--border-color: #555;
|
||||||
--text-muted: #9ea4b9;
|
--text-muted: #777;
|
||||||
--message-bg: #1e2028;
|
--message-bg: #1E1E1E;
|
||||||
--message-bg-user: #2d4fcc;
|
--message-bg-user: #3A3A3A;
|
||||||
|
|
||||||
|
/* Responsive variables */
|
||||||
|
--mobile-breakpoint: 768px;
|
||||||
|
--tablet-breakpoint: 1024px;
|
||||||
|
--mobile-sidebar-width: 280px;
|
||||||
|
--mobile-header-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -149,12 +155,15 @@ body {
|
|||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-container {
|
.app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background-color: #2D2D2D;
|
||||||
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar Styles */
|
/* Sidebar Styles */
|
||||||
@@ -164,13 +173,14 @@ body {
|
|||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
transition: width 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
resize: horizontal;
|
resize: horizontal;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-right {
|
.sidebar-right {
|
||||||
@@ -212,6 +222,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header h1 {
|
.sidebar-header h1 {
|
||||||
@@ -220,22 +231,24 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-sidebar {
|
.toggle-sidebar {
|
||||||
background: none;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-sidebar:hover {
|
.toggle-sidebar:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: #3A3A3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-section,
|
.connection-section,
|
||||||
@@ -246,7 +259,7 @@ body {
|
|||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -268,21 +281,22 @@ body {
|
|||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid #3a3f4d;
|
border: 1px solid #555;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #101218;
|
background: #3A3A3A;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus {
|
.form-group input:focus {
|
||||||
border-color: var(--primary-color);
|
border-color: #555;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,35 +304,44 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #3a3f4d;
|
border: 1px solid #555;
|
||||||
background: #242730;
|
background: #3A3A3A;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover:not(:disabled) {
|
button:hover:not(:disabled) {
|
||||||
background: #2c303a;
|
background: #4A4A4A;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active:not(:disabled) {
|
||||||
|
background: #2A2A2A;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.4;
|
background: #2A2A2A;
|
||||||
|
color: #777;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--primary-color);
|
background: #3A3A3A;
|
||||||
border-color: var(--primary-color);
|
border-color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
.btn-primary:hover:not(:disabled) {
|
||||||
background: #5a92ff;
|
background: #4A4A4A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@@ -332,6 +355,7 @@ button:disabled {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
@@ -339,6 +363,7 @@ button:disabled {
|
|||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--danger-color);
|
background: var(--danger-color);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.online {
|
.status-dot.online {
|
||||||
@@ -359,15 +384,16 @@ button:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-item:hover {
|
.channel-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: #3A3A3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-item.active {
|
.channel-item.active {
|
||||||
background: rgba(75, 130, 255, 0.15);
|
background: #3A3A3A;
|
||||||
color: var(--primary-color);
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-icon {
|
.channel-icon {
|
||||||
@@ -389,6 +415,7 @@ button:disabled {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-section {
|
.people-section {
|
||||||
@@ -402,17 +429,17 @@ button:disabled {
|
|||||||
.people-search-input {
|
.people-search-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border: 1px solid #3a3f4d;
|
border: 1px solid #555;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #101218;
|
background: #3A3A3A;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-search-input:focus {
|
.people-search-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--primary-color);
|
border-color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-list-container {
|
.people-list-container {
|
||||||
@@ -444,10 +471,11 @@ button:disabled {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-item:hover {
|
.people-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: #3A3A3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-item.op {
|
.people-item.op {
|
||||||
@@ -493,6 +521,7 @@ button:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-content);
|
background: var(--bg-content);
|
||||||
|
min-width: 0; /* Important for flexbox shrinking */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -500,40 +529,58 @@ button:disabled {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
background: #14151a;
|
background: #252525;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari and Opera */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
border: 1px solid #2f323a;
|
border: 1px solid #555;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
background: #1b1d23;
|
background: #2D2D2D;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: #23252d;
|
background: #3A3A3A;
|
||||||
color: #f5f5f5;
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-close {
|
.tab-close {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-close:hover {
|
.tab-close:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -555,14 +602,22 @@ button:disabled {
|
|||||||
.message-line {
|
.message-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
|
color: #DDD;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-sender {
|
.message-sender {
|
||||||
min-width: 120px;
|
min-width: 80px;
|
||||||
|
max-width: 120px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-sender.you {
|
.message-sender.you {
|
||||||
@@ -571,13 +626,16 @@ button:disabled {
|
|||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: #e1e1e1;
|
color: #DDD;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-message {
|
.system-message {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.irc-message {
|
.irc-message {
|
||||||
@@ -598,24 +656,27 @@ button:disabled {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #2a2e35;
|
background: #3A3A3A;
|
||||||
border-radius: 24px;
|
border-radius: 4px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-input input {
|
.composer-input input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-input input::placeholder {
|
.composer-input input::placeholder {
|
||||||
color: #6a7283;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions {
|
.suggestions {
|
||||||
@@ -623,13 +684,15 @@ button:disabled {
|
|||||||
bottom: 110%;
|
bottom: 110%;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: #101116;
|
background: #252525;
|
||||||
border: 1px solid #3a3f4d;
|
border: 1px solid #555;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions button {
|
.suggestions button {
|
||||||
@@ -637,11 +700,14 @@ button:disabled {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions button:hover,
|
.suggestions button:hover,
|
||||||
.suggestions button.active {
|
.suggestions button.active {
|
||||||
background: #222430;
|
background: #3A3A3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions.hidden {
|
.suggestions.hidden {
|
||||||
@@ -674,23 +740,420 @@ button:disabled {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Overlay */
|
||||||
|
.mobile-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-overlay.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Header */
|
||||||
|
.mobile-header {
|
||||||
|
display: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: var(--mobile-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #DDD;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-button:hover {
|
||||||
|
background: #3A3A3A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #DDD;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbars - Vertical */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #1E1E1E;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1E1E1E;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #3A3A3A;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background: #6A6A6A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox scrollbar */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #3A3A3A #1E1E1E;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive styles */
|
/* Responsive styles */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 1024px) {
|
||||||
.sidebar {
|
:root {
|
||||||
position: absolute;
|
--sidebar-width: 240px;
|
||||||
z-index: 100;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar:not(.collapsed) {
|
.sidebar {
|
||||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
min-width: 180px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender {
|
||||||
|
min-width: 70px;
|
||||||
|
max-width: 100px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
padding: 12px;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input input {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--mobile-sidebar-width);
|
||||||
|
max-width: 100vw;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 100;
|
||||||
|
resize: none;
|
||||||
|
min-width: unset;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.active {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-right {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-right.active {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-line {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 4px 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender {
|
||||||
|
min-width: auto;
|
||||||
|
max-width: none;
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer {
|
.composer {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide sidebar toggle in header when sidebar is open on mobile */
|
||||||
|
.sidebar.active ~ .main-content .mobile-header .mobile-menu-button {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
:root {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-section,
|
||||||
|
.channel-section,
|
||||||
|
.people-section {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input input {
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
:root {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
max-width: 100px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch device improvements */
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
button,
|
||||||
|
.channel-item,
|
||||||
|
.people-item,
|
||||||
|
.tab {
|
||||||
|
min-height: 44px; /* Better touch targets */
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-sidebar,
|
||||||
|
.mobile-menu-button {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle {
|
||||||
|
width: 8px; /* Larger touch target for resize */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce hover effects on touch devices */
|
||||||
|
.channel-item:hover,
|
||||||
|
.people-item:hover,
|
||||||
|
.tab:hover {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-item:active,
|
||||||
|
.people-item:active,
|
||||||
|
.tab:active {
|
||||||
|
background: #3A3A3A;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High DPI screens */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||||
|
:root {
|
||||||
|
font-size: 16px; /* Slightly larger text for better readability */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
.sidebar,
|
||||||
|
.composer,
|
||||||
|
.mobile-header,
|
||||||
|
.tabs {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
overflow: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=number]::-webkit-inner-spin-button {
|
input[type=number]::-webkit-inner-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-outer-spin-button,
|
||||||
|
input[type="number"]::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes for responsive behavior */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-on-mobile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-on-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hide-on-mobile {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-on-mobile {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-on-mobile-flex {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user