From 40c242bb1abee9ada3b25606bed1edc93802310d Mon Sep 17 00:00:00 2001 From: Arxip222 Date: Sun, 4 Oct 2026 08:25:17 +0300 Subject: [PATCH] feat(web): full desktop layout instead of a stretched phone view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - focus screens: question and context on the left, answers on the right in one wide card; consent text and the 0–10 scale in one wide column - today: dashboard with tasks, this week's doses and quick actions side by side; meds / doctor / profile in two-column grids; welcome gets «Как это работает» steps - fix: 0–10 scale no longer overflows narrow containers Co-Authored-By: Claude Opus 5.5 --- web/src/content/ru.ts | 10 ++ web/src/screens/AnonymousSignIn.tsx | 14 +- web/src/screens/Doctor.tsx | 4 +- web/src/screens/EmiasSignIn.tsx | 15 +- web/src/screens/Flow.tsx | 112 ++++++------ web/src/screens/Meds.tsx | 38 ++-- web/src/screens/Profile.tsx | 2 + web/src/screens/Today.tsx | 268 ++++++++++++++++----------- web/src/screens/Welcome.tsx | 17 ++ web/src/theme/global.css | 270 ++++++++++++++++++++++++++-- 10 files changed, 558 insertions(+), 192 deletions(-) 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("/")} /> +
+
+ + +

{t.title}

{t.lead}

- + {t.warning} +
+

@@ -73,12 +80,13 @@ export function AnonymousSignIn() {

- {t.warning} {error && {ru.common.networkError}} +
+
); 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.title}

{t.lead}

- + {t.demoNotice} +
+
+
); 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) => ( - - ))} -
); + act = ( +
+ {step.buttons.map((b, i) => ( + + ))} +
+ ); 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) => (
{plan && plan.prescriptions.length > 0 && {t.onlyDoctor}} @@ -95,8 +105,10 @@ export function DoseReminder() { return ( -
+
navigate("/today")} /> +
+
@@ -115,6 +127,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 ( + + ); +} + // 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)}

}
- @@ -64,113 +92,147 @@ export function Today() { {failed && {ru.common.networkError}} - {hasOpenStep && ( - - )} +
+ {hasOpenStep && ( + + )} - {allDone ? ( -
- -

{t.allDoneTitle}

-

{nextDose ? t.allDoneNext(`завтра в ${nextDose}`) : t.allDoneNoMeds}

- {plan && plan.prescriptions.length > 0 && ( - - )} -
- ) : ( -
- {pendingDoses.map((d) => ( + {allDone ? ( +
+ +

{t.allDoneTitle}

+

{nextDose ? t.allDoneNext(`завтра в ${nextDose}`) : t.allDoneNoMeds}

+ {plan && plan.prescriptions.length > 0 && ( + + )} +
+ ) : ( +
+ {pendingDoses.map((d) => ( + + ))} + {plan && !plan.checkin_done && ( + + )} +
+ )} + +
+ +
+ {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} + ) : ( +
- ))} - {plan && !plan.checkin_done && ( - - )} -
- )} - - {!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 && ( -
- - + +
- )} -
- )} - - {plan?.next_visit && ( -
- navigate("/doctor")} /> -
- )} - - {session?.mode === "anonymous" || (plan && !plan.has_doctor) ? ( - {t.noDoctor} - ) : ( -
- - -
- )} + + {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) => ( +
  1. + {i + 1} + + {step.title} + {step.text} + +
  2. + ))} +
+
+

{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; }