responsive
This commit is contained in:
+88
-1589
File diff suppressed because it is too large
Load Diff
@@ -181,11 +181,146 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="specs-section">
|
||||
<h3>Technical Specifications</h3>
|
||||
<div class="specs-grid">
|
||||
<div class="spec-tag" title="Reactive state management with Svelte 5 Runes">
|
||||
<i class="fab fa-js-square"></i>
|
||||
<span>Svelte 5</span>
|
||||
</div>
|
||||
<div class="spec-tag" title="High-performance relational database backend">
|
||||
<i class="fas fa-database"></i>
|
||||
<span>SpacetimeDB</span>
|
||||
</div>
|
||||
<div class="spec-tag" title="Secure, efficient logic compiled to WebAssembly">
|
||||
<i class="fab fa-rust"></i>
|
||||
<span>Rust / WASM</span>
|
||||
</div>
|
||||
<div class="spec-tag" title="Real-time peer-to-peer voice and screen sharing">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
<span>WebRTC Mesh</span>
|
||||
</div>
|
||||
<div class="spec-tag" title="End-to-end encryption via OpenPGP">
|
||||
<i class="fas fa-lock"></i>
|
||||
<span>GPG E2EE</span>
|
||||
</div>
|
||||
<div class="spec-tag" title="Cross-platform desktop app foundation">
|
||||
<i class="fas fa-desktop"></i>
|
||||
<span>Tauri 2.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.login-screen {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
var(--background-secondary) 0%,
|
||||
var(--background-tertiary) 100%
|
||||
);
|
||||
background-color: var(--background-tertiary);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background-color: var(--background-primary);
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
width: 480px;
|
||||
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-card h1 {
|
||||
color: var(--header-primary);
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-card p {
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: var(--status-danger);
|
||||
background-color: rgba(250, 119, 122, 0.1);
|
||||
border: 1px solid rgba(250, 119, 122, 0.2);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.specs-section {
|
||||
margin-top: 32px;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--background-modifier-accent);
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.specs-section h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.specs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.spec-tag {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-normal);
|
||||
font-size: 0.95rem; /* Bigger text */
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.spec-tag i {
|
||||
font-size: 1.1rem;
|
||||
color: var(--brand);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.spec-tag:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--brand);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.spec-tag:hover i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ios-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
+311
-126
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { useSpacetimeDB } from "spacetimedb/svelte";
|
||||
import { setContext } from "svelte";
|
||||
import { setContext, onMount, untrack } from "svelte";
|
||||
import { ChatService } from "./services/chat.svelte";
|
||||
import { WebRTCService } from "./services/webrtc/webrtc.svelte";
|
||||
import ServerList from "./components/ServerList.svelte";
|
||||
@@ -24,7 +24,6 @@
|
||||
const spacetime = useSpacetimeDB();
|
||||
|
||||
// identity is guaranteed to be non-null here because of the guard in SpacetimeProvider
|
||||
// but we should still handle it robustly
|
||||
const chat = new ChatService($spacetime.identity!);
|
||||
const webrtc = new WebRTCService($spacetime.identity, undefined);
|
||||
|
||||
@@ -47,10 +46,46 @@
|
||||
|
||||
let showSettings = $state(false);
|
||||
let showMemberList = $state(true);
|
||||
let showSidebar = $state(true); // Toggle for mobile
|
||||
|
||||
const isMobile = $state({ value: false });
|
||||
|
||||
onMount(() => {
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 1024;
|
||||
if (isMobile.value) {
|
||||
showSidebar = false;
|
||||
showMemberList = false;
|
||||
} else {
|
||||
showSidebar = true;
|
||||
showMemberList = true;
|
||||
}
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
});
|
||||
|
||||
// Auto-hide sidebar when switching channels on mobile
|
||||
$effect(() => {
|
||||
const _ = chat.activeChannelId;
|
||||
if (isMobile.value) {
|
||||
untrack(() => {
|
||||
showSidebar = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function closeSidebars() {
|
||||
showSidebar = false;
|
||||
showMemberList = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="chat-container">
|
||||
<div class="left-sidebar-wrapper">
|
||||
<!-- 1. Left Sidebar (Servers + Channels) -->
|
||||
<aside class="left-sidebar-wrapper" class:visible={showSidebar}>
|
||||
<div class="left-sidebar-top">
|
||||
<ServerList onShowServerSettings={_onShowServerSettings} />
|
||||
<ChannelList />
|
||||
@@ -66,7 +101,7 @@
|
||||
{chat.connectedVoiceChannel.name} / {chat.connectedVoiceServer?.name || "Server"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="voice-status-actions" style="display: flex; gap: 4px;">
|
||||
<div class="voice-status-actions">
|
||||
<button
|
||||
class="icon-btn {webrtc.isSharingScreen ? 'active danger' : ''}"
|
||||
onclick={() => webrtc.isSharingScreen ? webrtc.stopScreenShare() : webrtc.startScreenShare()}
|
||||
@@ -86,17 +121,16 @@
|
||||
{/if}
|
||||
|
||||
<div class="user-info-bar">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="user-info-main"
|
||||
oncontextmenu={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onclick={(e) => {
|
||||
if (chat.currentUser) {
|
||||
chat.userContextMenu = { x: e.clientX, y: e.clientY, user: chat.currentUser };
|
||||
}
|
||||
}}
|
||||
style="cursor: pointer;"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === 'Enter' && (chat.userContextMenu = null)}
|
||||
>
|
||||
<Avatar user={chat.currentUser} isTalking={webrtc.localMedia.isTalking} />
|
||||
<div class="user-details">
|
||||
@@ -138,21 +172,33 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 2. Main Content (Backdrop + Header + Chat/Video) -->
|
||||
<main class="main-content">
|
||||
{#if isMobile.value && (showSidebar || showMemberList)}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="mobile-backdrop" onclick={closeSidebars}></div>
|
||||
{/if}
|
||||
|
||||
<div class="main-content">
|
||||
<header class="chat-header">
|
||||
<div style="flex: 1; display: flex; align-items: center; gap: 8px;">
|
||||
<button
|
||||
class="icon-btn mobile-toggle"
|
||||
onclick={() => (showSidebar = !showSidebar)}
|
||||
title="Toggle Navigation"
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<div class="header-info">
|
||||
{#if chat.activeServer}
|
||||
<span style="color: var(--text-muted); font-size: 1.2rem;">
|
||||
<i class="fas {chat.isActiveChannelVoice ? 'fa-volume-up' : 'fa-hashtag'}"></i>
|
||||
</span>
|
||||
<h2 style="margin: 0; font-size: 1rem;">{chat.activeChannel?.name || "Select a channel"}</h2>
|
||||
<span class="header-icon"><i class="fas {chat.isActiveChannelVoice ? 'fa-volume-up' : 'fa-hashtag'}"></i></span>
|
||||
<h2 class="header-title">{chat.activeChannel?.name || "Select a channel"}</h2>
|
||||
{#if chat.activeChannelId !== undefined}
|
||||
<button
|
||||
class="icon-btn notification-toggle {chat.isChannelNotificationsEnabled(chat.activeChannelId) ? 'subscribed' : ''}"
|
||||
onclick={() => chat.toggleChannelNotifications(chat.activeChannelId!)}
|
||||
title={chat.isChannelNotificationsEnabled(chat.activeChannelId) ? "Unsubscribe from Notifications" : "Subscribe to Notifications"}
|
||||
>
|
||||
<i class="fas {chat.isChannelNotificationsEnabled(chat.activeChannelId) ? 'fa-bell' : 'fa-bell-slash'}"></i>
|
||||
</button>
|
||||
@@ -165,32 +211,26 @@
|
||||
{@const recipient = chat.users.find(u => u.identity.toHexString() === otherIdentity.toHexString())}
|
||||
{#if recipient}
|
||||
<Avatar user={recipient} size="tiny" />
|
||||
<h2 style="margin: 0; font-size: 1rem;">{recipient.name || "Unknown User"}</h2>
|
||||
<h2 class="header-title">{recipient.name || "Unknown User"}</h2>
|
||||
<button
|
||||
class="icon-btn notification-toggle {chat.isChannelNotificationsEnabled(chat.activeChannelId!) ? 'subscribed' : ''}"
|
||||
onclick={() => chat.toggleChannelNotifications(chat.activeChannelId!)}
|
||||
title={chat.isChannelNotificationsEnabled(chat.activeChannelId!) ? "Unsubscribe from Notifications" : "Subscribe to Notifications"}
|
||||
>
|
||||
<i class="fas {chat.isChannelNotificationsEnabled(chat.activeChannelId!) ? 'fa-bell' : 'fa-bell-slash'}"></i>
|
||||
</button>
|
||||
{#if chat.getRecipientPublicKey(otherIdentity)}
|
||||
<div class="encryption-indicator" title="End-to-end encrypted">
|
||||
<i class="fas fa-lock"></i>
|
||||
<span>Encryption Available</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<h2 style="margin: 0; font-size: 1rem;">Select a conversation</h2>
|
||||
<h2 class="header-title">Select a conversation</h2>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="chat-header-actions" style="display: flex; align-items: center; gap: 16px;">
|
||||
|
||||
<div class="header-actions">
|
||||
{#if chat.activeServer}
|
||||
<button
|
||||
class="icon-btn {showMemberList ? 'active' : ''}"
|
||||
onclick={() => (showMemberList = !showMemberList)}
|
||||
title="Member List"
|
||||
title="Toggle Member List"
|
||||
>
|
||||
<i class="fas fa-users"></i>
|
||||
</button>
|
||||
@@ -198,118 +238,263 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if chat.isActiveChannelVoice}
|
||||
<VideoGrid />
|
||||
{:else}
|
||||
<MessageList />
|
||||
<div class="chat-input-container">
|
||||
<div class="typing-indicator">
|
||||
{#if chat.typingUsers.length > 0}
|
||||
<div class="dots">
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
<span style="font-weight: bold;">
|
||||
{#if chat.typingUsers.length === 1}
|
||||
{chat.typingUsers[0].name || "Someone"}
|
||||
{:else if chat.typingUsers.length === 2}
|
||||
{chat.typingUsers[0].name || "Someone"} and {chat.typingUsers[1].name || "Someone"}
|
||||
{:else if chat.typingUsers.length === 3}
|
||||
{chat.typingUsers[0].name || "Someone"}, {chat.typingUsers[1].name || "Someone"} and {chat.typingUsers[2].name || "Someone"}
|
||||
{:else}
|
||||
Several people
|
||||
{/if}
|
||||
</span>
|
||||
<span>{chat.typingUsers.length === 1 ? "is" : "are"} typing...</span>
|
||||
{/if}
|
||||
<div class="chat-view-container">
|
||||
{#if chat.isActiveChannelVoice}
|
||||
<VideoGrid />
|
||||
{:else}
|
||||
<MessageList />
|
||||
<div class="chat-input-container">
|
||||
<div class="typing-indicator">
|
||||
{#if chat.typingUsers.length > 0}
|
||||
<div class="dots"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
|
||||
<span class="typing-text">
|
||||
<strong>{chat.typingUsers[0].name || "Someone"}</strong>
|
||||
{chat.typingUsers.length > 1 ? ` and ${chat.typingUsers.length - 1} more` : ""}
|
||||
{chat.typingUsers.length === 1 ? "is" : "are"} typing...
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<ChatInput
|
||||
activeChannelId={chat.activeChannelId}
|
||||
activeThreadId={null}
|
||||
isFullyAuthenticated={chat.isFullyAuthenticated}
|
||||
/>
|
||||
</div>
|
||||
<ChatInput
|
||||
activeChannelId={chat.activeChannelId}
|
||||
activeThreadId={null}
|
||||
isFullyAuthenticated={chat.isFullyAuthenticated}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 3. Right Sidebar (Members or Threads) -->
|
||||
<aside class="right-sidebar-wrapper" class:visible={showMemberList}>
|
||||
{#if chat.activeThreadId || chat.pendingThreadParentMessageId}
|
||||
<ThreadView
|
||||
activeThreadId={chat.activeThreadId}
|
||||
setActiveThreadId={(id) => (chat.activeThreadId = id)}
|
||||
pendingThreadParentMessageId={chat.pendingThreadParentMessageId}
|
||||
setPendingThreadParentMessageId={(id) => (chat.pendingThreadParentMessageId = id)}
|
||||
activeChannelId={chat.activeChannelId}
|
||||
activeServer={chat.activeServer}
|
||||
isFullyAuthenticated={chat.isFullyAuthenticated}
|
||||
users={chat.users}
|
||||
identity={chat.identity}
|
||||
allThreads={chat.allThreads}
|
||||
allMessages={chat.allMessages}
|
||||
allImages={chat.images}
|
||||
/>
|
||||
{:else if showMemberList && chat.activeServer}
|
||||
<MemberList />
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{#if chat.activeThreadId || chat.pendingThreadParentMessageId}
|
||||
<ThreadView
|
||||
activeThreadId={chat.activeThreadId}
|
||||
setActiveThreadId={(id) => (chat.activeThreadId = id)}
|
||||
pendingThreadParentMessageId={chat.pendingThreadParentMessageId}
|
||||
setPendingThreadParentMessageId={(id) => (chat.pendingThreadParentMessageId = id)}
|
||||
activeChannelId={chat.activeChannelId}
|
||||
activeServer={chat.activeServer}
|
||||
isFullyAuthenticated={chat.isFullyAuthenticated}
|
||||
users={chat.users}
|
||||
identity={chat.identity}
|
||||
allThreads={chat.allThreads}
|
||||
allMessages={chat.allMessages}
|
||||
allImages={chat.images}
|
||||
/>
|
||||
{:else if showMemberList && chat.activeServer}
|
||||
<MemberList />
|
||||
{/if}
|
||||
|
||||
{#if chat.showDiscoveryModal}
|
||||
<ServerDiscovery />
|
||||
{/if}
|
||||
|
||||
{#if showSettings}
|
||||
<SettingsPanel
|
||||
currentUser={chat.currentUser}
|
||||
onClose={() => (showSettings = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if chat.showServerSettings}
|
||||
<ServerSettingsPanel
|
||||
onClose={() => (chat.showServerSettings = false)}
|
||||
/>
|
||||
{/if}
|
||||
<!-- Overlays & Modals -->
|
||||
{#if chat.showDiscoveryModal} <ServerDiscovery /> {/if}
|
||||
{#if showSettings} <SettingsPanel currentUser={chat.currentUser} onClose={() => (showSettings = false)} /> {/if}
|
||||
{#if chat.showServerSettings} <ServerSettingsPanel onClose={() => (chat.showServerSettings = false)} /> {/if}
|
||||
|
||||
{#if chat.viewingImageId}
|
||||
{@const image = chat.images.find(img => img.id === chat.viewingImageId)}
|
||||
{#if image}
|
||||
<ImageViewer
|
||||
{image}
|
||||
onClose={() => (chat.viewingImageId = null)}
|
||||
/>
|
||||
{/if}
|
||||
{#if image} <ImageViewer {image} onClose={() => (chat.viewingImageId = null)} /> {/if}
|
||||
{/if}
|
||||
|
||||
{#if chat.viewingProfileUser}
|
||||
<ProfileModal
|
||||
user={chat.viewingProfileUser}
|
||||
onClose={() => (chat.viewingProfileUser = null)}
|
||||
/>
|
||||
<ProfileModal user={chat.viewingProfileUser} onClose={() => (chat.viewingProfileUser = null)} />
|
||||
{/if}
|
||||
|
||||
{#if chat.userContextMenu}
|
||||
<UserContextMenu
|
||||
x={chat.userContextMenu.x}
|
||||
y={chat.userContextMenu.y}
|
||||
user={chat.userContextMenu.user}
|
||||
onClose={() => (chat.userContextMenu = null)}
|
||||
/>
|
||||
<UserContextMenu {...chat.userContextMenu} onClose={() => (chat.userContextMenu = null)} />
|
||||
{/if}
|
||||
|
||||
{#if chat.confirmModal}
|
||||
<ConfirmModal
|
||||
title={chat.confirmModal.title}
|
||||
message={chat.confirmModal.message}
|
||||
confirmText={chat.confirmModal.confirmText}
|
||||
cancelText={chat.confirmModal.cancelText}
|
||||
isDanger={chat.confirmModal.isDanger}
|
||||
onConfirm={() => {
|
||||
chat.confirmModal?.onConfirm();
|
||||
chat.confirmModal = null;
|
||||
}}
|
||||
onCancel={() => {
|
||||
chat.confirmModal?.onCancel?.();
|
||||
chat.confirmModal = null;
|
||||
}}
|
||||
/>
|
||||
<ConfirmModal {...chat.confirmModal} onConfirm={() => { chat.confirmModal?.onConfirm(); chat.confirmModal = null; }} onCancel={() => { chat.confirmModal?.onCancel?.(); chat.confirmModal = null; }} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: var(--background-tertiary);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* SIDEBARS */
|
||||
.left-sidebar-wrapper, .right-sidebar-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--background-secondary);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-sidebar-wrapper {
|
||||
width: calc(var(--server-sidebar-width) + var(--channel-sidebar-width));
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.right-sidebar-wrapper {
|
||||
width: 240px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Hidden states for desktop */
|
||||
@media (min-width: 1025px) {
|
||||
.left-sidebar-wrapper:not(.visible) { display: none; }
|
||||
.right-sidebar-wrapper:not(.visible) { display: none; }
|
||||
}
|
||||
|
||||
.left-sidebar-top {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* MAIN CONTENT */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-primary);
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--header-primary);
|
||||
}
|
||||
|
||||
.chat-view-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; /* CRITICAL for scrolling children */
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 0 16px 24px 16px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
/* RESPONSIVE MOBILE LOGIC */
|
||||
.mobile-toggle { display: none; }
|
||||
.mobile-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 90;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.mobile-toggle { display: flex; margin-right: 8px; }
|
||||
|
||||
.left-sidebar-wrapper, .right-sidebar-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
transition: transform 0.2s ease;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.left-sidebar-wrapper {
|
||||
left: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.left-sidebar-wrapper.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.right-sidebar-wrapper {
|
||||
right: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.right-sidebar-wrapper.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* SHARED PARTS */
|
||||
.voice-status-bar {
|
||||
background-color: var(--background-tertiary);
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.voice-info { display: flex; flex-direction: column; }
|
||||
.voice-connected-text { color: var(--status-positive); font-size: 0.8rem; font-weight: bold; }
|
||||
.voice-channel-name { color: var(--text-muted); font-size: 0.75rem; }
|
||||
.voice-status-actions { display: flex; gap: 4px; }
|
||||
|
||||
.user-info-bar {
|
||||
background-color: var(--background-tertiary);
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.user-info-main { display: flex; align-items: center; gap: 8px; min-width: 0; cursor: pointer; }
|
||||
.user-details { min-width: 0; }
|
||||
.user-display-name { font-size: 0.85rem; font-weight: 600; color: var(--header-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.user-status { font-size: 0.7rem; color: var(--text-muted); display: flex; align-items: center; gap: 4px; }
|
||||
.user-actions { display: flex; gap: 2px; }
|
||||
|
||||
.typing-indicator {
|
||||
height: 24px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dots { display: flex; gap: 2px; }
|
||||
.dot { width: 4px; height: 4px; background-color: var(--text-muted); border-radius: 50%; animation: typing-dot 1.4s infinite ease-in-out; }
|
||||
.dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes typing-dot {
|
||||
0%, 80%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-4px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -724,6 +724,16 @@
|
||||
|
||||
.chat-input, .chat-input-wrapper {
|
||||
transition: border-radius 0.2s;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 0 12px;
|
||||
background-color: var(--background-tertiary);
|
||||
border-radius: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chat-input-wrapper {
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.has-staged {
|
||||
@@ -731,21 +741,7 @@
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.thread-input-inner .chat-input-wrapper textarea {
|
||||
font-size: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-normal);
|
||||
width: 100%;
|
||||
resize: none;
|
||||
padding: 11px 0;
|
||||
font-family: inherit;
|
||||
line-height: 1.375rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-input textarea {
|
||||
.chat-input textarea, .chat-input-wrapper textarea {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -761,7 +757,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
.chat-input, .chat-input-wrapper {
|
||||
height: auto !important;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
@@ -131,4 +131,75 @@
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
width: 240px;
|
||||
background-color: var(--background-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
padding: 16px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--interactive-normal);
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.member-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.member-item.offline {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.member-name.talking {
|
||||
color: var(--status-positive);
|
||||
}
|
||||
|
||||
.member-list-section-header {
|
||||
padding: 16px 8px 8px 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.sharing-badge {
|
||||
background-color: var(--status-danger);
|
||||
color: white;
|
||||
font-size: 0.6rem;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -454,7 +454,7 @@
|
||||
aria-expanded={!collapsedImages}
|
||||
>
|
||||
<div class="embed-type-label">
|
||||
<i class="fas fa-image" style="color: var(--brand);"></i>
|
||||
<i class="fas fa-image"></i>
|
||||
{msg.imageIds.length} Image{msg.imageIds.length > 1 ? 's' : ''}
|
||||
</div>
|
||||
<i class="fas fa-chevron-{collapsedImages ? 'down' : 'up'}" style="color: var(--text-muted); font-size: 0.8rem;"></i>
|
||||
@@ -764,6 +764,57 @@
|
||||
border-top-color: var(--background-floating);
|
||||
}
|
||||
|
||||
.message-image {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.message-image-container {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.embed-wrapper {
|
||||
border-left: 4px solid var(--brand);
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
max-width: 520px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.embed-header {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.embed-header:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.embed-type-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.embed-content-body {
|
||||
padding: 8px 12px 12px 12px;
|
||||
}
|
||||
|
||||
.reaction-badge {
|
||||
background-color: var(--background-accent);
|
||||
border: 1px solid transparent;
|
||||
|
||||
@@ -414,4 +414,14 @@
|
||||
max-width: 400px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 24px;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -191,9 +191,9 @@
|
||||
{:else if embed.type === 'facebook'}
|
||||
<i class="fab fa-facebook" style="color: #1877F2;"></i> Facebook
|
||||
{:else if embed.type === 'image'}
|
||||
<i class="fas fa-image" style="color: var(--brand);"></i> Image
|
||||
<i class="fas fa-image"></i> Image
|
||||
{:else}
|
||||
<i class="fas fa-link" style="color: var(--text-normal);"></i> Link
|
||||
<i class="fas fa-link"></i> Link
|
||||
{/if}
|
||||
</div>
|
||||
<i class="fas fa-chevron-{isCollapsed(i, embed.type) ? 'down' : 'up'}" style="color: var(--text-muted); font-size: 0.8rem;"></i>
|
||||
@@ -322,5 +322,71 @@
|
||||
|
||||
.message-image {
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.embeds-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.embed-wrapper {
|
||||
border-left: 4px solid var(--brand);
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
max-width: 520px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.embed-header {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.embed-header:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.embed-type-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.embed-content-body {
|
||||
padding: 8px 12px 12px 12px;
|
||||
}
|
||||
|
||||
.media-embed-container {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.media-iframe {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.message-image-container {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -261,4 +261,30 @@
|
||||
background-color: var(--brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.server-list {
|
||||
width: var(--server-sidebar-width);
|
||||
background-color: var(--background-tertiary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.server-icon.active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8px;
|
||||
height: 40px;
|
||||
background-color: white;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -76,4 +76,49 @@
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-section {
|
||||
padding: 16px 0 8px 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s, color 0.1s;
|
||||
}
|
||||
|
||||
.section-header:hover .add-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.channel-item-hash {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-muted);
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -257,4 +257,200 @@
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-section {
|
||||
padding: 16px 0 8px 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s, color 0.1s;
|
||||
}
|
||||
|
||||
.section-header:hover .add-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.channel-item-hash {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-muted);
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Voice Member List Styles */
|
||||
.voice-member-list {
|
||||
padding-left: 36px;
|
||||
padding-right: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.voice-member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.voice-member-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.voice-member-name {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.voice-member-name.talking {
|
||||
color: var(--header-primary);
|
||||
}
|
||||
|
||||
.voice-member-status-container {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.voice-member-indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.voice-indicator-icon {
|
||||
font-size: 0.75rem;
|
||||
color: var(--interactive-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.deafen-indicator-svg {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
}
|
||||
|
||||
.watcher-eye {
|
||||
color: var(--brand);
|
||||
font-size: 0.75rem;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.sharing-badge {
|
||||
background-color: var(--status-danger);
|
||||
color: white;
|
||||
font-size: 0.6rem;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Connection Popover */
|
||||
.connection-popover {
|
||||
position: fixed;
|
||||
width: 216px;
|
||||
background-color: var(--background-floating);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
|
||||
z-index: 3000;
|
||||
color: var(--text-normal);
|
||||
pointer-events: none;
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.popover-name {
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.popover-status {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.popover-status.green { color: var(--status-positive); }
|
||||
.popover-status.yellow { color: var(--status-warning); }
|
||||
.popover-status.red { color: var(--status-danger); }
|
||||
|
||||
.popover-info {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.stat-row span:last-child {
|
||||
color: var(--header-primary);
|
||||
font-family: var(--font-code);
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
accent-color: var(--brand);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user