responsive

This commit is contained in:
2026-04-16 22:04:14 -04:00
parent abdf79fc54
commit 49a325ebee
11 changed files with 1014 additions and 1734 deletions
+88 -1589
View File
File diff suppressed because it is too large Load Diff
+135
View File
@@ -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
View File
@@ -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>
+12 -16
View File
@@ -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;
}
+71
View File
@@ -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>
+52 -1
View File
@@ -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;
+10
View File
@@ -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>
+68 -2
View File
@@ -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>
+26
View File
@@ -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>