feat(web): screens from docs/mock — focus flow, today, meds, doctor, profile
- /flow renders the dialog engine's current step as a mock screen (consent,
age, onboarding and scales with progress, mood 0–10, free text, dose
reminder, crisis with hotlines and breathing, safety plan), so every
engine scenario gets the design without duplicating its logic
- today / meds / doctor / profile / messages tabs; navy sidebar and
emergency rail on desktop, emergency block and tab bar on phones
- API: GET /me/{token}/today (today's doses, week marks, next visit, demo
clock time); demo ЕМИАС sign-in seeds the persona's synthetic profile and
prescription from the dashboard fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+159
-47
@@ -1,64 +1,176 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { ReactNode } from "react";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
|
||||
import { ru } from "../content/ru";
|
||||
import { HelpSheet } from "./HelpSheet";
|
||||
import { Icon, LogoMark } from "./Icon";
|
||||
import { useDialog } from "../dialog/DialogContext";
|
||||
import { HotlineCard, useHelp } from "./HelpSheet";
|
||||
import { Icon, LogoMark, type IconName } from "./Icon";
|
||||
|
||||
export function AppShell({ children, footer = true }: { children: ReactNode; footer?: boolean }) {
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
/*
|
||||
* Layout from docs/mock (series 2).
|
||||
* phone: content → «Экстренная помощь» block → bottom tab bar (signed in)
|
||||
* desktop: navy sidebar | content | right rail (optional `aside` + emergency panel)
|
||||
*/
|
||||
|
||||
const TABS: { to: string; label: string; icon: IconName }[] = [
|
||||
{ to: "/today", label: ru.nav.today, icon: "home" },
|
||||
{ to: "/meds", label: ru.nav.meds, icon: "pill" },
|
||||
{ to: "/doctor", label: ru.nav.doctor, icon: "doctor" },
|
||||
{ to: "/profile", label: ru.nav.profile, icon: "user" },
|
||||
];
|
||||
|
||||
/** Inline emergency block shown at the bottom of every phone screen (hidden on desktop: the rail has it). */
|
||||
export function EmergencyBlock() {
|
||||
const { resources, open } = useHelp();
|
||||
return (
|
||||
<div className="shell">
|
||||
<header className="topbar">
|
||||
<div className="topbar__inner">
|
||||
<Link to="/" className="brand" aria-label={ru.product.name}>
|
||||
<LogoMark />
|
||||
<span className="brand__text">
|
||||
<span className="brand__name">{ru.product.name}</span>
|
||||
<span className="brand__tagline">{ru.product.tagline}</span>
|
||||
</span>
|
||||
</Link>
|
||||
<button type="button" className="help-btn" onClick={() => setHelpOpen(true)}>
|
||||
<Icon name="phone" size={18} />
|
||||
<span>{ru.help.button}</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<section className="sosblock" aria-label={ru.help.block}>
|
||||
<button type="button" className="sosblock__head" onClick={open}>
|
||||
<span className="sosblock__icon">
|
||||
<Icon name="phone" size={18} />
|
||||
</span>
|
||||
<span>{ru.help.block}</span>
|
||||
<Icon name="chevronRight" size={20} />
|
||||
</button>
|
||||
<div className="sosblock__lines">
|
||||
{resources.slice(0, 2).map((r) => (
|
||||
<HotlineCard key={r.phone} r={r} size="sm" />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
{children}
|
||||
|
||||
{footer && (
|
||||
<footer className="footer">
|
||||
<div className="footer__inner">
|
||||
<p className="footer__line">
|
||||
<Icon name="lock" size={16} />
|
||||
<span>{ru.footer.law}</span>
|
||||
</p>
|
||||
<p className="footer__line footer__line--muted">{ru.footer.prototype}</p>
|
||||
</div>
|
||||
</footer>
|
||||
function Sidebar({ signedIn }: { signedIn: boolean }) {
|
||||
const { open } = useHelp();
|
||||
const { unread } = useDialog();
|
||||
return (
|
||||
<nav className="sidebar" aria-label="Разделы">
|
||||
<Link to={signedIn ? "/today" : "/"} className="sidebar__brand">
|
||||
<LogoMark />
|
||||
<span>
|
||||
<span className="sidebar__name">{ru.product.name}</span>
|
||||
<span className="sidebar__tag">{ru.product.tagline}</span>
|
||||
</span>
|
||||
</Link>
|
||||
{signedIn ? (
|
||||
<ul className="sidebar__list">
|
||||
{TABS.map((t) => (
|
||||
<li key={t.to}>
|
||||
<NavLink to={t.to} className="sidebar__link">
|
||||
<Icon name={t.icon} />
|
||||
{t.label}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<NavLink to="/messages" className="sidebar__link">
|
||||
<Icon name="message" />
|
||||
{ru.nav.messages}
|
||||
{unread > 0 && <span className="dot" aria-label="новое" />}
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<ul className="sidebar__list">
|
||||
<li>
|
||||
<NavLink to="/" end className="sidebar__link">
|
||||
<Icon name="home" />
|
||||
{ru.nav.signIn}
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" className="sidebar__link" onClick={open}>
|
||||
<Icon name="question" />
|
||||
{ru.nav.help}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
<HelpSheet open={helpOpen} onClose={() => setHelpOpen(false)} />
|
||||
function BottomNav() {
|
||||
return (
|
||||
<nav className="tabbar" aria-label="Разделы">
|
||||
{TABS.map((t) => (
|
||||
<NavLink key={t.to} to={t.to} className="tabbar__item">
|
||||
<Icon name={t.icon} />
|
||||
<span>{t.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function Rail({ aside }: { aside?: ReactNode }) {
|
||||
const { resources, open } = useHelp();
|
||||
return (
|
||||
<aside className="rail">
|
||||
{aside}
|
||||
<section className="railsos" aria-label={ru.help.block}>
|
||||
<button type="button" className="railsos__head" onClick={open}>
|
||||
<Icon name="phone" size={18} />
|
||||
<span>{ru.help.block}</span>
|
||||
</button>
|
||||
{resources.map((r) => (
|
||||
<HotlineCard key={r.phone} r={r} size="sm" />
|
||||
))}
|
||||
</section>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppShell({
|
||||
children,
|
||||
signedIn = true,
|
||||
aside,
|
||||
focus = false,
|
||||
sos = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
signedIn?: boolean;
|
||||
aside?: ReactNode;
|
||||
/** Focus screens (one question per screen) hide the tab bar on phones, like in the mocks. */
|
||||
focus?: boolean;
|
||||
/** The crisis screen shows the hotlines itself, so it drops the bottom block. */
|
||||
sos?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={`app${signedIn && !focus ? " app--tabs" : ""}`}>
|
||||
<Sidebar signedIn={signedIn} />
|
||||
<div className="app__main">
|
||||
<main className="screen" id="main">
|
||||
{children}
|
||||
{sos && <EmergencyBlock />}
|
||||
</main>
|
||||
<Rail aside={aside} />
|
||||
</div>
|
||||
{signedIn && !focus && <BottomNav />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BackLink({ to, children }: { to: string; children: ReactNode }) {
|
||||
return (
|
||||
<Link to={to} className="back-link">
|
||||
<Icon name="arrowLeft" size={18} />
|
||||
<span>{children}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function Notice({ tone = "info", children }: { tone?: "info" | "warning"; children: ReactNode }) {
|
||||
export function Notice({ tone = "info", children }: { tone?: "info" | "warn" | "danger" | "ok"; children: ReactNode }) {
|
||||
const icon: IconName = tone === "info" ? "info" : tone === "ok" ? "check" : tone === "danger" ? "x" : "alert";
|
||||
return (
|
||||
<div className={`notice notice--${tone}`} role="note">
|
||||
<Icon name={tone === "warning" ? "alert" : "info"} size={20} />
|
||||
<p>{children}</p>
|
||||
<Icon name={icon} />
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopBar({ onBack, title, children }: { onBack?: () => void; title?: string; children?: ReactNode }) {
|
||||
return (
|
||||
<div className="topbar">
|
||||
{onBack && (
|
||||
<button type="button" className="icon-btn" onClick={onBack} aria-label={ru.common.back}>
|
||||
<Icon name="arrowLeft" />
|
||||
</button>
|
||||
)}
|
||||
{title && <span className="topbar__title">{title}</span>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user