fix(communications): compact mobile messenger workspace

This commit is contained in:
ochenstarik 2026-09-12 13:37:58 +07:00
parent f2ac97a841
commit 3425be1b8a
10 changed files with 70 additions and 73 deletions

View file

@ -1,5 +1,5 @@
{
"generatedAt": "2026-09-12T05:25:45.504Z",
"generatedAt": "2026-09-12T06:26:42.753Z",
"module": "communications",
"version": "v3.0",
"referenceMockups": [
@ -128,19 +128,19 @@
"filename": "mobile-chat.png",
"theme": "state-capture",
"viewport": "mobile-chat",
"capturedAt": "2026-09-12T05:25:36.209Z"
"capturedAt": "2026-09-12T06:26:42.411Z"
},
{
"filename": "mobile-contact.png",
"theme": "state-capture",
"viewport": "mobile-contact",
"capturedAt": "2026-09-12T05:25:36.390Z"
"capturedAt": "2026-09-12T06:26:42.582Z"
},
{
"filename": "mobile-list.png",
"theme": "state-capture",
"viewport": "mobile-list",
"capturedAt": "2026-09-12T05:25:35.998Z"
"capturedAt": "2026-09-12T06:26:42.215Z"
},
{
"filename": "noble-green-desktop-1280.png",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View file

@ -134,7 +134,7 @@ test.describe('Scenario 13: Communications Visual & Responsive Design System V3
await expect(backToListBtn).toBeVisible();
// Check bottom contact teaser button
const profileTeaserBtn = activeChat.locator('button[aria-label="Открыть карточку контакта"]');
const profileTeaserBtn = activeChat.getByRole('button', { name: /Открыть.*контакт/i });
await expect(profileTeaserBtn).toBeVisible();
// Check message composer is positioned within viewport

View file

@ -10,6 +10,7 @@ import { QuickActionsModal } from '../shell/QuickActionsModal';
import { NotificationsDrawer } from '../shell/NotificationsDrawer';
import { Drawer } from '../../shared/ui';
import { useResponsive } from '../../shared/lib/useResponsive';
import { useCommunicationsStore } from '../../features/communications/services/communicationsService';
export const AppShell: React.FC = () => {
const {
@ -25,6 +26,7 @@ export const AppShell: React.FC = () => {
const profile = getActiveProfile();
const location = useLocation();
const isCommunicationsPage = location.pathname.startsWith('/communications');
const isMobileChatOpen = useCommunicationsStore((state) => state.isMobileChatOpen);
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
@ -80,13 +82,13 @@ export const AppShell: React.FC = () => {
{/* Center Work Area */}
<div className="flex-1 flex flex-col min-w-0 overflow-hidden bg-krona-workspace">
<Topbar />
{!(isEffectiveMobile && isCommunicationsPage && isMobileChatOpen) && <Topbar />}
{/* Main Content Router Outlet */}
<main
className={`flex-1 min-h-0 ${
isCommunicationsPage
? `overflow-hidden flex flex-col pt-0 px-0 ${isEffectiveMobile ? 'pb-20' : 'pb-0'}`
? `overflow-hidden flex flex-col pt-0 px-0 ${isEffectiveMobile ? 'pb-14' : 'pb-0'}`
: `overflow-y-auto ${isEffectiveMobile ? 'pb-24' : 'pb-8'} p-3 sm:p-4 md:p-6`
}`}
>

View file

@ -22,12 +22,12 @@ export const BottomNav: React.FC = () => {
<nav
aria-label="Мобильная навигация"
data-testid="bottom-nav"
className="fixed bottom-0 inset-x-0 bg-krona-surface text-krona-secondary border-t border-krona-border flex items-center justify-around z-40 px-1 pt-1 pb-[max(0.5rem,env(safe-area-inset-bottom))] shadow-lg backdrop-blur-md"
className="fixed bottom-0 inset-x-0 bg-krona-surface text-krona-secondary border-t border-krona-border flex items-center justify-around z-40 px-1 pt-0.5 pb-[max(0.25rem,env(safe-area-inset-bottom))] shadow-lg backdrop-blur-md"
>
{/* 1. Главная (KPI Сводка) */}
<button
onClick={() => navigate('/?tab=dashboard')}
className={`flex-1 min-h-[48px] min-w-[44px] flex flex-col items-center justify-center gap-1 p-1 rounded-xl transition-colors select-none ${
className={`flex-1 min-h-[44px] min-w-[44px] flex flex-col items-center justify-center gap-0.5 p-0.5 rounded-lg transition-colors select-none ${
isDashboardActive
? 'text-krona-gold font-bold bg-krona-gold/10'
: 'text-krona-secondary hover:text-krona-primary active:bg-krona-workspace'
@ -41,7 +41,7 @@ export const BottomNav: React.FC = () => {
{/* 2. Модули (Единый каталог) */}
<button
onClick={() => navigate('/')}
className={`flex-1 min-h-[48px] min-w-[44px] flex flex-col items-center justify-center gap-1 p-1 rounded-xl transition-colors select-none ${
className={`flex-1 min-h-[44px] min-w-[44px] flex flex-col items-center justify-center gap-0.5 p-0.5 rounded-lg transition-colors select-none ${
isCatalogActive
? 'text-krona-gold font-bold bg-krona-gold/10'
: 'text-krona-secondary hover:text-krona-primary active:bg-krona-workspace'
@ -56,7 +56,7 @@ export const BottomNav: React.FC = () => {
<NavLink
to="/tasks"
className={({ isActive }) =>
`flex-1 min-h-[48px] min-w-[44px] flex flex-col items-center justify-center gap-1 p-1 rounded-xl transition-colors select-none ${
`flex-1 min-h-[44px] min-w-[44px] flex flex-col items-center justify-center gap-0.5 p-0.5 rounded-lg transition-colors select-none ${
isActive
? 'text-krona-gold font-bold bg-krona-gold/10'
: 'text-krona-secondary hover:text-krona-primary active:bg-krona-workspace'
@ -72,7 +72,7 @@ export const BottomNav: React.FC = () => {
<NavLink
to="/communications"
className={({ isActive }) =>
`flex-1 min-h-[48px] min-w-[44px] flex flex-col items-center justify-center gap-1 p-1 rounded-xl transition-colors select-none ${
`flex-1 min-h-[44px] min-w-[44px] flex flex-col items-center justify-center gap-0.5 p-0.5 rounded-lg transition-colors select-none ${
isActive
? 'text-krona-gold font-bold bg-krona-gold/10'
: 'text-krona-secondary hover:text-krona-primary active:bg-krona-workspace'
@ -87,7 +87,7 @@ export const BottomNav: React.FC = () => {
{/* 5. Ещё */}
<button
onClick={() => setIsDrawerOpen(true)}
className={`flex-1 min-h-[48px] min-w-[44px] flex flex-col items-center justify-center gap-1 p-1 rounded-xl transition-colors select-none ${
className={`flex-1 min-h-[44px] min-w-[44px] flex flex-col items-center justify-center gap-0.5 p-0.5 rounded-lg transition-colors select-none ${
isDrawerOpen
? 'text-krona-gold font-bold bg-krona-gold/10'
: 'text-krona-secondary hover:text-krona-primary active:bg-krona-workspace'

View file

@ -75,7 +75,7 @@ export const Topbar: React.FC = () => {
return (
<header className="sticky top-0 z-30 bg-krona-surface/95 backdrop-blur-md border-b border-krona-border text-krona-primary shadow-xs">
{/* Mobile Topbar (< 768px): Minimal, clean, zero overflow */}
<div className="flex md:hidden h-14 px-3 items-center justify-between gap-2">
<div className="flex md:hidden h-12 px-2.5 items-center justify-between gap-2">
{/* Left: Logo & Current Section Title */}
<div className="flex items-center gap-2 min-w-0">
<div
@ -99,7 +99,7 @@ export const Topbar: React.FC = () => {
{/* AI Trigger */}
<button
onClick={() => setIsAiDrawerOpen(true)}
className="w-11 h-11 min-w-[44px] min-h-[44px] rounded-xl bg-krona-workspace text-krona-gold hover:text-krona-gold-text border border-krona-border flex items-center justify-center active:scale-95 transition-all"
className="w-10 h-10 min-w-10 min-h-10 rounded-xl bg-krona-workspace text-krona-gold hover:text-krona-gold-text border border-krona-border flex items-center justify-center active:scale-95 transition-all"
title="Krona AI"
aria-label="Krona AI"
>
@ -109,7 +109,7 @@ export const Topbar: React.FC = () => {
{/* Notifications Trigger */}
<button
onClick={() => setIsNotificationsOpen(true)}
className="relative w-11 h-11 min-w-[44px] min-h-[44px] rounded-xl bg-krona-workspace text-krona-secondary hover:text-krona-primary border border-krona-border flex items-center justify-center active:scale-95 transition-all"
className="relative w-10 h-10 min-w-10 min-h-10 rounded-xl bg-krona-workspace text-krona-secondary hover:text-krona-primary border border-krona-border flex items-center justify-center active:scale-95 transition-all"
title="Уведомления"
aria-label="Уведомления"
data-testid="mobile-notifications-btn"
@ -123,7 +123,7 @@ export const Topbar: React.FC = () => {
{/* Profile / Menu Drawer Trigger */}
<button
onClick={() => setIsMobileDrawerOpen(true)}
className="w-11 h-11 min-w-[44px] min-h-[44px] rounded-xl bg-krona-workspace text-krona-primary border border-krona-border flex items-center justify-center active:scale-95 transition-all"
className="w-10 h-10 min-w-10 min-h-10 rounded-xl bg-krona-workspace text-krona-primary border border-krona-border flex items-center justify-center active:scale-95 transition-all"
title="Меню управления"
aria-label="Открыть меню управления"
data-testid="mobile-menu-btn"

View file

@ -17,7 +17,7 @@ import {
export function CommunicationsModule() {
const [searchParams, setSearchParams] = useSearchParams();
const { activeTab, setActiveTab } = useCommunicationsStore();
const { activeTab, setActiveTab, isMobileChatOpen } = useCommunicationsStore();
const [isComposeEmailOpen, setIsComposeEmailOpen] = useState(false);
const [isCreateChannelOpen, setIsCreateChannelOpen] = useState(false);
@ -38,13 +38,15 @@ export function CommunicationsModule() {
return (
<div className="h-full min-h-0 flex flex-col bg-[var(--krona-bg-workspace)] overflow-hidden">
{/* Header */}
<CommunicationsHeader
activeTab={currentTab}
onTabChange={handleTabChange}
unreadChats={48}
unreadEmails={10}
unreadChannels={4}
/>
<div className={currentTab === 'messengers' && isMobileChatOpen ? 'hidden sm:block' : ''}>
<CommunicationsHeader
activeTab={currentTab}
onTabChange={handleTabChange}
unreadChats={48}
unreadEmails={10}
unreadChannels={4}
/>
</div>
{/* Body Views */}
<main className="flex-1 min-h-0 flex flex-col overflow-hidden">

View file

@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect, type FormEvent } from 'react';
import {
Search, ArrowUpDown, Star, Phone, Video, UserPlus,
MoreVertical, Paperclip, Sparkles, Smile, Mic, Send, Download, FileText,
ArrowLeft, ChevronRight, CheckCheck, Plus,
ArrowLeft, CheckCheck, Plus,
Briefcase, CheckSquare, ShieldCheck
} from 'lucide-react';
import { useCommunicationsStore } from '../services/communicationsService';
@ -171,11 +171,15 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
// Dialogs list section
const dialogListSection = (
<section
className="w-full h-full bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] flex flex-col min-h-0 overflow-hidden"
className={`w-full h-full bg-[var(--krona-surface-raised)] flex flex-col min-h-0 overflow-hidden ${
layoutMode === 'compact'
? 'rounded-none border-0 shadow-none'
: 'rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)]'
}`}
aria-label="Список диалогов"
>
{/* Search and filters header */}
<div className="p-3.5 border-b border-[var(--krona-border-subtle)] space-y-2.5 shrink-0">
<div className={`${layoutMode === 'compact' ? 'p-2 space-y-1.5' : 'p-3.5 space-y-2.5'} border-b border-[var(--krona-border-subtle)] shrink-0`}>
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-[var(--krona-text-muted)]" />
<input
@ -255,7 +259,9 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
setIsMobileChatOpen(true);
setIsContactDrawerOpen(false);
}}
className={`w-full p-3.5 text-left flex items-start gap-3 transition-colors border-l-4 min-h-[76px] ${
className={`w-full text-left flex items-start transition-colors border-l-4 ${
layoutMode === 'compact' ? 'px-3 py-2.5 gap-2.5 min-h-[64px]' : 'p-3.5 gap-3 min-h-[76px]'
} ${
isActive
? 'bg-[var(--krona-surface-selected)] border-l-[var(--krona-primary)]'
: 'border-l-transparent hover:bg-[var(--krona-surface-hover)]'
@ -316,17 +322,21 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
// Active chat section
const activeChatSection = (
<section
className="w-full h-full bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] flex flex-col min-h-0 overflow-hidden"
className={`w-full h-full bg-[var(--krona-surface-raised)] flex flex-col min-h-0 overflow-hidden ${
layoutMode === 'compact'
? 'rounded-none border-0 shadow-none'
: 'rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)]'
}`}
aria-label="Активный диалог"
>
{/* Chat Header */}
<div className="h-16 px-4 bg-[var(--krona-surface)] border-b border-[var(--krona-border-subtle)] flex items-center justify-between gap-3 shrink-0">
<div className="flex items-center gap-3 min-w-0">
<div className={`${layoutMode === 'compact' ? 'h-12 px-2' : 'h-16 px-4'} bg-[var(--krona-surface)] border-b border-[var(--krona-border-subtle)] flex items-center justify-between gap-2 shrink-0`}>
<div className="flex items-center gap-2 min-w-0">
{layoutMode === 'compact' && (
<button
type="button"
onClick={() => setIsMobileChatOpen(false)}
className="w-9 h-9 -ml-1 rounded-lg flex items-center justify-center text-[var(--krona-text-secondary)] hover:bg-[var(--krona-surface-hover)]"
className="w-9 h-9 -ml-1 rounded-full flex items-center justify-center text-[var(--krona-text-secondary)] hover:bg-[var(--krona-surface-hover)]"
aria-label="Назад к списку чатов"
>
<ArrowLeft className="w-5 h-5" />
@ -379,7 +389,7 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
type: 'info',
})
}
className="w-9 h-9 rounded-lg hover:bg-[var(--krona-surface-hover)] flex items-center justify-center transition-colors"
className="hidden sm:flex w-9 h-9 rounded-lg hover:bg-[var(--krona-surface-hover)] items-center justify-center transition-colors"
title="Аудиозвонок"
aria-label="Аудиозвонок"
>
@ -394,7 +404,7 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
type: 'info',
})
}
className="w-9 h-9 rounded-lg hover:bg-[var(--krona-surface-hover)] flex items-center justify-center transition-colors"
className="hidden sm:flex w-9 h-9 rounded-lg hover:bg-[var(--krona-surface-hover)] items-center justify-center transition-colors"
title="Видеозвонок"
aria-label="Видеозвонок"
>
@ -414,6 +424,18 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
</button>
)}
{layoutMode === 'compact' && (
<button
type="button"
onClick={() => setIsContactDrawerOpen(true)}
className="w-9 h-9 rounded-full flex items-center justify-center text-[var(--krona-text-secondary)] hover:bg-[var(--krona-surface-hover)]"
title="Карточка контакта"
aria-label="Открыть карточку контакта"
>
<UserPlus className="w-4 h-4" />
</button>
)}
<button
type="button"
className="w-9 h-9 rounded-lg hover:bg-[var(--krona-surface-hover)] flex items-center justify-center transition-colors"
@ -428,7 +450,7 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
{/* Messages Feed */}
<div
ref={messagesContainerRef}
className="flex-1 min-h-0 overflow-y-auto p-4 space-y-3 bg-[var(--krona-bg-workspace)]/40"
className={`flex-1 min-h-0 overflow-y-auto bg-[var(--krona-bg-workspace)]/40 ${layoutMode === 'compact' ? 'px-2.5 py-2 space-y-2' : 'p-4 space-y-3'}`}
>
<div className="text-center my-2">
<span className="px-3 py-1 rounded-full text-[11px] font-medium bg-[var(--krona-surface)] text-[var(--krona-text-muted)] border border-[var(--krona-border-subtle)] shadow-2xs">
@ -439,7 +461,7 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
{activeChat.messages.map((msg) => (
<div
key={msg.id}
className={`flex gap-3 ${
className={`flex ${layoutMode === 'compact' ? 'gap-2' : 'gap-3'} ${
msg.direction === 'outgoing' ? 'justify-end' : 'justify-start'
}`}
>
@ -455,7 +477,7 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
)}
<div
className={`max-w-[85%] sm:max-w-[75%] rounded-2xl p-3.5 shadow-xs text-sm transition-colors ${
className={`max-w-[88%] sm:max-w-[75%] rounded-2xl shadow-xs text-sm transition-colors ${layoutMode === 'compact' ? 'px-3 py-2' : 'p-3.5'} ${
msg.direction === 'outgoing'
? 'bg-[var(--krona-primary-soft)] border border-[var(--krona-border-subtle)] text-[var(--krona-text-primary)] rounded-tr-xs'
: 'bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] text-[var(--krona-text-primary)] rounded-tl-xs'
@ -515,7 +537,7 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
</div>
{/* Quick Replies Bar */}
<div className="px-4 py-2 bg-[var(--krona-surface)]/60 border-t border-[var(--krona-border-subtle)] flex items-center gap-2 overflow-x-auto scrollbar-none shrink-0">
{layoutMode !== 'compact' && <div className="px-4 py-2 bg-[var(--krona-surface)]/60 border-t border-[var(--krona-border-subtle)] flex items-center gap-2 overflow-x-auto scrollbar-none shrink-0">
<span className="text-[11px] text-[var(--krona-text-muted)] font-medium shrink-0 flex items-center gap-1">
<Sparkles className="w-3 h-3 text-[var(--krona-gold)]" /> Быстрые:
</span>
@ -529,12 +551,12 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
{qrText}
</button>
))}
</div>
</div>}
{/* Message Composer */}
<form
onSubmit={handleSend}
className="p-3 sm:p-4 bg-[var(--krona-surface)] border-t border-[var(--krona-border-subtle)] shrink-0"
className="p-1.5 sm:p-4 bg-[var(--krona-surface)] border-t border-[var(--krona-border-subtle)] shrink-0"
>
<div className="flex items-end gap-2 bg-[var(--krona-bg-workspace)] border border-[var(--krona-border)] rounded-xl p-1.5 focus-within:border-[var(--krona-primary)] transition-colors">
<button
@ -613,35 +635,6 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
</div>
</form>
{/* Mobile Bottom Contact Teaser Bar (Level 2 -> Level 3 drilldown) */}
{layoutMode === 'compact' && (
<div className="p-3 bg-[var(--krona-surface)] border-t border-[var(--krona-border-subtle)] flex items-center justify-between gap-3 shrink-0">
<div className="flex items-center gap-2.5 min-w-0">
<Avatar
src={activeChat.contact.avatarUrl}
alt={activeChat.contact.name}
initials={activeChat.contact.initials}
size="sm"
/>
<div className="min-w-0">
<span className="font-semibold text-xs text-[var(--krona-text-primary)] truncate block">
{activeChat.contact.name}
</span>
<span className="text-[11px] text-[var(--krona-text-secondary)] truncate block">
{activeChat.contact.company}
</span>
</div>
</div>
<button
type="button"
onClick={() => setIsContactDrawerOpen(true)}
className="p-1.5 rounded-lg text-[var(--krona-primary)] hover:bg-[var(--krona-surface-hover)]"
aria-label="Открыть карточку контакта"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
)}
</section>
);
@ -652,7 +645,7 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
className="flex-1 min-h-0 flex flex-col bg-[var(--krona-bg-workspace)] overflow-hidden"
>
{/* 1. Channel Switcher Row (# 17. Channel Switcher) */}
<div className="bg-[var(--krona-surface)]/80 backdrop-blur-xs border-b border-[var(--krona-border-subtle)] px-3 sm:px-6 py-2 flex items-center justify-between gap-3 min-w-0 shrink-0">
{!(layoutMode === 'compact' && isMobileChatOpen) && <div className="bg-[var(--krona-surface)]/80 backdrop-blur-xs border-b border-[var(--krona-border-subtle)] px-2 sm:px-6 py-1.5 sm:py-2 flex items-center justify-between gap-2 sm:gap-3 min-w-0 shrink-0">
<div className="relative flex-1 min-w-0 flex items-center overflow-x-auto scrollbar-none py-0.5">
<div className="flex items-center gap-2 shrink-0 pr-6">
<ConnectorPill
@ -722,10 +715,10 @@ export function MessengersView({ onOpenConnectModal, onOpenAddContactModal }: Me
<Plus className="w-4 h-4 text-[var(--krona-primary)]" />
</button>
</div>
</div>
</div>}
{/* 2. Main Workspace Canvas */}
<div className="flex-1 min-h-0 p-3 sm:p-4 lg:p-5 flex gap-4 overflow-hidden">
<div className={`flex-1 min-h-0 flex gap-4 overflow-hidden ${layoutMode === 'compact' ? 'p-0' : 'p-3 sm:p-4 lg:p-5'}`}>
{/* COMPACT MODE (<600px): strictly 1 level at a time */}
{layoutMode === 'compact' && (
<>