Complete Sprint 2 (chat hub) and Sprint 3 (color palettes)
Sprint 2 — chat hub: - Chat tab becomes a list of three conversations: AI assistant (featured, gradient card), doctor Syndaev, and clinic administrator - data.js: new `chats` array with kind, participants, message history, online/unread state, time of last message, AI suggestions - screens-chats.jsx: ChatsListScreen and ChatConversationScreen with per-kind UI — AI gets suggestion chips + AI-badge, doctor gets video-call button, operator gets phone button - Recovery surgeon chat button routes to chat:doctor-syndaev directly - Tab bar auto-hides on pushed chat:<id> routes - ChatTabScreen removed from screens-misc.jsx Sprint 3 — color palettes: - ACCENT_OPTIONS extended with accent/accentDark/accent50, p300/success50/fg4 so palette switches change the full theme (primary + warm + accent + muted + success) - New palette "Лагуна" from the design-system screenshot: primary #29AEE3 (sky blue), accent #FFA39C (coral), warm #E9E4D4 (beige) - New palette "Бриз": Лагуна variant with primary #63BAC3 (muted teal) and the bright sky blue #29AEE3 demoted to p300 - All 9 screenshot colors wired: #f2fee6→success-50, #93908f→fg-4, #63bac3→p300 (visible as border on Clinic Stats card in Home V2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import React from 'react';
|
||||
import { I } from '../icons.jsx';
|
||||
import { CLINIC_DATA } from '../data.js';
|
||||
import { Avatar } from '../components.jsx';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Chats list — the main chat tab
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
export function ChatsListScreen({ nav }) {
|
||||
const { chats, doctors } = CLINIC_DATA;
|
||||
const ai = chats.find(c => c.kind === 'ai');
|
||||
const rest = chats.filter(c => c.kind !== 'ai');
|
||||
|
||||
const subjectFor = (c) => {
|
||||
if (c.kind === 'doctor') {
|
||||
const d = doctors.find(x => x.id === c.doctorId);
|
||||
return {
|
||||
avatar: <Avatar init={d.init} size={48} />,
|
||||
title: d.name.split(' ').slice(0, 2).join(' '),
|
||||
subtitle: d.spec,
|
||||
};
|
||||
}
|
||||
const bg = c.kind === 'operator' ? 'var(--c-warm-100)' : 'var(--c-primary-100)';
|
||||
return {
|
||||
avatar: (
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 999, background: bg,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22,
|
||||
}}>{c.icon}</div>
|
||||
),
|
||||
title: c.name,
|
||||
subtitle: c.subtitle,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ paddingBottom: 100 }}>
|
||||
<div style={{ padding: '12px 20px 12px' }}>
|
||||
<h1 className="h-screen" style={{ marginBottom: 14 }}>Чаты</h1>
|
||||
<div style={{
|
||||
background: '#fff', borderRadius: 14, padding: '10px 14px',
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
border: '1px solid var(--c-border)',
|
||||
}}>
|
||||
<I.search size={18} style={{ color: 'var(--c-fg-4)' }} />
|
||||
<input placeholder="Найти в чатах..." style={{ flex: 1, border: 0, outline: 0, fontSize: 15, background: 'transparent' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI — featured */}
|
||||
{ai && (
|
||||
<div style={{ padding: '0 16px 18px' }}>
|
||||
<button onClick={() => nav.push('chat:' + ai.id)} className="press" style={{
|
||||
width: '100%', padding: 16,
|
||||
background: 'linear-gradient(135deg, var(--c-primary-darker), var(--c-primary-dark))',
|
||||
color: '#fff', borderRadius: 20, textAlign: 'left',
|
||||
display: 'flex', gap: 14, alignItems: 'center',
|
||||
boxShadow: '0 10px 28px rgba(22,107,99,.22)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 52, height: 52, borderRadius: 14,
|
||||
background: 'rgba(255,255,255,0.18)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 26,
|
||||
}}>✨</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
|
||||
<span style={{ fontSize: 15, fontWeight: 700 }}>{ai.name}</span>
|
||||
<span style={{ padding: '2px 6px', borderRadius: 5, background: 'rgba(255,255,255,0.25)', fontSize: 9, fontWeight: 700, letterSpacing: .5 }}>AI</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, opacity: .85, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{ai.lastMessage}
|
||||
</div>
|
||||
</div>
|
||||
<I.chev size={18} style={{ color: 'rgba(255,255,255,0.8)', flexShrink: 0 }} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other chats */}
|
||||
<div style={{ padding: '0 16px' }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: .6, color: 'var(--c-fg-3)', padding: '0 4px 10px' }}>Все диалоги · {rest.length}</div>
|
||||
<div className="card" style={{ padding: 0 }}>
|
||||
{rest.map((c, i) => {
|
||||
const s = subjectFor(c);
|
||||
return (
|
||||
<React.Fragment key={c.id}>
|
||||
<button onClick={() => nav.push('chat:' + c.id)} className="press" style={{
|
||||
width: '100%', padding: '14px 16px', display: 'flex', gap: 12, alignItems: 'center', textAlign: 'left',
|
||||
}}>
|
||||
<div style={{ position: 'relative', flexShrink: 0 }}>
|
||||
{s.avatar}
|
||||
{c.online && <span style={{
|
||||
position: 'absolute', bottom: 1, right: 1,
|
||||
width: 12, height: 12, borderRadius: 999, background: 'var(--c-success)',
|
||||
border: '2px solid #fff',
|
||||
}} />}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10, marginBottom: 2 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--c-fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.title}</div>
|
||||
<div className="sub" style={{ fontSize: 11, flexShrink: 0, color: c.unread > 0 ? 'var(--c-primary-darker)' : 'var(--c-fg-3)', fontWeight: c.unread > 0 ? 700 : 400 }}>{c.lastTime}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
|
||||
<div style={{ fontSize: 13, color: c.unread > 0 ? 'var(--c-fg-1)' : 'var(--c-fg-3)', fontWeight: c.unread > 0 ? 600 : 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1, minWidth: 0 }}>{c.lastMessage}</div>
|
||||
{c.unread > 0 && (
|
||||
<span style={{
|
||||
background: 'var(--c-accent)', color: '#fff', fontSize: 11, fontWeight: 700,
|
||||
padding: '2px 7px', borderRadius: 999, flexShrink: 0, minWidth: 20, textAlign: 'center',
|
||||
}}>{c.unread}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{i < rest.length - 1 && <div style={{ height: 1, background: 'var(--c-divider)', marginLeft: 76 }} />}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button onClick={() => nav.push('doctors')} className="btn-g" style={{
|
||||
marginTop: 14, width: '100%', padding: 14, fontSize: 14,
|
||||
}}>
|
||||
<I.plus size={18} /> Новый чат с врачом
|
||||
</button>
|
||||
|
||||
<div style={{ marginTop: 14, padding: 12, background: 'var(--c-primary-50)', borderRadius: 12, display: 'flex', gap: 10, alignItems: 'flex-start' }}>
|
||||
<I.shield size={18} style={{ color: 'var(--c-primary-darker)', flexShrink: 0, marginTop: 2 }} />
|
||||
<div className="sub" style={{ fontSize: 12, lineHeight: 1.5 }}>
|
||||
Чаты с врачом доступны в течение 14 дней после приёма. Экстренные случаи — по телефону (342) 207-03-03.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Chat conversation
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
export function ChatConversationScreen({ nav, chatId }) {
|
||||
const chat = CLINIC_DATA.chats.find(c => c.id === chatId) || CLINIC_DATA.chats[0];
|
||||
const isAI = chat.kind === 'ai';
|
||||
const isOp = chat.kind === 'operator';
|
||||
const isDoc = chat.kind === 'doctor';
|
||||
const doc = isDoc ? CLINIC_DATA.doctors.find(d => d.id === chat.doctorId) : null;
|
||||
|
||||
const title = isDoc ? doc.name.split(' ').slice(0, 2).join(' ') : chat.name;
|
||||
const subtitle = isDoc
|
||||
? (chat.online ? 'Онлайн · отвечает 5 мин' : 'Был(а) в сети недавно')
|
||||
: isAI
|
||||
? (chat.online ? 'Онлайн · отвечает мгновенно' : chat.subtitle)
|
||||
: chat.subtitle;
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '12px 16px 10px', display: 'flex', gap: 10, alignItems: 'center',
|
||||
background: 'var(--c-bg)', borderBottom: '1px solid var(--c-border)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<button onClick={() => nav.pop()} className="press" style={{
|
||||
width: 38, height: 38, borderRadius: 999, background: '#fff',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: 'var(--sh-sm)', border: '1px solid var(--c-border)', flexShrink: 0,
|
||||
}}>
|
||||
<I.chevL size={20} />
|
||||
</button>
|
||||
<div style={{ position: 'relative', flexShrink: 0 }}>
|
||||
{isDoc && <Avatar init={doc.init} size={42} />}
|
||||
{!isDoc && (
|
||||
<div style={{
|
||||
width: 42, height: 42, borderRadius: 999,
|
||||
background: isAI
|
||||
? 'linear-gradient(135deg, var(--c-primary-darker), var(--c-primary-dark))'
|
||||
: 'var(--c-warm-100)',
|
||||
color: isAI ? '#fff' : 'var(--c-warm-text)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20,
|
||||
}}>{chat.icon}</div>
|
||||
)}
|
||||
{chat.online && <span style={{
|
||||
position: 'absolute', bottom: 0, right: 0,
|
||||
width: 11, height: 11, borderRadius: 999, background: 'var(--c-success)',
|
||||
border: '2px solid var(--c-bg)',
|
||||
}} />}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{title}</span>
|
||||
{isAI && <span style={{ padding: '1px 6px', borderRadius: 5, background: 'var(--c-primary-100)', color: 'var(--c-primary-darker)', fontSize: 9, fontWeight: 700, letterSpacing: .5 }}>AI</span>}
|
||||
</div>
|
||||
<div className="sub" style={{ fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{chat.online && <span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--c-success)' }} />}
|
||||
{subtitle}
|
||||
</div>
|
||||
</div>
|
||||
{isDoc && (
|
||||
<button onClick={() => nav.push('telemed')} className="btn-s" style={{ padding: 0, width: 38, height: 38, borderRadius: 999, flexShrink: 0 }}>
|
||||
<I.video size={16} />
|
||||
</button>
|
||||
)}
|
||||
{isOp && (
|
||||
<button className="btn-s" style={{ padding: 0, width: 38, height: 38, borderRadius: 999, flexShrink: 0 }}>
|
||||
<I.phone size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{chat.messages.map((m, i) => {
|
||||
const mine = m.from === 'me';
|
||||
return (
|
||||
<div key={i} style={{ alignSelf: mine ? 'flex-end' : 'flex-start', maxWidth: '80%' }}>
|
||||
<div style={{
|
||||
background: mine
|
||||
? 'var(--c-primary-darker)'
|
||||
: isAI
|
||||
? 'linear-gradient(135deg, #F2FAF9, #E3F4F2)'
|
||||
: '#fff',
|
||||
color: mine ? '#fff' : 'var(--c-fg-1)',
|
||||
padding: '10px 14px', borderRadius: 16,
|
||||
borderBottomRightRadius: mine ? 4 : 16,
|
||||
borderBottomLeftRadius: mine ? 16 : 4,
|
||||
fontSize: 14, lineHeight: 1.5,
|
||||
boxShadow: mine ? 'none' : 'var(--sh-sm)',
|
||||
border: !mine && isAI ? '1px solid var(--c-primary-200)' : 'none',
|
||||
}}>{m.t}</div>
|
||||
<div className="sub" style={{ fontSize: 11, marginTop: 3, textAlign: mine ? 'right' : 'left', padding: '0 4px' }}>{m.tm}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Suggested replies (AI only) */}
|
||||
{isAI && chat.suggestions && (
|
||||
<div style={{ padding: '4px 16px 8px', display: 'flex', gap: 8, overflowX: 'auto', flexShrink: 0 }} className="noscroll">
|
||||
{chat.suggestions.map((s, i) => (
|
||||
<button key={i} style={{
|
||||
flexShrink: 0, padding: '8px 14px', borderRadius: 999, fontSize: 13,
|
||||
background: '#fff', color: 'var(--c-primary-darker)',
|
||||
border: '1px solid var(--c-primary-200)', fontWeight: 600,
|
||||
}}>{s}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input */}
|
||||
<div style={{ padding: '10px 16px 100px', borderTop: '1px solid var(--c-border)', background: '#fff', flexShrink: 0 }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button style={{ width: 38, height: 38, borderRadius: 999, background: 'var(--c-bg)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<I.plus size={20} />
|
||||
</button>
|
||||
<div style={{
|
||||
flex: 1, background: 'var(--c-bg)', borderRadius: 999,
|
||||
padding: '10px 16px', fontSize: 14, color: 'var(--c-fg-4)',
|
||||
}}>
|
||||
{isAI ? 'Спросите что-нибудь...' : 'Сообщение...'}
|
||||
</div>
|
||||
<button style={{ width: 38, height: 38, borderRadius: 999, background: 'var(--c-primary-darker)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<I.mic size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -464,7 +464,7 @@ export function RecoveryScreen({ nav }) {
|
||||
<div className="sub" style={{ fontSize: 11 }}>Ваш хирург</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>{surgeon.name.split(' ').slice(0,2).join(' ')}</div>
|
||||
</div>
|
||||
<button onClick={() => nav.push('chat')} className="btn-s" style={{ padding: '8px 12px' }}>
|
||||
<button onClick={() => nav.push('chat:doctor-syndaev')} className="btn-s" style={{ padding: '8px 12px' }}>
|
||||
<I.chat size={15} /> Чат
|
||||
</button>
|
||||
</div>
|
||||
@@ -606,67 +606,6 @@ export function AudioTestScreen({ nav }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatTabScreen() {
|
||||
const msgs = [
|
||||
{ from: 'doc', t: 'Добрый день, Анна! Как самочувствие после операции?', tm: '14:02' },
|
||||
{ from: 'me', t: 'Здравствуйте! В целом хорошо, немного саднит в носу по утрам.', tm: '14:08' },
|
||||
{ from: 'doc', t: 'Это нормально на 6-й день. Продолжайте промывания Аква Марис 4 раза в день.', tm: '14:10' },
|
||||
{ from: 'doc', t: 'Выходите на осмотр сегодня? Я свободен после 15:00.', tm: '14:11' },
|
||||
{ from: 'me', t: 'Да, буду в 16:00 как запланировано.', tm: '14:14' },
|
||||
{ from: 'doc', t: 'Отлично, жду. Если что-то изменится — напишите.', tm: '14:15' },
|
||||
];
|
||||
const doc = CLINIC_DATA.doctors.find(d => d.id === 'syndaev');
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ padding: '12px 20px 12px' }}>
|
||||
<h1 className="h-screen">Чат</h1>
|
||||
</div>
|
||||
<div style={{ padding: '0 16px 12px' }}>
|
||||
<div className="card" style={{ display: 'flex', gap: 12, alignItems: 'center', padding: 12, background: 'var(--c-primary-50)' }}>
|
||||
<Avatar init={doc.init} size={44} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>{doc.name.split(' ').slice(0,2).join(' ')}</div>
|
||||
<div className="sub" style={{ fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--c-success)' }} />
|
||||
Онлайн · отвечает 5 мин
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn-s" style={{ padding: 10, borderRadius: 999 }}>
|
||||
<I.video size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 16px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{msgs.map((m,i)=>(
|
||||
<div key={i} style={{ alignSelf: m.from === 'me' ? 'flex-end' : 'flex-start', maxWidth: '78%' }}>
|
||||
<div style={{
|
||||
background: m.from === 'me' ? 'var(--c-primary-darker)' : '#fff',
|
||||
color: m.from === 'me' ? '#fff' : 'var(--c-fg-1)',
|
||||
padding: '10px 14px', borderRadius: 16,
|
||||
borderBottomRightRadius: m.from === 'me' ? 4 : 16,
|
||||
borderBottomLeftRadius: m.from === 'me' ? 16 : 4,
|
||||
fontSize: 14, lineHeight: 1.45,
|
||||
boxShadow: m.from === 'me' ? 'none' : 'var(--sh-sm)',
|
||||
}}>{m.t}</div>
|
||||
<div className="sub" style={{ fontSize: 11, marginTop: 3, textAlign: m.from === 'me' ? 'right' : 'left', padding: '0 4px' }}>{m.tm}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ padding: '10px 16px 100px', borderTop: '1px solid var(--c-border)', background: '#fff' }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button style={{ width: 38, height: 38, borderRadius: 999, background: 'var(--c-bg)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<I.plus size={20} />
|
||||
</button>
|
||||
<div style={{ flex: 1, background: 'var(--c-bg)', borderRadius: 999, padding: '10px 16px', fontSize: 14, color: 'var(--c-fg-4)' }}>Сообщение...</div>
|
||||
<button style={{ width: 38, height: 38, borderRadius: 999, background: 'var(--c-primary-darker)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<I.mic size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProfileTabScreen({ nav }) {
|
||||
const sections = [
|
||||
{
|
||||
|
||||
@@ -142,7 +142,8 @@ export function HomeV2Screen({ nav }) {
|
||||
{/* Clinic stats */}
|
||||
<div style={{ padding: '0 20px 16px' }}>
|
||||
<div className="card" style={{
|
||||
padding: 18, background: 'linear-gradient(135deg, var(--c-primary-100), var(--c-warm-100))', border: 0,
|
||||
padding: 18, background: 'linear-gradient(135deg, var(--c-primary-100), var(--c-warm-100))',
|
||||
border: '1.5px solid var(--c-primary-300)',
|
||||
}}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12 }}>Клиника УГН</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10 }}>
|
||||
|
||||
Reference in New Issue
Block a user