diff --git a/web/src/content/ru.ts b/web/src/content/ru.ts
index 89b9a83..fb22275 100644
--- a/web/src/content/ru.ts
+++ b/web/src/content/ru.ts
@@ -48,6 +48,12 @@ export const ru = {
text: "Не нужны имя, полис и телефон, врач ничего не видит, история только на этом устройстве.",
},
disclaimer: "Сервис не ставит диагнозы и не назначает лечение. Решения о лечении принимает ваш врач.",
+ howTitle: "Как это работает",
+ how: [
+ { title: "Врач подключает", text: "На приёме вы получаете код или входите анонимно" },
+ { title: "Минута в день", text: "Напоминание о лекарстве и короткая отметка самочувствия" },
+ { title: "Врач видит динамику", text: "И получает сигнал, если состоянию нужно внимание" },
+ ],
},
emias: {
title: "Введите код от врача",
@@ -90,7 +96,11 @@ export const ru = {
viewMeds: "Посмотреть лекарства",
visit: "Следующий визит к врачу",
medQuestion: "Вопрос о лекарстве",
+ medQuestionHint: "Ответ по официальной инструкции",
messageDoctor: "Сообщение врачу",
+ messageDoctorHint: "Врач ответит в рабочее время",
+ week: "Приём на этой неделе",
+ weekTaken: (taken: number, total: number) => (total ? `Принято ${taken} из ${total}` : "Отметок пока нет"),
noDoctor: "Врач не подключён. Подключите ЕМИАС в профиле, чтобы назначения и визиты появились здесь.",
newMessage: "Новое сообщение",
open: "Открыть",
diff --git a/web/src/screens/AnonymousSignIn.tsx b/web/src/screens/AnonymousSignIn.tsx
index ccfe4a5..4736702 100644
--- a/web/src/screens/AnonymousSignIn.tsx
+++ b/web/src/screens/AnonymousSignIn.tsx
@@ -32,13 +32,20 @@ export function AnonymousSignIn() {
return (
-
+
navigate("/")} />
+
+
+
@@ -73,12 +80,13 @@ export function AnonymousSignIn() {
-
{t.warning}
{error &&
{ru.common.networkError} }
{busy ? t.submitting : t.submit}
+
+
);
diff --git a/web/src/screens/Doctor.tsx b/web/src/screens/Doctor.tsx
index 0aca7c8..4417812 100644
--- a/web/src/screens/Doctor.tsx
+++ b/web/src/screens/Doctor.tsx
@@ -32,7 +32,7 @@ export function Doctor() {
{plan && !plan.has_doctor ? (
{t.noDoctor}
) : (
- <>
+
@@ -65,7 +65,7 @@ export function Doctor() {
))}
{t.writeNote}
- >
+
)}
diff --git a/web/src/screens/EmiasSignIn.tsx b/web/src/screens/EmiasSignIn.tsx
index b5d601b..1e20fc1 100644
--- a/web/src/screens/EmiasSignIn.tsx
+++ b/web/src/screens/EmiasSignIn.tsx
@@ -59,13 +59,20 @@ export function EmiasSignIn() {
return (
-
+
navigate("/")} />
+
+
+
-
{t.demoNotice}
-
{t.or}
@@ -115,6 +120,8 @@ export function EmiasSignIn() {
))}
+
+
);
diff --git a/web/src/screens/Flow.tsx b/web/src/screens/Flow.tsx
index ecca256..e304fa3 100644
--- a/web/src/screens/Flow.tsx
+++ b/web/src/screens/Flow.tsx
@@ -322,70 +322,59 @@ export function Flow() {
const top = (
{step.kind === "choice" && step.progress && }
);
- let body: ReactNode;
+ // Phone: one column. Desktop: question and context on the left, answers and actions on the right.
+ let lead: ReactNode;
+ let act: ReactNode;
switch (step.kind) {
case "consent":
- body = (
+ lead = (
<>
-
- {step.buttons.map((b, i) => (
- press(b)}
- >
- {b.label}
-
- ))}
-
>
);
+ act = (
+
+ {step.buttons.map((b, i) => (
+ press(b)}
+ >
+ {b.label}
+
+ ))}
+
+ );
break;
case "number":
- body = (
- <>
-
-
- >
- );
+ lead =
;
+ act = ;
break;
case "actions":
- body = (
- <>
-
-
-
-
-
{step.title || ru.meds.reminderTitle}
-
-
-
- >
+ lead = (
+
+
+
+
+
{step.title || ru.meds.reminderTitle}
+
+
);
+ act = ;
break;
case "text":
- body = (
- <>
-
-
- >
+ lead =
;
+ act = (
+
);
break;
case "crisis":
- body = (
+ lead = (
<>
@@ -393,15 +382,19 @@ export function Flow() {
{ru.flow.supportTitle}
+
+
+
+ >
+ );
+ act = (
+ <>
{resources.map((r) => (
))}
-
-
-
{step.buttons.map((b) => (
press(b)}>
{b.label}
@@ -423,9 +416,9 @@ export function Flow() {
);
break;
case "plan":
- body = (
+ lead = ;
+ act = (
<>
-
{step.lines.map((l) => (
@@ -441,19 +434,20 @@ export function Flow() {
);
break;
case "choice":
- body = (
- <>
-
-
- >
- );
+ lead = ;
+ act = ;
break;
}
content = (
-
+
{top}
- {body}
- {dialog.error &&
{ru.common.networkError} }
+
+
{lead}
+
+ {act}
+ {dialog.error && {ru.common.networkError} }
+
+
);
}
diff --git a/web/src/screens/Meds.tsx b/web/src/screens/Meds.tsx
index 09fd452..fa6a5f4 100644
--- a/web/src/screens/Meds.tsx
+++ b/web/src/screens/Meds.tsx
@@ -1,5 +1,7 @@
import { useNavigate, useParams } from "react-router-dom";
+import type { TodayPrescription } from "../api/types";
+
import { AppShell, Notice, TopBar } from "../components/AppShell";
import { Icon, type IconName } from "../components/Icon";
import { ru } from "../content/ru";
@@ -13,6 +15,21 @@ const DOT: Record
= {
none: { cls: "wk" },
};
+export function WeekDots({ week }: { week: TodayPrescription["week"] }) {
+ return (
+
+ {week.map((w, i) => (
+
+ {ru.meds.week[i]}
+
+ {DOT[w.status].icon && }
+
+
+ ))}
+
+ );
+}
+
// E4 · Моя схема приёма
export function Meds() {
const t = ru.meds;
@@ -33,6 +50,7 @@ export function Meds() {
{plan && plan.prescriptions.length === 0 && {t.empty} }
+
{plan?.prescriptions.map((p) => (
@@ -55,18 +73,10 @@ export function Meds() {
))}
-
- {p.week.map((w, i) => (
-
- {t.week[i]}
-
- {DOT[w.status].icon && }
-
-
- ))}
-
+
))}
+
{plan && plan.prescriptions.length > 0 && {t.onlyDoctor} }
@@ -95,8 +105,10 @@ export function DoseReminder() {
return (
-
+
navigate("/today")} />
+
+
@@ -115,6 +127,8 @@ export function DoseReminder() {
)}
+
+
answer("med:taken", t.taken)}>
@@ -141,6 +155,8 @@ export function DoseReminder() {
{dialog.error &&
{ru.common.networkError} }
+
+
);
diff --git a/web/src/screens/Profile.tsx b/web/src/screens/Profile.tsx
index ed4cad3..7f05273 100644
--- a/web/src/screens/Profile.tsx
+++ b/web/src/screens/Profile.tsx
@@ -38,6 +38,7 @@ export function Profile() {
{t.title}
+
@@ -107,6 +108,7 @@ export function Profile() {
+
setConfirm(false)}>
diff --git a/web/src/screens/Today.tsx b/web/src/screens/Today.tsx
index 68f7d31..809f4f9 100644
--- a/web/src/screens/Today.tsx
+++ b/web/src/screens/Today.tsx
@@ -1,12 +1,13 @@
import { useNavigate } from "react-router-dom";
-import type { TodayDose } from "../api/types";
+import type { TodayDose, TodayPrescription } from "../api/types";
import { AppShell, Notice } from "../components/AppShell";
import { Icon } from "../components/Icon";
import { ru } from "../content/ru";
import { useDialog } from "../dialog/DialogContext";
import { classify } from "../dialog/step";
import { useSession } from "../session/SessionContext";
+import { WeekDots } from "./Meds";
import { humanDate, useToday } from "./useToday";
function VisitCard({ date, onOpen }: { date: string; onOpen: () => void }) {
@@ -24,6 +25,26 @@ function VisitCard({ date, onOpen }: { date: string; onOpen: () => void }) {
);
}
+function WeekCard({ rx, onOpen }: { rx: TodayPrescription; onOpen: () => void }) {
+ const taken = rx.week.filter((w) => w.status === "taken").length;
+ const counted = rx.week.filter((w) => w.status !== "none").length;
+ return (
+
+
+
+
+
+
+ {ru.today.week}
+ {ru.today.weekTaken(taken, counted)}
+
+
+
+
+
+ );
+}
+
// C1 / C2 · Сегодня
export function Today() {
const t = ru.today;
@@ -43,20 +64,27 @@ export function Today() {
const takenDoses = plan?.doses.filter((d) => d.status === "taken") ?? [];
const allDone = plan !== null && pendingDoses.length === 0 && plan.checkin_done && !hasOpenStep;
const nextDose = plan?.prescriptions.flatMap((p) => p.times).sort()[0];
+ const noDoctor = session?.mode === "anonymous" || (plan !== null && !plan.has_doctor);
const now = plan?.now ?? "00:00";
const openDose = (d: TodayDose) => navigate(`/dose/${d.id}`);
const visit = plan?.next_visit ? navigate("/doctor")} /> : null;
+ const hasDone = takenDoses.length > 0 || plan?.checkin_done;
return (
-
-
+
+
{t.title}
{plan &&
{humanDate(plan.today)}
}
- navigate("/messages")} aria-label={ru.nav.messages}>
+ navigate("/messages")}
+ aria-label={ru.nav.messages}
+ >
{dialog.unread > 0 && }
@@ -64,113 +92,147 @@ export function Today() {
{failed && {ru.common.networkError} }
- {hasOpenStep && (
- navigate("/flow")}>
-
-
-
-
- {t.newMessage}
- {dialog.pending[0]?.text.split("\n")[0].slice(0, 80)}
-
- {t.open}
-
- )}
+
+ {hasOpenStep && (
+ navigate("/flow")}>
+
+
+
+
+ {t.newMessage}
+ {dialog.pending[0]?.text.split("\n")[0].slice(0, 90)}
+
+ {t.open}
+
+ )}
- {allDone ? (
-
-
-
-
- {t.allDoneTitle}
- {nextDose ? t.allDoneNext(`завтра в ${nextDose}`) : t.allDoneNoMeds}
- {plan && plan.prescriptions.length > 0 && (
- navigate("/meds")}>
- {t.viewMeds}
-
-
- )}
-
- ) : (
-
- {pendingDoses.map((d) => (
+ {allDone ? (
+
+
+
+
+
{t.allDoneTitle}
+
{nextDose ? t.allDoneNext(`завтра в ${nextDose}`) : t.allDoneNoMeds}
+ {plan && plan.prescriptions.length > 0 && (
+
navigate("/meds")}>
+ {t.viewMeds}
+
+
+ )}
+
+ ) : (
+
+ {pendingDoses.map((d) => (
+ openDose(d)}
+ >
+
+
+
+
+ {t.takeMed}
+
+ {d.time} · {d.drug}, {d.dose}
+
+
+ {d.time <= now && {t.now} }
+
+
+ ))}
+ {plan && !plan.checkin_done && (
+ go("menu:checkin", t.checkin)}
+ disabled={dialog.busy}
+ >
+
+
+
+
+ {t.checkin}
+ {t.checkinHint}
+
+
+
+ )}
+
+ )}
+
+
+
+
+ {plan?.next_visit && (
+
+ navigate("/doctor")} />
+
+ )}
+
+ {!allDone && hasDone && (
+
+
{t.done}
+ {takenDoses.map((d) => (
+
+
+
+
+
+ {t.medTaken}
+ {t.medTakenAt(d.time)}
+
+
+ ))}
+ {plan?.checkin_done && (
+
+
+
+
+ {t.checkinDone}
+
+ )}
+
+ )}
+
+ {plan?.prescriptions[0] && navigate("/meds")} />}
+
+ {noDoctor ? (
+ {t.noDoctor}
+ ) : (
+
openDose(d)}
+ className="quick__btn"
+ onClick={() => go("menu:med_question", t.medQuestion)}
+ disabled={dialog.busy}
>
-
-
-
-
- {t.takeMed}
- {d.time}
-
- {d.time <= now && {t.now} }
-
-
- ))}
- {plan && !plan.checkin_done && (
- go("menu:checkin", t.checkin)} disabled={dialog.busy}>
-
-
-
-
- {t.checkin}
- {t.checkinHint}
-
-
-
- )}
-
- )}
-
- {!allDone && (takenDoses.length > 0 || plan?.checkin_done) && (
-
- {t.done}
- {takenDoses.map((d) => (
-
-
-
+
+
- {t.medTaken}
- {t.medTakenAt(d.time)}
+ {t.medQuestion}
+ {t.medQuestionHint}
-
- ))}
- {plan?.checkin_done && (
-
-
-
+
+ go("menu:contact_doctor", t.messageDoctor)}
+ disabled={dialog.busy}
+ >
+
+
- {t.checkinDone}
-
- )}
-
- )}
-
- {plan?.next_visit && (
-
- navigate("/doctor")} />
-
- )}
-
- {session?.mode === "anonymous" || (plan && !plan.has_doctor) ? (
- {t.noDoctor}
- ) : (
-
- go("menu:med_question", t.medQuestion)} disabled={dialog.busy}>
-
- {t.medQuestion}
-
- go("menu:contact_doctor", t.messageDoctor)} disabled={dialog.busy}>
-
- {t.messageDoctor}
-
-
- )}
+
+ {t.messageDoctor}
+ {t.messageDoctorHint}
+
+
+
+ )}
+
);
diff --git a/web/src/screens/Welcome.tsx b/web/src/screens/Welcome.tsx
index 8cc98b4..abe81fc 100644
--- a/web/src/screens/Welcome.tsx
+++ b/web/src/screens/Welcome.tsx
@@ -46,6 +46,23 @@ export function Welcome() {
+
+
+ {t.howTitle}
+
+
+ {t.how.map((step, i) => (
+
+ {i + 1}
+
+ {step.title}
+ {step.text}
+
+
+ ))}
+
+
+
{t.disclaimer}
diff --git a/web/src/theme/global.css b/web/src/theme/global.css
index 6baaa0e..62b7500 100644
--- a/web/src/theme/global.css
+++ b/web/src/theme/global.css
@@ -788,7 +788,9 @@ button.row-card:hover:not(:disabled) {
.num {
aspect-ratio: 1;
- min-height: 30px;
+ width: 100%;
+ max-width: 52px;
+ justify-self: center;
display: grid;
place-items: center;
padding: 0;
@@ -1170,22 +1172,128 @@ button.row-card:hover:not(:disabled) {
.quick__btn {
display: flex;
align-items: center;
- gap: var(--s-2);
- min-height: 64px;
+ gap: var(--s-3);
+ min-height: 72px;
padding: var(--s-3);
border: 1px solid var(--c-border);
border-radius: var(--r-tile);
background: var(--c-surface);
text-align: left;
- font-size: var(--fs-14);
- font-weight: 600;
color: var(--c-navy);
}
+.quick__title {
+ display: block;
+ font-size: var(--fs-14);
+ font-weight: 700;
+}
+
+.quick__sub {
+ display: none;
+ font-size: var(--fs-13);
+ color: var(--c-text-2);
+}
+
.quick__btn svg {
flex: none;
}
+/* ---------------------------------------------------------------- shared blocks: focus, dashboard, grids */
+
+.focus,
+.split,
+.split__lead,
+.split__act {
+ display: grid;
+ gap: var(--s-5);
+ align-content: start;
+}
+
+.dash,
+.dash__main,
+.dash__side {
+ display: grid;
+ gap: var(--s-4);
+ align-content: start;
+}
+
+.card {
+ display: grid;
+ gap: var(--s-3);
+ width: 100%;
+ padding: var(--s-4);
+ border: 1px solid var(--c-border);
+ border-radius: var(--r-tile);
+ background: var(--c-surface);
+ text-align: left;
+ color: var(--c-text);
+}
+
+.card__head {
+ display: flex;
+ align-items: center;
+ gap: var(--s-3);
+}
+
+.card__head > svg:last-child {
+ flex: none;
+ color: var(--c-text-3);
+}
+
+.grid2,
+.cols {
+ display: grid;
+ gap: var(--s-4);
+}
+
+.btn--auto {
+ width: auto;
+ justify-self: center;
+}
+
+.how {
+ display: grid;
+ gap: var(--s-3);
+}
+
+.how__list {
+ display: grid;
+ gap: var(--s-3);
+}
+
+.how__item {
+ display: flex;
+ gap: var(--s-3);
+ align-items: flex-start;
+ padding: var(--s-4);
+ border-radius: var(--r-tile);
+ background: var(--c-tile);
+}
+
+.how__num {
+ flex: none;
+ width: 32px;
+ height: 32px;
+ display: grid;
+ place-items: center;
+ border-radius: 50%;
+ background: var(--c-navy);
+ color: #fff;
+ font-weight: 700;
+ font-size: var(--fs-14);
+}
+
+.how__title {
+ display: block;
+ font-weight: 700;
+}
+
+.how__text {
+ display: block;
+ font-size: var(--fs-13);
+ color: var(--c-text-2);
+}
+
/* ---------------------------------------------------------------- meds */
.rx {
@@ -1717,19 +1825,152 @@ button.row-card:hover:not(:disabled) {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--rail-w);
gap: var(--s-6);
- max-width: 1180px;
+ max-width: 1320px;
padding: var(--s-8) var(--s-8) var(--s-8) var(--s-10);
}
.screen {
- max-width: 760px;
+ max-width: none;
margin: 0;
- padding: var(--s-8);
+ padding: 0;
+ gap: var(--s-6);
+ align-self: start;
+ }
+
+ /* Tab screens and sign-in: the page content sits on one wide white card. */
+ .screen > .stack {
+ padding: var(--s-8) var(--s-10);
border-radius: 24px;
background: var(--c-surface);
box-shadow: 0 1px 2px rgb(15 39 71 / 0.06);
- align-self: start;
- min-height: 560px;
+ }
+
+ /* Focus screens: question on the left, answers on the right. */
+ .focus,
+ .done {
+ padding: var(--s-8) var(--s-10) var(--s-10);
+ border-radius: 24px;
+ background: var(--c-surface);
+ box-shadow: 0 1px 2px rgb(15 39 71 / 0.06);
+ }
+
+ .done {
+ max-width: none;
+ justify-items: center;
+ }
+
+ .done > .panel,
+ .done > .btn {
+ width: min(100%, 520px);
+ }
+
+ .split {
+ grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
+ gap: var(--s-12);
+ align-items: start;
+ }
+
+ .split__lead {
+ position: sticky;
+ top: var(--s-8);
+ }
+
+ .split__act {
+ padding: var(--s-6);
+ border-radius: 20px;
+ background: var(--c-tile);
+ }
+
+ .split__act .option,
+ .split__act .row-card,
+ .split__act .textarea textarea,
+ .split__act .code-input {
+ background: var(--c-surface);
+ }
+
+ .split--crisis .split__act,
+ .split--plan .split__act {
+ background: transparent;
+ padding: 0;
+ }
+
+ .btn-row--stack-desk {
+ grid-template-columns: 1fr;
+ }
+
+ /* Wide content (consent text, 0–10 scale) reads better in one centred column. */
+ .split--consent,
+ .split--number {
+ grid-template-columns: minmax(0, 1fr);
+ max-width: 760px;
+ gap: var(--s-6);
+ }
+
+ .split--consent .split__lead,
+ .split--number .split__lead {
+ position: static;
+ }
+
+ .split--consent .btn-row--stack-desk {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .split--number .numscale__row {
+ gap: var(--s-2);
+ }
+
+ .num {
+ font-size: var(--fs-16);
+ }
+
+ .focus .h1 {
+ font-size: var(--fs-28);
+ }
+
+ .panel--scroll {
+ max-height: 56vh;
+ }
+
+ /* Today: dashboard. */
+ .dash {
+ grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
+ gap: var(--s-6);
+ align-items: start;
+ }
+
+ .dash__head {
+ grid-column: 1 / -1;
+ padding: 0 var(--s-2);
+ }
+
+ .dash__main {
+ padding: var(--s-6);
+ border-radius: 24px;
+ background: var(--c-surface);
+ box-shadow: 0 1px 2px rgb(15 39 71 / 0.06);
+ }
+
+ .dash__side .card {
+ border-color: transparent;
+ box-shadow: 0 1px 2px rgb(15 39 71 / 0.06);
+ }
+
+ .grid2,
+ .cols {
+ grid-template-columns: 1fr 1fr;
+ align-items: start;
+ }
+
+ .dash__side .quick {
+ grid-template-columns: 1fr;
+ }
+
+ .cols > .notice {
+ grid-column: 1 / -1;
+ }
+
+ .how__list {
+ grid-template-columns: repeat(3, 1fr);
}
.rail {
@@ -1769,6 +2010,15 @@ button.row-card:hover:not(:disabled) {
font-size: var(--fs-34);
}
+ .quick__sub {
+ display: block;
+ }
+
+ .quick__btn {
+ min-height: 84px;
+ padding: var(--s-4);
+ }
+
.entry-grid {
grid-template-columns: 1fr 1fr;
}