mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 01:26:43 +02:00
🚸 ux(matrix-web): improve mobile responsiveness
- Add slide-in sidebar overlay with backdrop on mobile - Make message actions appear below message on mobile - Adjust emoji picker positioning for viewport awareness - Reduce excessive padding on mobile screens - Hide disabled call buttons on small screens - Add responsive widths to panels and dialogs - Close sidebar automatically when selecting room on mobile
This commit is contained in:
parent
6f1b2654f1
commit
f2cd8621cb
19 changed files with 1231 additions and 85 deletions
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import ContactCardSkeleton from './ContactCardSkeleton.svelte';
|
||||
import { calculateFadeOpacity } from './utils';
|
||||
import { calculateFadeOpacity } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
/** Number of skeleton cards to show */
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import ContactRowSkeleton from './ContactRowSkeleton.svelte';
|
||||
import { calculateFadeOpacity } from './utils';
|
||||
import { calculateFadeOpacity } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
/** Number of skeleton rows to show */
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@
|
|||
* Shows stats cards and duplicate groups with fade effect
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
import { SkeletonBox, calculateFadeOpacity } from '@manacore/shared-ui';
|
||||
import DuplicateGroupSkeleton from './DuplicateGroupSkeleton.svelte';
|
||||
import { calculateFadeOpacity } from './utils';
|
||||
|
||||
interface Props {
|
||||
/** Number of duplicate groups to show */
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import TagCardSkeleton from './TagCardSkeleton.svelte';
|
||||
import { calculateFadeOpacity } from './utils';
|
||||
import { calculateFadeOpacity } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
/** Number of skeleton cards to show */
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
* Built on top of @manacore/shared-ui skeleton primitives.
|
||||
*/
|
||||
|
||||
// Utilities
|
||||
export { calculateFadeOpacity } from './utils';
|
||||
// Utilities (re-exported from shared-ui)
|
||||
export { calculateFadeOpacity } from '@manacore/shared-ui';
|
||||
|
||||
// Contact List/Grid Skeletons
|
||||
export { default as ContactRowSkeleton } from './ContactRowSkeleton.svelte';
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
/**
|
||||
* Skeleton utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate opacity for cascading fade effect in skeleton lists
|
||||
* @param index Current item index
|
||||
* @param count Total number of items
|
||||
* @param minOpacity Minimum opacity (default: 0.3)
|
||||
* @returns Opacity value between minOpacity and 1
|
||||
*/
|
||||
export function calculateFadeOpacity(
|
||||
index: number,
|
||||
count: number,
|
||||
minOpacity: number = 0.3
|
||||
): number {
|
||||
const fadeStep = (1 - minOpacity) / Math.max(count - 1, 1);
|
||||
return Math.max(minOpacity, 1 - index * fadeStep);
|
||||
}
|
||||
|
|
@ -103,12 +103,12 @@
|
|||
{#if open}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
onclick={handleClose}
|
||||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="w-full max-w-md rounded-xl bg-base-100 shadow-xl"
|
||||
class="w-full max-w-md rounded-xl bg-base-100 shadow-xl max-h-[90vh] overflow-y-auto"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
|
|
|||
|
|
@ -74,7 +74,10 @@
|
|||
const codeColor = isOwn ? 'bg-white/20 text-white' : 'bg-black/5 dark:bg-white/10';
|
||||
|
||||
// Inline code (backticks) - process first to avoid conflicts
|
||||
text = text.replace(/`([^`]+)`/g, `<code class="px-1 py-0.5 rounded text-sm font-mono ${codeColor}">$1</code>`);
|
||||
text = text.replace(
|
||||
/`([^`]+)`/g,
|
||||
`<code class="px-1 py-0.5 rounded text-sm font-mono ${codeColor}">$1</code>`
|
||||
);
|
||||
|
||||
// Bold (**text** or __text__)
|
||||
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
|
|
@ -251,7 +254,11 @@
|
|||
{/if}
|
||||
|
||||
<!-- Message Content -->
|
||||
<div class="flex flex-col {message.isOwn ? 'items-end' : 'items-start'} max-w-[75%] relative">
|
||||
<div
|
||||
class="flex flex-col {message.isOwn
|
||||
? 'items-end'
|
||||
: 'items-start'} max-w-[85%] sm:max-w-[75%] relative"
|
||||
>
|
||||
<!-- Sender name (for others only) -->
|
||||
{#if showAvatar && !message.isOwn}
|
||||
<span class="text-xs text-muted-foreground mb-1 px-1">{message.senderName}</span>
|
||||
|
|
@ -433,7 +440,9 @@
|
|||
</p>
|
||||
{:else}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<p class="whitespace-pre-wrap break-words text-[15px] leading-relaxed">{@html formatMessageBody(message.body, message.isOwn)}</p>
|
||||
<p class="whitespace-pre-wrap break-words text-[15px] leading-relaxed">
|
||||
{@html formatMessageBody(message.body, message.isOwn)}
|
||||
</p>
|
||||
|
||||
<!-- Link Preview Card -->
|
||||
{#if firstUrl()}
|
||||
|
|
@ -451,7 +460,9 @@
|
|||
class="h-5 w-5 rounded-sm"
|
||||
onerror={(e) => ((e.currentTarget as HTMLImageElement).style.display = 'none')}
|
||||
/>
|
||||
<span class="text-xs truncate {message.isOwn ? 'text-white/80' : 'text-muted-foreground'}">
|
||||
<span
|
||||
class="text-xs truncate {message.isOwn ? 'text-white/80' : 'text-muted-foreground'}"
|
||||
>
|
||||
{getDomain(firstUrl() || '')}
|
||||
</span>
|
||||
</a>
|
||||
|
|
@ -489,34 +500,31 @@
|
|||
{/if}
|
||||
|
||||
<!-- Time and read status -->
|
||||
<div
|
||||
class="flex items-center gap-1.5 mt-1.5 px-1 {message.isOwn ? 'justify-end' : ''}"
|
||||
>
|
||||
<span class="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">{formattedTime()}</span>
|
||||
<div class="flex items-center gap-1.5 mt-1.5 px-1 {message.isOwn ? 'justify-end' : ''}">
|
||||
<span
|
||||
class="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>{formattedTime()}</span
|
||||
>
|
||||
<!-- Read receipt indicator (for own messages) -->
|
||||
{#if message.isOwn}
|
||||
{#if message.readBy && message.readBy.length > 0}
|
||||
<Checks
|
||||
class="h-4 w-4 text-blue-500"
|
||||
weight="bold"
|
||||
title="Gelesen von: {message.readBy.map(r => r.userName).join(', ')}"
|
||||
title="Gelesen von: {message.readBy.map((r) => r.userName).join(', ')}"
|
||||
/>
|
||||
{:else}
|
||||
<Check
|
||||
class="h-4 w-4 text-muted-foreground/50"
|
||||
weight="bold"
|
||||
title="Gesendet"
|
||||
/>
|
||||
<Check class="h-4 w-4 text-muted-foreground/50" weight="bold" title="Gesendet" />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Message actions (hover) -->
|
||||
<!-- Message actions (hover/tap) -->
|
||||
{#if showActions && !message.redacted}
|
||||
<div
|
||||
class="absolute {message.isOwn
|
||||
? '-left-28'
|
||||
: '-right-28'} top-0 flex items-center gap-1 rounded-xl glass p-1.5 shadow-lg"
|
||||
class="absolute flex items-center gap-1 rounded-xl glass p-1.5 shadow-lg z-20
|
||||
{message.isOwn ? 'right-0 lg:-left-28 lg:right-auto' : 'left-0 lg:-right-28 lg:left-auto'}
|
||||
top-full mt-1 lg:top-0 lg:mt-0"
|
||||
>
|
||||
<!-- Emoji reaction button -->
|
||||
<div class="relative">
|
||||
|
|
@ -536,9 +544,9 @@
|
|||
></button>
|
||||
<!-- Emoji picker dropdown -->
|
||||
<div
|
||||
class="absolute {message.isOwn
|
||||
? 'right-0'
|
||||
: 'left-0'} bottom-full mb-2 z-50 flex gap-1 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 p-2 shadow-xl"
|
||||
class="absolute z-50 flex gap-1 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 p-2 shadow-xl
|
||||
left-0 top-full mt-2 lg:bottom-full lg:top-auto lg:mt-0 lg:mb-2
|
||||
{message.isOwn ? 'lg:right-0 lg:left-auto' : ''}"
|
||||
>
|
||||
{#each quickEmojis as emoji}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="p-3 pb-20">
|
||||
<div class="p-3 pb-4 lg:pb-20">
|
||||
<!-- Reply/Edit Preview -->
|
||||
{#if replyTo || editMessage}
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -102,14 +102,14 @@
|
|||
<!-- Actions -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
class="p-2.5 rounded-xl glass-button shadow-sm disabled:opacity-40"
|
||||
class="hidden sm:flex p-2.5 rounded-xl glass-button shadow-sm disabled:opacity-40"
|
||||
title="Sprachanruf"
|
||||
disabled
|
||||
>
|
||||
<Phone class="h-5 w-5 text-muted-foreground" />
|
||||
</button>
|
||||
<button
|
||||
class="p-2.5 rounded-xl glass-button shadow-sm disabled:opacity-40"
|
||||
class="hidden sm:flex p-2.5 rounded-xl glass-button shadow-sm disabled:opacity-40"
|
||||
title="Videoanruf"
|
||||
disabled
|
||||
>
|
||||
|
|
|
|||
|
|
@ -13,9 +13,18 @@
|
|||
|
||||
interface Props {
|
||||
onCreateRoom?: () => void;
|
||||
onSelectRoom?: (roomId: string) => void;
|
||||
}
|
||||
|
||||
let { onCreateRoom }: Props = $props();
|
||||
let { onCreateRoom, onSelectRoom }: Props = $props();
|
||||
|
||||
function handleSelectRoom(roomId: string) {
|
||||
if (onSelectRoom) {
|
||||
onSelectRoom(roomId);
|
||||
} else {
|
||||
matrixStore.selectRoom(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
let search = $state('');
|
||||
|
||||
|
|
@ -143,7 +152,7 @@
|
|||
<RoomItem
|
||||
{room}
|
||||
selected={room.id === matrixStore.currentRoomId}
|
||||
onclick={() => matrixStore.selectRoom(room.id)}
|
||||
onclick={() => handleSelectRoom(room.id)}
|
||||
/>
|
||||
{:else}
|
||||
{#if !search}
|
||||
|
|
@ -171,7 +180,7 @@
|
|||
<RoomItem
|
||||
{room}
|
||||
selected={room.id === matrixStore.currentRoomId}
|
||||
onclick={() => matrixStore.selectRoom(room.id)}
|
||||
onclick={() => handleSelectRoom(room.id)}
|
||||
/>
|
||||
{:else}
|
||||
{#if !search}
|
||||
|
|
@ -191,7 +200,7 @@
|
|||
</div>
|
||||
|
||||
<!-- New Room Button -->
|
||||
<div class="border-t border-black/10 dark:border-white/10 p-3 pb-20">
|
||||
<div class="border-t border-black/10 dark:border-white/10 p-3 pb-4 lg:pb-20">
|
||||
<button
|
||||
class="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl
|
||||
bg-gradient-to-r from-violet-500 to-purple-600 text-white font-medium
|
||||
|
|
|
|||
|
|
@ -78,9 +78,15 @@
|
|||
</script>
|
||||
|
||||
{#if open && room}
|
||||
<!-- Backdrop for mobile -->
|
||||
<button
|
||||
class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
|
||||
onclick={onClose}
|
||||
aria-label="Schließen"
|
||||
></button>
|
||||
<!-- Slide-in Panel -->
|
||||
<div
|
||||
class="fixed inset-y-0 right-0 z-40 flex w-80 flex-col border-l border-base-300 bg-base-100 shadow-xl"
|
||||
class="fixed inset-y-0 right-0 z-50 flex w-[90vw] max-w-[320px] lg:w-80 flex-col border-l border-base-300 bg-base-100 shadow-xl"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="flex items-center justify-between border-b border-base-300 px-4 py-3">
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
import CreateRoomDialog from '$lib/components/chat/CreateRoomDialog.svelte';
|
||||
import RoomSettingsPanel from '$lib/components/chat/RoomSettingsPanel.svelte';
|
||||
import { ChatCircle, Plus } from '@manacore/shared-icons';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let sidebarOpen = $state(true);
|
||||
// Start with sidebar closed on mobile
|
||||
let sidebarOpen = $state(browser ? window.innerWidth >= 1024 : true);
|
||||
let showCreateRoom = $state(false);
|
||||
let showRoomSettings = $state(false);
|
||||
|
||||
|
|
@ -13,10 +15,31 @@
|
|||
let replyTo = $state<SimpleMessage | null>(null);
|
||||
let editMessage = $state<SimpleMessage | null>(null);
|
||||
|
||||
// Check if mobile
|
||||
let isMobile = $state(browser ? window.innerWidth < 1024 : false);
|
||||
|
||||
// Update on resize
|
||||
if (browser) {
|
||||
window.addEventListener('resize', () => {
|
||||
isMobile = window.innerWidth < 1024;
|
||||
// Auto-close sidebar on resize to mobile
|
||||
if (isMobile && sidebarOpen) {
|
||||
sidebarOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebarOpen = !sidebarOpen;
|
||||
}
|
||||
|
||||
function selectRoomAndCloseSidebar(roomId: string) {
|
||||
matrixStore.selectRoom(roomId);
|
||||
if (isMobile) {
|
||||
sidebarOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleReply(message: SimpleMessage) {
|
||||
editMessage = null;
|
||||
replyTo = message;
|
||||
|
|
@ -32,12 +55,22 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="chat-layout flex h-full min-h-0 overflow-hidden bg-background">
|
||||
<div class="chat-layout flex h-full min-h-0 overflow-hidden bg-background relative">
|
||||
<!-- Mobile Sidebar Backdrop -->
|
||||
{#if sidebarOpen && isMobile}
|
||||
<button
|
||||
class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
|
||||
onclick={() => (sidebarOpen = false)}
|
||||
aria-label="Sidebar schließen"
|
||||
></button>
|
||||
{/if}
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="flex w-80 flex-shrink-0 flex-col border-r border-black/10 dark:border-white/10 bg-white/50 dark:bg-white/5 backdrop-blur-sm transition-all duration-300 ease-in-out"
|
||||
class:hidden={!sidebarOpen}
|
||||
class:lg:flex={true}
|
||||
class="flex flex-col border-r border-black/10 dark:border-white/10 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl transition-all duration-300 ease-in-out
|
||||
fixed lg:relative inset-y-0 left-0 z-50 lg:z-auto
|
||||
w-[85vw] max-w-[320px] lg:w-80 lg:max-w-none
|
||||
{sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}"
|
||||
>
|
||||
<!-- User Info / Status Bar -->
|
||||
<div class="border-b border-black/10 dark:border-white/10 px-4 py-3">
|
||||
|
|
@ -66,7 +99,10 @@
|
|||
|
||||
<!-- Room List -->
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<RoomList onCreateRoom={() => (showCreateRoom = true)} />
|
||||
<RoomList
|
||||
onCreateRoom={() => (showCreateRoom = true)}
|
||||
onSelectRoom={selectRoomAndCloseSidebar}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue