new frontend
This commit is contained in:
376
src/UI.js
376
src/UI.js
@@ -31,7 +31,6 @@ const uiState = {
|
||||
userColors: new Map(),
|
||||
currentUser: 'IRCDUser',
|
||||
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
|
||||
virtualScrollState: {
|
||||
startIndex: 0,
|
||||
@@ -280,8 +279,7 @@ function addChannel(name) {
|
||||
if (!uiState.channels.includes(normalizedName)) {
|
||||
uiState.channels.push(normalizedName);
|
||||
|
||||
// Add to channel list (async DOM update)
|
||||
requestAnimationFrame(() => {
|
||||
// Add to channel list
|
||||
const channelItem = document.createElement('div');
|
||||
channelItem.className = 'channel-item';
|
||||
channelItem.innerHTML = `
|
||||
@@ -295,15 +293,9 @@ function addChannel(name) {
|
||||
`;
|
||||
channelItem.addEventListener('click', () => setActiveTab(normalizedName));
|
||||
channelList.appendChild(channelItem);
|
||||
});
|
||||
|
||||
// Add tab (async)
|
||||
requestAnimationFrame(() => addTab(normalizedName));
|
||||
|
||||
// Initialize message storage for this channel
|
||||
if (!uiState.channelMessages.has(normalizedName)) {
|
||||
uiState.channelMessages.set(normalizedName, []);
|
||||
}
|
||||
// Add tab
|
||||
addTab(normalizedName);
|
||||
|
||||
// Auto-join the channel when connected
|
||||
if (uiState.connected) {
|
||||
@@ -330,9 +322,6 @@ function removeChannel(name) {
|
||||
// Clear users for this channel
|
||||
clearChannelUsers(name);
|
||||
|
||||
// Clear messages for this channel (optional - you might want to keep them)
|
||||
// uiState.channelMessages.delete(name);
|
||||
|
||||
// Remove tab
|
||||
removeTab(name);
|
||||
|
||||
@@ -398,114 +387,51 @@ function setActiveTab(name) {
|
||||
|
||||
uiState.activeTab = normalizedName;
|
||||
|
||||
// Update tabs (async)
|
||||
requestAnimationFrame(() => {
|
||||
// Update tabs
|
||||
const tabs = tabBar.querySelectorAll('.tab');
|
||||
tabs.forEach(tab => {
|
||||
// Compare with both normalized and original name
|
||||
tab.classList.toggle('active', tab.dataset.tab === normalizedName || tab.dataset.tab === name);
|
||||
});
|
||||
});
|
||||
|
||||
// Update channels (async)
|
||||
requestAnimationFrame(() => {
|
||||
// Update channels
|
||||
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||
channelItems.forEach(item => {
|
||||
const channelName = item.querySelector('.channel-name').textContent;
|
||||
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);
|
||||
|
||||
// Update messages (async)
|
||||
requestAnimationFrame(() => {
|
||||
// Update messages
|
||||
updateMessagesForTab(normalizedName);
|
||||
});
|
||||
}
|
||||
|
||||
function updateMessagesForTab(name) {
|
||||
// Clear messages
|
||||
messagesContainer.innerHTML = '';
|
||||
|
||||
// Ensure channel name format is correct
|
||||
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)
|
||||
// Add welcome messages
|
||||
if (name === 'welcome') {
|
||||
// Don't store welcome messages, just display
|
||||
requestAnimationFrame(() => {
|
||||
const welcomeMsg = document.createElement('div');
|
||||
welcomeMsg.className = 'system-message';
|
||||
welcomeMsg.textContent = 'IRCD - Welcome! SystemTime: ' + new Date().toLocaleString();
|
||||
messagesContainer.appendChild(welcomeMsg);
|
||||
});
|
||||
} else if (channelKey !== 'welcome') {
|
||||
// Only add welcome message if channel has no messages
|
||||
addSystemMessage(`Welcome to ${channelKey}!`);
|
||||
addSystemMessage('This is the beginning of the channel.');
|
||||
}
|
||||
} else {
|
||||
// For channels, just show a welcome message
|
||||
const welcomeMsg = document.createElement('div');
|
||||
welcomeMsg.className = 'system-message';
|
||||
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
|
||||
@@ -545,19 +471,6 @@ function addMessage(sender, content, isUser = false) {
|
||||
const channel = uiState.activeTab;
|
||||
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');
|
||||
messageLine.className = 'message-line';
|
||||
|
||||
@@ -573,261 +486,29 @@ function addMessage(sender, content, isUser = false) {
|
||||
messageLine.appendChild(senderElement);
|
||||
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);
|
||||
scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
function addSystemMessage(content) {
|
||||
const channel = uiState.activeTab;
|
||||
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');
|
||||
systemMessage.className = 'system-message';
|
||||
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);
|
||||
scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
ircMessage.className = 'irc-message';
|
||||
ircMessage.innerHTML = content; // Use innerHTML to render HTML formatting
|
||||
ircMessage.innerHTML = content;
|
||||
|
||||
messagesContainer.appendChild(ircMessage);
|
||||
scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
@@ -959,7 +640,6 @@ function updatePeopleList(channel) {
|
||||
}
|
||||
|
||||
// Sort and filter users (all stored in RAM, not DOM)
|
||||
setTimeout(() => {
|
||||
let sortedUsers = Array.from(users).sort((a, b) => {
|
||||
const aIsOp = a.startsWith('@');
|
||||
const bIsOp = b.startsWith('@');
|
||||
@@ -994,7 +674,6 @@ function updatePeopleList(channel) {
|
||||
|
||||
// Update virtual scroll - only renders visible items
|
||||
renderVisibleUsers();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function renderVisibleUsers() {
|
||||
@@ -1039,7 +718,6 @@ function renderVisibleUsers() {
|
||||
}
|
||||
|
||||
// Clear and render only visible items
|
||||
requestAnimationFrame(() => {
|
||||
if (!peopleList) return;
|
||||
|
||||
peopleList.innerHTML = '';
|
||||
@@ -1052,7 +730,6 @@ function renderVisibleUsers() {
|
||||
}
|
||||
|
||||
peopleList.appendChild(fragment);
|
||||
});
|
||||
}
|
||||
|
||||
function createPeopleItem(user) {
|
||||
@@ -1098,25 +775,19 @@ function handlePeopleSearch(e) {
|
||||
}
|
||||
|
||||
function handlePeopleListScroll() {
|
||||
requestAnimationFrame(() => {
|
||||
renderVisibleUsers();
|
||||
});
|
||||
}
|
||||
|
||||
function updateVirtualScrollContainerHeight() {
|
||||
requestAnimationFrame(() => {
|
||||
if (peopleListContainer) {
|
||||
uiState.virtualScrollState.containerHeight = peopleListContainer.clientHeight;
|
||||
renderVisibleUsers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseNamesMessage(channel, usersStr) {
|
||||
if (!channel || !usersStr) return;
|
||||
|
||||
// Process parsing asynchronously
|
||||
setTimeout(() => {
|
||||
// Initialize users set for this channel if it doesn't exist
|
||||
// Note: We accumulate users from multiple NAMES messages until ENDOFNAMES
|
||||
// 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
|
||||
if (uiState.activeTab === channel) {
|
||||
updatePeopleList(channel);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function clearChannelUsers(channel) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="./styles/styles.css">
|
||||
<link rel="stylesheet" href="./styles/mobile.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
@@ -14,7 +15,7 @@
|
||||
<aside class="sidebar" id="channels">
|
||||
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
||||
<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">
|
||||
<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"/>
|
||||
@@ -134,5 +135,6 @@
|
||||
|
||||
<script src="tauri.js"></script>
|
||||
<script src="UI.js"></script>
|
||||
<script src="mobile.js"></script>
|
||||
</body>
|
||||
</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 {
|
||||
font-family: "Inter", "Segoe UI", sans-serif;
|
||||
font-size: 15px;
|
||||
background: #1a1c21;
|
||||
color: #f1f1f1;
|
||||
background: #2D2D2D;
|
||||
color: #DDD;
|
||||
--sidebar-width: 280px;
|
||||
--sidebar-collapsed-width: 60px;
|
||||
--primary-color: #4b82ff;
|
||||
--primary-color: #4A4A4A;
|
||||
--success-color: #7edba5;
|
||||
--danger-color: #d36d6d;
|
||||
--bg-dark: #111217;
|
||||
--bg-panel: #181a1f;
|
||||
--bg-content: #1a1c21;
|
||||
--border-color: #2a2d35;
|
||||
--text-muted: #9ea4b9;
|
||||
--message-bg: #1e2028;
|
||||
--message-bg-user: #2d4fcc;
|
||||
--bg-dark: #2D2D2D;
|
||||
--bg-panel: #252525;
|
||||
--bg-content: #2D2D2D;
|
||||
--border-color: #555;
|
||||
--text-muted: #777;
|
||||
--message-bg: #1E1E1E;
|
||||
--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);
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow: hidden;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #2D2D2D;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
@@ -164,13 +173,14 @@ body {
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
resize: horizontal;
|
||||
flex-shrink: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar-right {
|
||||
@@ -212,6 +222,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
@@ -220,22 +231,24 @@ body {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.toggle-sidebar {
|
||||
background: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-sidebar:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: #3A3A3A;
|
||||
}
|
||||
|
||||
.connection-section,
|
||||
@@ -246,7 +259,7 @@ body {
|
||||
|
||||
.section-title {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
@@ -268,21 +281,22 @@ body {
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #3a3f4d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
background: #101218;
|
||||
color: #f1f1f1;
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: var(--primary-color);
|
||||
border-color: #555;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -290,35 +304,44 @@ body {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #3a3f4d;
|
||||
background: #242730;
|
||||
color: #f1f1f1;
|
||||
border: 1px solid #555;
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #2c303a;
|
||||
background: #4A4A4A;
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
background: #2A2A2A;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
background: #2A2A2A;
|
||||
color: #777;
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
background: #3A3A3A;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #5a92ff;
|
||||
background: #4A4A4A;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@@ -332,6 +355,7 @@ button:disabled {
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
font-size: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -339,6 +363,7 @@ button:disabled {
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--danger-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
@@ -359,15 +384,16 @@ button:disabled {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.channel-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: #3A3A3A;
|
||||
}
|
||||
|
||||
.channel-item.active {
|
||||
background: rgba(75, 130, 255, 0.15);
|
||||
color: var(--primary-color);
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.channel-icon {
|
||||
@@ -389,6 +415,7 @@ button:disabled {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.people-section {
|
||||
@@ -402,17 +429,17 @@ button:disabled {
|
||||
.people-search-input {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #3a3f4d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
background: #101218;
|
||||
color: #f1f1f1;
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.people-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.people-list-container {
|
||||
@@ -444,10 +471,11 @@ button:disabled {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.people-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: #3A3A3A;
|
||||
}
|
||||
|
||||
.people-item.op {
|
||||
@@ -493,6 +521,7 @@ button:disabled {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-content);
|
||||
min-width: 0; /* Important for flexbox shrinking */
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -500,40 +529,58 @@ button:disabled {
|
||||
gap: 2px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: #14151a;
|
||||
background: #252525;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.tabs::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border: 1px solid #2f323a;
|
||||
border: 1px solid #555;
|
||||
border-bottom: none;
|
||||
background: #1b1d23;
|
||||
background: #2D2D2D;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #23252d;
|
||||
color: #f5f5f5;
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
margin-left: 4px;
|
||||
opacity: 0.6;
|
||||
font-size: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -555,14 +602,22 @@ button:disabled {
|
||||
.message-line {
|
||||
display: flex;
|
||||
padding: 2px 0;
|
||||
color: #DDD;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
min-width: 120px;
|
||||
min-width: 80px;
|
||||
max-width: 120px;
|
||||
font-weight: 500;
|
||||
padding-right: 8px;
|
||||
text-align: right;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-sender.you {
|
||||
@@ -571,13 +626,16 @@ button:disabled {
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
color: #e1e1e1;
|
||||
color: #DDD;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.irc-message {
|
||||
@@ -598,24 +656,27 @@ button:disabled {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #2a2e35;
|
||||
border-radius: 24px;
|
||||
background: #3A3A3A;
|
||||
border-radius: 4px;
|
||||
padding: 0 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.composer-input input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #f1f1f1;
|
||||
color: #DDD;
|
||||
padding: 12px 0;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.composer-input input::placeholder {
|
||||
color: #6a7283;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
@@ -623,13 +684,15 @@ button:disabled {
|
||||
bottom: 110%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #101116;
|
||||
border: 1px solid #3a3f4d;
|
||||
background: #252525;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.suggestions button {
|
||||
@@ -637,11 +700,14 @@ button:disabled {
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
border-radius: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.suggestions button:hover,
|
||||
.suggestions button.active {
|
||||
background: #222430;
|
||||
background: #3A3A3A;
|
||||
}
|
||||
|
||||
.suggestions.hidden {
|
||||
@@ -674,23 +740,420 @@ button:disabled {
|
||||
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 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
height: 100%;
|
||||
@media (max-width: 1024px) {
|
||||
:root {
|
||||
--sidebar-width: 240px;
|
||||
}
|
||||
|
||||
.sidebar:not(.collapsed) {
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
||||
.sidebar {
|
||||
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 {
|
||||
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 {
|
||||
-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